Skip to content
Back to skills

Terraform Patterns

BSecurity

Terraform patterns — reusable modules, remote state, workspaces, drift detection, cost estimation, and importing existing infrastructure.

  • 6 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added June 1, 2026
ai-agentsgoshellbashdockerawsterraformgitapidatabasebackend

Works with

  • api

Security analysis

B84/100
  • mediumUses curl or wget to download content
  • criticalExfiltrates credentials via HTTP — exact pattern from Snyk ToxicSkills study

Pro shows the line behind each finding and how to fix it

Scanned June 1, 2026

npx -y skills add KevinZai/commander --skill terraform-patterns --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Terraform Patterns?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Terraform Patterns
[![Security: B — Skills Directory](https://www.skillsdirectory.com/api/skills/kevinzai-terraform-patterns/badge)](https://www.skillsdirectory.com/skills/kevinzai-terraform-patterns)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: Terraform Patterns
description: "Terraform patterns — reusable modules, remote state, workspaces, drift detection, cost estimation, and importing existing infrastructure."
version: 1.0.0
category: infrastructure-as-code
parent: ccc-devops
---

# Terraform Patterns

> Infrastructure as Code done right. Modular, versioned, drift-aware Terraform with remote state, environment isolation, and cost visibility.

## Use this skill when

- Designing Terraform module structure for a new project
- Setting up remote state with locking
- Managing multiple environments (dev, staging, production)
- Importing existing infrastructure into Terraform
- Detecting and resolving state drift
- Estimating infrastructure costs before applying

## Do not use this skill when

- Choosing which AWS services to use (use `aws-solution-architect`)
- Setting up CI/CD for Terraform (use `cicd-pipeline-generator`)
- Configuring networking in the cloud (use `network-engineer`)

---

## Module Design

### Directory Structure

```
infrastructure/
├── modules/                    # Reusable modules
│   ├── networking/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── outputs.tf
│   │   └── README.md
│   ├── ecs-service/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── outputs.tf
│   │   ├── iam.tf
│   │   └── README.md
│   ├── rds/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── outputs.tf
│   └── monitoring/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
├── environments/               # Environment-specific configs
│   ├── dev/
│   │   ├── main.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf
│   ├── staging/
│   │   ├── main.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf
│   └── production/
│       ├── main.tf
│       ├── terraform.tfvars
│       └── backend.tf
└── global/                     # Shared resources (IAM, DNS)
    ├── main.tf
    ├── terraform.tfvars
    └── backend.tf
```

### Module Design Principles

```hcl
# modules/ecs-service/variables.tf
# 1. Required variables first, optional with defaults after
# 2. Use validation blocks for constraints
# 3. Type everything explicitly

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

  validation {
    condition     = can(regex("^[a-z][a-z0-9-]{2,28}[a-z0-9]$", var.service_name))
    error_message = "Service name must be 4-30 lowercase alphanumeric characters or hyphens."
  }
}

variable "container_image" {
  type        = string
  description = "Docker image URI (e.g., 123456789012.dkr.ecr.us-east-1.amazonaws.com/app:latest)"
}

variable "cpu" {
  type        = number
  default     = 256
  description = "CPU units (256 = 0.25 vCPU)"

  validation {
    condition     = contains([256, 512, 1024, 2048, 4096], var.cpu)
    error_message = "CPU must be one of: 256, 512, 1024, 2048, 4096."
  }
}

variable "memory" {
  type        = number
  default     = 512
  description = "Memory in MB"
}

variable "desired_count" {
  type        = number
  default     = 2
  description = "Number of tasks to run"
}

variable "environment_variables" {
  type        = map(string)
  default     = {}
  description = "Environment variables for the container"
}

variable "secrets" {
  type = map(string)
  default     = {}
  description = "Secrets from SSM or Secrets Manager (name → ARN)"
  sensitive   = true
}

variable "tags" {
  type        = map(string)
  default     = {}
  description = "Additional tags to apply to all resources"
}
```

```hcl
# modules/ecs-service/main.tf
# 1. Use data sources for lookups, not hardcoded values
# 2. Use locals for computed values
# 3. Tag everything with a consistent scheme

locals {
  common_tags = merge(var.tags, {
    Module    = "ecs-service"
    Service   = var.service_name
    ManagedBy = "terraform"
  })
}

resource "aws_ecs_task_definition" "this" {
  family                   = var.service_name
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = var.cpu
  memory                   = var.memory
  execution_role_arn       = aws_iam_role.execution.arn
  task_role_arn            = aws_iam_role.task.arn

  container_definitions = jsonencode([{
    name      = var.service_name
    image     = var.container_image
    essential = true

    portMappings = [{
      containerPort = 3000
      protocol      = "tcp"
    }]

    environment = [
      for name, value in var.environment_variables : {
        name  = name
        value = value
      }
    ]

    secrets = [
      for name, arn in var.secrets : {
        name      = name
        valueFrom = arn
      }
    ]

    logConfiguration = {
      logDriver = "awslogs"
      options = {
        awslogs-group         = aws_cloudwatch_log_group.this.name
        awslogs-region        = data.aws_region.current.name
        awslogs-stream-prefix = var.service_name
      }
    }

    healthCheck = {
      command     = ["CMD-SHELL", "curl -f http://localhost:3000/health || exit 1"]
      interval    = 30
      timeout     = 5
      retries     = 3
      startPeriod = 60
    }
  }])

  tags = local.common_tags
}

resource "aws_ecs_service" "this" {
  name            = var.service_name
  cluster         = var.cluster_id
  task_definition = aws_ecs_task_definition.this.arn
  desired_count   = var.desired_count
  launch_type     = "FARGATE"

  deployment_circuit_breaker {
    enable   = true
    rollback = true
  }

  deployment_configuration {
    maximum_percent         = 200
    minimum_healthy_percent = 100
  }

  network_configuration {
    subnets          = var.private_subnet_ids
    security_groups  = [aws_security_group.service.id]
    assign_public_ip = false
  }

  tags = local.common_tags
}
```

```hcl
# modules/ecs-service/outputs.tf
# Output everything consumers might need

output "service_arn" {
  value       = aws_ecs_service.this.id
  description = "ARN of the ECS service"
}

output "task_definition_arn" {
  value       = aws_ecs_task_definition.this.arn
  description = "ARN of the current task definition"
}

output "security_group_id" {
  value       = aws_security_group.service.id
  description = "Security group ID for the service"
}

output "log_group_name" {
  value       = aws_cloudwatch_log_group.this.name
  description = "CloudWatch log group name"
}
```

---

## State Management

### Remote State with S3 + DynamoDB

```hcl
# environments/production/backend.tf
terraform {
  backend "s3" {
    bucket         = "mycompany-terraform-state"
    key            = "production/app/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-locks"
    # Use OIDC or instance profile — no hardcoded credentials
  }
}
```

### State locking table

```hcl
# global/state-backend.tf — create this ONCE, manually
resource "aws_s3_bucket" "terraform_state" {
  bucket = "mycompany-terraform-state"

  lifecycle {
    prevent_destroy = true
  }

  tags = { ManagedBy = "terraform" }
}

resource "aws_s3_bucket_versioning" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id
  rule {
    apply_server_side_encryption_by_default {
      sse_algorithm = "aws:kms"
    }
  }
}

resource "aws_s3_bucket_public_access_block" "terraform_state" {
  bucket = aws_s3_bucket.terraform_state.id

  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_dynamodb_table" "terraform_locks" {
  name         = "terraform-locks"
  billing_mode = "PAY_PER_REQUEST"
  hash_key     = "LockID"

  attribute {
    name = "LockID"
    type = "S"
  }

  tags = { ManagedBy = "terraform" }
}
```

### Accessing state from other configurations

```hcl
# Reference outputs from another state file
data "terraform_remote_state" "networking" {
  backend = "s3"
  config = {
    bucket = "mycompany-terraform-state"
    key    = "production/networking/terraform.tfstate"
    region = "us-east-1"
  }
}

# Use outputs from the networking state
module "app" {
  source = "../../modules/ecs-service"

  service_name       = "app"
  cluster_id         = data.terraform_remote_state.networking.outputs.ecs_cluster_id
  private_subnet_ids = data.terraform_remote_state.networking.outputs.private_subnet_ids
  vpc_id             = data.terraform_remote_state.networking.outputs.vpc_id
}
```

---

## Environment Separation

### Using tfvars per environment

```hcl
# environments/dev/terraform.tfvars
environment   = "dev"
instance_type = "t3.small"
desired_count = 1
cpu           = 256
memory        = 512

# environments/production/terraform.tfvars
environment   = "production"
instance_type = "t3.large"
desired_count = 3
cpu           = 1024
memory        = 2048
```

### Using workspaces (simpler environments)

```bash
# Create workspaces
terraform workspace new dev
terraform workspace new staging
terraform workspace new production

# Switch workspace
terraform workspace select production

# Use workspace in config
locals {
  env_config = {
    dev = {
      instance_type = "t3.small"
      desired_count = 1
    }
    staging = {
      instance_type = "t3.medium"
      desired_count = 2
    }
    production = {
      instance_type = "t3.large"
      desired_count = 3
    }
  }

  config = local.env_config[terraform.workspace]
}
```

### When to use workspaces vs. directories

| Approach | When to Use |
|----------|------------|
| **Directories** (`environments/dev/`, `environments/prod/`) | Different backends, different providers, significantly different resources |
| **Workspaces** | Same resources, different sizes/counts, shared backend |

---

## Resource Naming Conventions

```hcl
locals {
  # Consistent naming: {project}-{environment}-{resource}-{qualifier}
  name_prefix = "${var.project}-${var.environment}"

  # Examples:
  # myapp-production-vpc
  # myapp-production-ecs-cluster
  # myapp-production-rds-primary
  # myapp-production-s3-assets
}

resource "aws_vpc" "main" {
  cidr_block = var.vpc_cidr

  tags = {
    Name        = "${local.name_prefix}-vpc"
    Environment = var.environment
    Project     = var.project
    ManagedBy   = "terraform"
  }
}
```

---

## Secret Management

### AWS Secrets Manager

```hcl
# Store secrets
resource "aws_secretsmanager_secret" "db_password" {
  name                    = "${local.name_prefix}/db-password"
  recovery_window_in_days = 7

  tags = local.common_tags
}

# Reference in ECS task (via module variable)
module "app" {
  source = "../../modules/ecs-service"

  secrets = {
    DATABASE_PASSWORD = aws_secretsmanager_secret.db_password.arn
  }
}
```

### HashiCorp Vault

```hcl
provider "vault" {
  address = "https://vault.internal.example.com"
  # Auth via VAULT_TOKEN env var or AppRole
}

data "vault_generic_secret" "db" {
  path = "secret/data/${var.environment}/database"
}

resource "aws_ssm_parameter" "db_url" {
  name  = "/${var.environment}/app/database-url"
  type  = "SecureString"
  value = data.vault_generic_secret.db.data["connection_string"]
}
```

---

## Import Existing Infrastructure

```bash
# 1. Write the resource block first
cat >> main.tf <<'EOF'
resource "aws_s3_bucket" "existing" {
  bucket = "my-existing-bucket"
}
EOF

# 2. Import the resource into state
terraform import aws_s3_bucket.existing my-existing-bucket

# 3. Run plan to see drift
terraform plan

# 4. Adjust the config until plan shows no changes
# 5. Commit the config — infrastructure is now managed by Terraform
```

### Bulk import with import blocks (Terraform 1.5+)

```hcl
# imports.tf — declarative import
import {
  to = aws_s3_bucket.assets
  id = "my-existing-assets-bucket"
}

import {
  to = aws_iam_role.app
  id = "my-existing-app-role"
}

# Generate config from imports
# terraform plan -generate-config-out=generated.tf
```

---

## Drift Detection

### Scheduled drift detection in CI

```yaml
# .github/workflows/drift-detection.yml
name: Terraform Drift Detection
on:
  schedule:
    - cron: '0 8 * * 1-5'  # weekdays at 8am

jobs:
  drift-check:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        environment: [dev, staging, production]
    steps:
      - uses: actions/checkout@v4

      - uses: hashicorp/setup-terraform@v3
        with:
          terraform_version: '1.7'

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

      - name: Terraform init
        working-directory: environments/${{ matrix.environment }}
        run: terraform init -input=false

      - name: Check for drift
        working-directory: environments/${{ matrix.environment }}
        id: plan
        run: |
          terraform plan -detailed-exitcode -input=false -no-color 2>&1 | tee plan.txt
          echo "exitcode=$?" >> $GITHUB_OUTPUT
        continue-on-error: true

      - name: Notify on drift
        if: steps.plan.outputs.exitcode == '2'
        run: |
          curl -X POST "${{ secrets.SLACK_WEBHOOK }}" \
            -H 'Content-Type: application/json' \
            -d "{\"text\":\"Terraform drift detected in ${{ matrix.environment }}. Review the plan output.\"}"
```

---

## Cost Estimation

### Infracost integration

```yaml
# .github/workflows/infracost.yml
name: Infracost
on: pull_request

jobs:
  cost:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: infracost/actions/setup@v3
        with:
          api-key: ${{ secrets.INFRACOST_API_KEY }}

      - name: Generate cost estimate
        run: |
          infracost breakdown \
            --path=environments/production \
            --format=json \
            --out-file=/tmp/infracost.json

      - name: Post PR comment
        run: |
          infracost comment github \
            --path=/tmp/infracost.json \
            --repo=${{ github.repository }} \
            --pull-request=${{ github.event.pull_request.number }} \
            --github-token=${{ secrets.GITHUB_TOKEN }} \
            --behavior=update
```

### Cost-aware module design

```hcl
# Include cost tags on every resource
variable "cost_center" {
  type        = string
  description = "Cost center for billing allocation"
}

locals {
  cost_tags = {
    CostCenter = var.cost_center
    ManagedBy  = "terraform"
    Module     = "ecs-service"
  }
}
```

---

## Terraform Best Practices Checklist

- [ ] Remote state with S3 + DynamoDB locking (never local state in production)
- [ ] State encryption enabled (SSE-KMS)
- [ ] Modules are versioned (use git tags or registry versions)
- [ ] Variables have types, descriptions, and validation blocks
- [ ] All resources tagged with environment, project, and managed-by
- [ ] `prevent_destroy` on critical resources (state buckets, databases)
- [ ] Drift detection running on a schedule
- [ ] Cost estimation (infracost) on every PR
- [ ] Secrets managed via Secrets Manager or Vault (never in tfvars)
- [ ] Import blocks used for existing infrastructure (no manual state surgery)
- [ ] `terraform fmt` and `terraform validate` in CI
- [ ] Plan output reviewed before every apply

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…