Skip to main content

Command Palette

Search for a command to run...

Infrastructure as Conversation: Collaborating with Docker, Terraform & Packer

How spec-driven development with Kiro transforms infrastructure from siloed tasks into transparent, reviewable dialogues

Published
14 min read
Infrastructure as Conversation: Collaborating with Docker, Terraform & Packer

The 3 AM Incident That Changed Everything

Picture this: It's 3 AM. Your phone buzzes with PagerDuty alerts. Production is down. You scramble to check — and realize nobody knows who changed what. The last deployment was "manual." The infrastructure looks nothing like what's in the repo. Sound familiar?

This scenario plays out in organizations every single day. And it's not because engineers are careless — it's because infrastructure has traditionally been built in isolation.

What if every infrastructure change was a conversation instead?

What if instead of shadow changes and "who touched production?" mysteries, every modification was proposed, discussed, reviewed, and approved — just like application code?

That's exactly what Infrastructure as Conversation is about.


What is Infrastructure as Conversation?

Infrastructure as Conversation is a collaborative approach where every infrastructure change flows through transparent, reviewable dialogues. It's built on three pillars:

Pillar Tool Conversation
🐳 Golden Docker Images HashiCorp Packer "Let's agree on our starting point"
🏗️ Shared Infrastructure Modules HashiCorp Terraform "Let's speak the same language"
🔄 PR-Driven Workflows GitHub Actions "Let's review before we ship"

Each tool addresses a specific collaboration gap:

  • Packer eliminates "works on my machine" by giving teams a single, versioned, peer-reviewed base image
  • Terraform modules create a shared language between platform teams (who build) and app teams (who consume)
  • PR workflows make every change visible, auditable, and discussable

Let me walk you through each pillar with real code — and show you how Kiro's spec-driven development makes the entire workflow even more powerful.


Chapter 1: Golden Docker Images with Packer

The Problem: Environment Drift

Every team I've worked with has faced this: Developer A has Node.js 18, Developer B has Node.js 20, and production is running Node.js 16 because nobody updated the base image in 8 months. The result? "It works on my machine" becomes the team's unofficial motto.

The Solution: Golden Images as Code

A golden image is a standardized, versioned base Docker image that serves as the consistent starting point for all services. Built with Packer, stored in Amazon ECR, and updated through PR-based review workflows.

Here's the key insight: image definitions are code, and code gets reviewed.

Spec-Driven Image Definition with Kiro

Before writing a single line of HCL, I start with a specification in Kiro:

# Spec: Golden Docker Image

## Requirements

### Base Image
- **Source**: `ubuntu:22.04` (LTS)
- **Architecture**: `linux/amd64`

### Pre-installed Software
| Software | Version | Purpose |
|----------|---------|---------|
| Node.js | 20.x LTS | Application runtime |
| AWS CLI v2 | Latest | AWS service interaction |
| curl | Latest | Health checks & debugging |

### Security Requirements
- [ ] Non-root user (`appuser`) with UID 1001
- [ ] Minimal package footprint
- [ ] No secrets baked into the image
- [ ] Health check endpoint on `/healthz`

### Acceptance Criteria
- [ ] Image builds successfully with `packer build`
- [ ] Image size < 250MB
- [ ] All security scans pass (no HIGH/CRITICAL CVEs)

This spec IS the conversation. It documents what the team agreed on. When someone wants to add Python or change the base OS, they update the spec first — and that change goes through review.

The Packer Template

Kiro generates the implementation from the spec. Here's the Packer template:

packer {
  required_plugins {
    docker = {
      version = ">= 1.1.0"
      source  = "github.com/hashicorp/docker"
    }
  }
}

variable "image_version" {
  type        = string
  default     = "1.0.0"
  description = "Semantic version for the golden image"
}

variable "aws_account_id" {
  type        = string
  default     = "123456789012"
  description = "AWS Account ID"
}

locals {
  ecr_url   = "${var.aws_account_id}.dkr.ecr.us-east-1.amazonaws.com"
  timestamp = formatdate("YYYY-MM-DD'T'hh:mm:ss", timestamp())
}

source "docker" "ubuntu" {
  image  = "ubuntu:22.04"
  commit = true
  changes = [
    "USER appuser",
    "WORKDIR /app",
    "EXPOSE 3000",
    "HEALTHCHECK --interval=30s --timeout=3s CMD curl -f http://localhost:3000/healthz || exit 1",
    "ENTRYPOINT [\"node\"]",
    "LABEL maintainer=platform-team",
    "LABEL version=${var.image_version}",
    "LABEL build-date=${local.timestamp}"
  ]
}

build {
  name    = "golden-image"
  sources = ["source.docker.ubuntu"]

  # System packages
  provisioner "shell" {
    inline = [
      "apt-get update -qq",
      "apt-get install -y --no-install-recommends curl wget ca-certificates gnupg",
      "rm -rf /var/lib/apt/lists/*"
    ]
  }

  # Node.js 20.x LTS
  provisioner "shell" {
    inline = [
      "curl -fsSL https://deb.nodesource.com/setup_20.x | bash -",
      "apt-get install -y nodejs"
    ]
  }

  # Non-root user (security best practice)
  provisioner "shell" {
    inline = [
      "groupadd -g 1001 appgroup",
      "useradd -u 1001 -g appgroup -m -s /bin/bash appuser",
      "mkdir -p /app && chown -R appuser:appgroup /app"
    ]
  }

  # Health check endpoint
  provisioner "shell" {
    inline = [
      "cat > /app/healthz.js << 'EOF'",
      "const http = require('http');",
      "const server = http.createServer((req, res) => {",
      "  if (req.url === '/healthz') {",
      "    res.writeHead(200, { 'Content-Type': 'application/json' });",
      "    res.end(JSON.stringify({ status: 'healthy', version: '${var.image_version}' }));",
      "  } else { res.writeHead(404); res.end(); }",
      "});",
      "server.listen(3000, () => console.log('Health check ready on :3000'));",
      "EOF",
      "chown appuser:appgroup /app/healthz.js"
    ]
  }

  # Tag and push to ECR
  post-processors {
    post-processor "docker-tag" {
      repository = "${local.ecr_url}/golden"
      tags       = ["v${var.image_version}", "latest"]
    }
    post-processor "docker-push" {
      ecr_login    = true
      login_server = local.ecr_url
    }
  }
}

Why This Matters

Every line of this template is:

  • Version-controlled → Git tracks every change
  • Peer-reviewed → PRs require approval before merge
  • Auditable → Labels record version, date, and commit SHA
  • Reproducible → Same template = same image, every time

The golden image becomes your team's shared starting point — agreed upon through conversation, not imposed in isolation.


Chapter 2: Terraform Modules for Container Infrastructure

The Problem: Infrastructure Knowledge Silos

Platform engineers understand VPCs, IAM roles, and ECS task definitions. Application developers understand their service, its ports, and its resource needs. The gap between these two worlds creates bottlenecks, misconfigurations, and frustration.

The Solution: Modules as a Shared Language

Well-designed Terraform modules bridge this gap. They become a shared language where:

  • Platform teams encode their expertise into reusable modules
  • Application teams consume those modules with simple, well-documented inputs
  • Governance is built-in, not bolted-on

The ECS Service Module

Here's a composable module for deploying any service on AWS ECS Fargate:

# modules/ecs-service/variables.tf

variable "service_name" {
  type        = string
  description = "Name of the ECS service"

  validation {
    condition     = can(regex("^[a-z][a-z0-9-]+$", var.service_name))
    error_message = "Service name must be lowercase alphanumeric with hyphens."
  }
}

variable "image" {
  type        = string
  description = "Docker image URI from ECR"
}

variable "cpu" {
  type        = number
  default     = 256
  description = "CPU units for the Fargate task"

  validation {
    condition     = contains([256, 512, 1024, 2048, 4096], var.cpu)
    error_message = "CPU must be a valid Fargate value: 256, 512, 1024, 2048, or 4096."
  }
}

variable "memory" {
  type    = number
  default = 512
}

variable "container_port" {
  type    = number
  default = 3000
}

variable "desired_count" {
  type    = number
  default = 2
}

variable "health_check_path" {
  type    = string
  default = "/healthz"
}

variable "vpc_id" {
  type = string
}

variable "subnet_ids" {
  type = list(string)
}

Notice the input validations — they're governance guardrails baked into the module. An app team can't accidentally request an invalid Fargate CPU value. The naming convention is enforced. The module teaches best practices through its interface.

How App Teams Consume It

This is where the magic happens. An application team deploys their service with just a few lines:

# environments/prod/main.tf

module "payment_api" {
  source = "../../modules/ecs-service"

  service_name      = "payment-api"
  image             = "123456.dkr.ecr.us-east-1.amazonaws.com/golden:v1.0.0"
  cpu               = 256
  memory            = 512
  container_port    = 3000
  desired_count     = 2
  health_check_path = "/healthz"

  vpc_id     = data.aws_vpc.default.id
  subnet_ids = data.aws_subnets.default.ids
}

That's it. 8 lines of meaningful configuration to deploy a fully governed, observable, secure ECS service. Behind the scenes, the module creates:

  • ECS Cluster with Container Insights
  • Task Definition with health checks and logging
  • IAM roles (least-privilege execution + task roles)
  • Security groups
  • CloudWatch Log Group (30-day retention)

The module is the conversation between platform and app teams. Platform teams say: "Here's how we run containers at our org." App teams respond: "Great, here's my service — deploy it."

Design Principles for Shareable Modules

Principle Implementation
🧱 Composable Single-purpose, can be combined with others
📝 Versioned Semantic versioning, published to registry
🔒 Governed Input validation + policy as code
🔄 Reusable Works across teams, environments, services
📋 Documented Clear variable descriptions + outputs

Chapter 3: PR-Driven Infrastructure Collaboration

The Problem: Shadow Changes

In traditional infrastructure management, changes happen in the dark:

  • Someone SSH'd into a server and tweaked a config
  • A manual console change was made "just to test" and never reverted
  • A Terraform apply was run from someone's laptop with no audit trail

These shadow changes are the infrastructure equivalent of whispering in a meeting — the rest of the team is left in the dark.

The Solution: Every Change is a Conversation

PR-driven infrastructure workflows make every change visible, reviewable, and discussable:

# .github/workflows/terraform-plan.yml

name: 🏗️ Terraform Plan Preview

on:
  pull_request:
    branches: [main]
    paths:
      - 'modules/**'
      - 'environments/**'

jobs:
  terraform-plan:
    name: Plan & Review
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: ${{ secrets.AWS_ROLE_ARN }}
          aws-region: us-east-1

      - uses: hashicorp/setup-terraform@v3

      - name: Terraform Plan
        id: plan
        run: terraform plan -no-color -out=tfplan 2>&1 | tee plan_output.txt
        working-directory: environments/prod

      - name: 💬 Post Plan to PR
        uses: actions/github-script@v7
        with:
          script: |
            const plan = require('fs').readFileSync('environments/prod/plan_output.txt', 'utf8');
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: `## ✅ Terraform Plan Preview
              
              | Check | Status |
              |-------|--------|
              | Format | ✅ Passed |
              | Validate | ✅ Passed |
              | Plan | ✅ Passed |
              | Security | 🛡️ Passed |
              | Cost | 💰 +$12.50/mo |
              
              <details><summary>📋 Plan Output</summary>
              
              \`\`\`hcl
              ${plan.substring(0, 60000)}
              \`\`\`
              </details>
              
              💬 *This is your infrastructure conversation. 
              Review the plan, discuss changes, and approve when ready.*`
            });

  security-scan:
    name: 🛡️ Security Scan
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: aquasecurity/tfsec-action@v1.0.3
        with:
          working_directory: modules/

  cost-estimate:
    name: 💰 Cost Estimate
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: infracost/actions/setup@v3
        with:
          api-key: ${{ secrets.INFRACOST_API_KEY }}
      - run: infracost breakdown --path=environments/prod

What the PR Conversation Looks Like

When someone opens a PR to add a new service, the automated checks fire immediately:

## ✅ Terraform Plan Preview

Environment: prod | Triggered by: @vishal

| Check | Status |
|-------|--------|
| Format | ✅ Passed |
| Validate | ✅ Passed |
| Plan | ✅ 3 to add, 0 to change, 0 to destroy |
| Security | 🛡️ No issues found |
| Cost | 💰 +$12.50/month |

💬 This is your infrastructure conversation. 
Review the plan, discuss changes, and approve when ready.

This IS the conversation. Everyone can see:

  • What is being changed (plan output)
  • Whether it's safe (security scan)
  • How much it costs (cost estimate)
  • Who approved it (peer review)

No more shadow changes. No more "who touched production?" at 3 AM.


The Kiro Advantage: Spec-Driven Infrastructure

Here's where it gets really interesting. Kiro brings spec-driven development to infrastructure code — the same approach that works brilliantly for application development.

The Three-Phase Flow

📋 SPEC → 🏗️ DESIGN → ⚡ IMPLEMENT
  1. Spec: Define what you want (golden image requirements, module interface, pipeline behavior)
  2. Design: Kiro generates architecture, data flow, and task breakdown
  3. Implement: Kiro generates code that satisfies the spec — with acceptance criteria as validation gates

Why Spec-Driven IaC Matters

Traditional IaC development often starts with code:

"Let me write a Terraform module and figure out the interface as I go."

Spec-driven IaC starts with intent:

"Here's what our team needs, here are the constraints, here are the acceptance criteria — now generate the implementation."

This is particularly powerful for infrastructure because:

  1. Specs capture the "why" — Code shows how something is built; specs document why those decisions were made
  2. Specs enable collaboration — Non-IaC engineers can review and contribute to specs without knowing HCL
  3. Specs prevent drift — When the spec and the code diverge, you know something's wrong
  4. Specs enable AI assistance — Kiro can generate, validate, and update implementations when specs change

Kiro Project Spec Example

# Kiro Spec: Infrastructure as Conversation

## Project Context
This repository demonstrates collaborative infrastructure management 
using Docker, Packer, and Terraform on AWS.

## Key Principles
1. Everything as Code
2. Spec-Driven
3. Peer-Reviewed
4. Composable
5. Transparent

## Demo Flow
1. Open Kiro → Show specs → Generate Packer template
2. Build golden image → Push to ECR
3. Use shared Terraform module → Provision ECS service
4. Open PR → Plan preview → Approve → Merge

When this spec lives in .kiro/specs/, Kiro understands the full project context and can assist with any component — from generating new modules to updating existing templates when requirements change.


The End-to-End Workflow

Let's connect all three chapters into one seamless pipeline:

┌─────────────┐    ┌──────────────┐    ┌─────────────────┐    ┌──────────────┐
│  Kiro Specs  │───▶│ Packer Build │───▶│  ECR Registry   │───▶│  ECS Service  │
│  (markdown)  │    │ (golden img) │    │ (golden:v1.x)   │    │ (Terraform)   │
└─────────────┘    └──────────────┘    └─────────────────┘    └──────────────┘
       │                                                              │
       └──────────────── PR Review ◀── Plan Preview ◀─────────────────┘

The flow:

  1. Define specs in Kiro (what the team agrees on)
  2. Kiro generates Packer template → builds golden Docker image
  3. Image is pushed to Amazon ECR with immutable tags
  4. Terraform module references the golden image → provisions ECS Fargate
  5. Every change goes through PR → plan preview → security scan → cost estimate → peer approval
  6. Merge auto-applies → transparent, auditable deployment

Every step is a conversation. Every change is visible. Every decision is documented.


Running the Demo with Kiro

Here's how you can reproduce this entire workflow in under 10 minutes:

Act 1: Golden Image (~3 min)

# Open Kiro → Show the spec
# Kiro generates the Packer template from the spec
cd packer/
packer init .
packer build golden-image.pkr.hcl

Act 2: Terraform Module (~3 min)

# Show the module — platform team's shared language
cd environments/prod/
terraform init
terraform plan
# Point out: the payment-api uses just 8 lines to deploy a fully governed service

Act 3: The Conversation (~3 min)

# Create a branch, make a change, open a PR
git checkout -b feat/add-payment-api
git add . && git commit -m "feat: add payment API service"
git push origin feat/add-payment-api
# Open PR → Watch plan preview appear → Security scan → Cost estimate → Approve

Key Takeaways

If you take away only four things from this post:

  1. 📦 Golden Images = Shared Starting Points — Use Packer to build versioned, peer-reviewed base images that eliminate environment drift across your organization.

  2. 🧩 Terraform Modules = Shared Language — Design composable modules that bridge the gap between platform teams (who build) and app teams (who consume). Self-service with guardrails.

  3. 🔄 PR Workflows = Visible Conversations — Make every infrastructure change transparent through plan previews, security scans, cost estimates, and peer approvals. No more shadow changes.

  4. 📋 Specs Drive Everything — Start with intent (specs), not implementation (code). Kiro makes this practical by generating and maintaining code from specifications.


Final Thought

"The best infrastructure is built together — one conversation at a time."

Infrastructure isn't just about servers, containers, and networks. It's about people — collaborating, reviewing, teaching, and building together. When we treat infrastructure changes as conversations rather than commands, we build systems that are not only more reliable but also more understandable, more maintainable, and more human.

Stop building infrastructure in isolation. Start having conversations.


Tags: Docker, HashiCorp Packer, HashiCorp Terraform, AWS ECS, AWS EKS, Infrastructure as Code, DevOps, Platform Engineering, Kiro, Spec-Driven Development, GitHub Actions, Collaboration

Originally presented at Docker Meetup Pune — Infrastructure as Conversation: Collaborating with Docker, Terraform & Packer