Skip to content
Back to skills

Terraform Patterns

ASecurity

Module composition, state management, workspace strategy, provider versioning, and infrastructure-as-code best practices.

  • 530 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added May 29, 2026
ai-agentsawsterraformdatabasebackendsecurity

Security analysis

A100/100

Scanned May 29, 2026

npx -y skills add vibeeval/vibecosystem --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: A — Skills Directory](https://www.skillsdirectory.com/api/skills/vibeeval-terraform-patterns/badge)](https://www.skillsdirectory.com/skills/vibeeval-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: Module composition, state management, workspace strategy, provider versioning, and infrastructure-as-code best practices.
---

# Terraform Patterns

Production Terraform patterns for maintainable infrastructure-as-code.

## Module Composition

```hcl
# modules/vpc/main.tf - Reusable VPC module
variable "name" {
  type        = string
  description = "VPC name prefix"
}

variable "cidr" {
  type    = string
  default = "10.0.0.0/16"
}

variable "azs" {
  type    = list(string)
  default = ["us-east-1a", "us-east-1b", "us-east-1c"]
}

resource "aws_vpc" "main" {
  cidr_block           = var.cidr
  enable_dns_hostnames = true
  enable_dns_support   = true

  tags = {
    Name        = var.name
    ManagedBy   = "terraform"
    Environment = terraform.workspace
  }
}

resource "aws_subnet" "private" {
  count             = length(var.azs)
  vpc_id            = aws_vpc.main.id
  cidr_block        = cidrsubnet(var.cidr, 8, count.index)
  availability_zone = var.azs[count.index]

  tags = {
    Name = "${var.name}-private-${var.azs[count.index]}"
    Tier = "private"
  }
}

output "vpc_id" {
  value = aws_vpc.main.id
}

output "private_subnet_ids" {
  value = aws_subnet.private[*.id]
}
```

## Root Module Composition

```hcl
# environments/production/main.tf
terraform {
  required_version = ">= 1.5.0"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"    # Pin major version, allow minor updates
    }
  }

  backend "s3" {
    bucket         = "mycompany-terraform-state"
    key            = "production/terraform.tfstate"
    region         = "us-east-1"
    dynamodb_table = "terraform-locks"    # State locking
    encrypt        = true
  }
}

module "vpc" {
  source = "../../modules/vpc"
  name   = "prod"
  cidr   = "10.0.0.0/16"
  azs    = ["us-east-1a", "us-east-1b", "us-east-1c"]
}

module "database" {
  source    = "../../modules/rds"
  vpc_id    = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnet_ids

  instance_class    = "db.r6g.xlarge"
  allocated_storage = 100
  multi_az          = true
}

module "app" {
  source     = "../../modules/ecs-service"
  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnet_ids

  container_image = "myapp:${var.app_version}"
  desired_count   = 3
  cpu             = 512
  memory          = 1024

  environment_variables = {
    DATABASE_URL = module.database.connection_string
    LOG_LEVEL    = "info"
  }
}
```

## State Management

```hcl
# Remote state data source: reference other state files safely
data "terraform_remote_state" "networking" {
  backend = "s3"
  config = {
    bucket = "mycompany-terraform-state"
    key    = "networking/terraform.tfstate"
    region = "us-east-1"
  }
}

# Use outputs from networking state
module "app" {
  source     = "../../modules/ecs-service"
  vpc_id     = data.terraform_remote_state.networking.outputs.vpc_id
  subnet_ids = data.terraform_remote_state.networking.outputs.private_subnet_ids
}
```

## Resource Lifecycle

```hcl
resource "aws_instance" "app" {
  ami           = var.ami_id
  instance_type = var.instance_type

  lifecycle {
    # Create new before destroying old (zero-downtime replacement)
    create_before_destroy = true

    # Prevent accidental deletion of critical resources
    prevent_destroy = true

    # Ignore changes made outside Terraform (e.g., auto-scaling tags)
    ignore_changes = [tags["LastScaleEvent"]]
  }
}

# Import existing resources
import {
  to = aws_s3_bucket.existing
  id = "my-existing-bucket-name"
}
```

## Variables and Validation

```hcl
variable "environment" {
  type        = string
  description = "Deployment environment"

  validation {
    condition     = contains(["development", "staging", "production"], var.environment)
    error_message = "Environment must be development, staging, or production."
  }
}

variable "instance_count" {
  type    = number
  default = 2

  validation {
    condition     = var.instance_count >= 1 && var.instance_count <= 20
    error_message = "Instance count must be between 1 and 20."
  }
}

# Local values for computed configuration
locals {
  common_tags = {
    Environment = var.environment
    Project     = var.project_name
    ManagedBy   = "terraform"
    Team        = var.team
  }

  is_production = var.environment == "production"
  replica_count = local.is_production ? 3 : 1
}
```

## Dynamic Blocks and for_each

```hcl
# for_each over map (preferred over count for named resources)
variable "services" {
  type = map(object({
    port        = number
    health_path = string
    replicas    = number
  }))
}

resource "aws_ecs_service" "services" {
  for_each = var.services

  name            = each.key
  cluster         = aws_ecs_cluster.main.id
  desired_count   = each.value.replicas
  task_definition = aws_ecs_task_definition.tasks[each.key].arn
}

# Dynamic block for security group rules
resource "aws_security_group" "app" {
  name   = "${var.name}-sg"
  vpc_id = var.vpc_id

  dynamic "ingress" {
    for_each = var.ingress_rules
    content {
      from_port   = ingress.value.port
      to_port     = ingress.value.port
      protocol    = "tcp"
      cidr_blocks = ingress.value.cidr_blocks
      description = ingress.value.description
    }
  }
}
```

## Checklist

- [ ] Pin provider versions with `~>` (allow patch, lock major/minor)
- [ ] Remote state with locking (S3 + DynamoDB or Terraform Cloud)
- [ ] Separate state files per environment (not workspaces for prod vs dev)
- [ ] `prevent_destroy` on databases, S3 buckets, and IAM roles
- [ ] Variable validation blocks for all user-facing inputs
- [ ] Common tags via `locals` applied to every resource
- [ ] Use `for_each` over `count` (survives reordering without recreation)
- [ ] Run `terraform plan` in CI, `terraform apply` requires approval

## Anti-Patterns

- Single state file for all environments: one bad apply affects everything
- Hardcoded values: use variables with defaults and validation
- Using `count` with lists: removing item N recreates items N+1 through end
- No state locking: concurrent applies corrupt state
- Monolithic root module: 500+ resources in one state (split by lifecycle)
- Storing secrets in `.tf` files: use AWS Secrets Manager or Vault references
- Not pinning provider versions: surprise breaking changes on next init

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…