terraform-iac

Terraform infrastructure as code with state management, modules, workspaces, lifecycle rules, and CI/CD patterns

Terraform Infrastructure as Code

You are an expert in Terraform infrastructure as code, state management, module composition, and production deployment patterns.

Project Structure

BAD - Monolithic configuration:

# main.tf (2000+ lines)
provider "aws" {
  region = "us-east-1"
}

resource "aws_vpc" "main" {
  cidr_block = "10.0.0.0/16"
}

resource "aws_subnet" "public_1" {
  vpc_id     = aws_vpc.main.id
  cidr_block = "10.0.1.0/24"
}
# ... 100+ more resources

GOOD - Modular structure:

terraform/
├── environments/
│   ├── dev/
│   │   ├── main.tf
│   │   ├── terraform.tfvars
│   │   └── backend.tf
│   ├── staging/
│   └── prod/
├── modules/
│   ├── networking/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   ├── outputs.tf
│   │   └── versions.tf
│   ├── compute/
│   └── database/
└── shared/
    └── variables.tf
# environments/prod/main.tf
module "networking" {
  source = "../../modules/networking"

  environment    = var.environment
  vpc_cidr       = var.vpc_cidr
  azs            = var.availability_zones
  public_subnets = var.public_subnet_cidrs
}

module "database" {
  source = "../../modules/database"

  environment       = var.environment
  vpc_id            = module.networking.vpc_id
  private_subnets   = module.networking.private_subnet_ids
  instance_class    = var.db_instance_class
}

State Management

BAD - Local state with no locking:

# No backend configuration, uses local terraform.tfstate
terraform {
  required_version = ">= 1.0"
}

GOOD - Remote backend with state locking:

# environments/prod/backend.tf
terraform {
  backend "s3" {
    bucket         = "mycompany-terraform-state"
    key            = "prod/terraform.tfstate"
    region         = "us-east-1"
    encrypt        = true
    dynamodb_table = "terraform-state-lock"

    # Prevent accidental deletion
    lifecycle {
      prevent_destroy = true
    }
  }

  required_version = ">= 1.5"

  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.0"
    }
  }
}

Setup state backend resources:

# bootstrap/state-backend.tf
resource "aws_s3_bucket" "terraform_state" {
  bucket = "mycompany-terraform-state"
}

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 = "AES256"
    }
  }
}

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

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

Module Composition

BAD - Hardcoded values, no versioning:

module "vpc" {
  source = "../../modules/vpc"

  cidr_block = "10.0.0.0/16"
  name       = "my-vpc"
}

GOOD - Parameterized with version pinning:

# modules/networking/variables.tf
variable "environment" {
  type        = string
  description = "Environment name"

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

variable "vpc_cidr" {
  type        = string
  description = "CIDR block for VPC"

  validation {
    condition     = can(cidrhost(var.vpc_cidr, 0))
    error_message = "Must be valid IPv4 CIDR."
  }
}

variable "azs" {
  type        = list(string)
  description = "Availability zones"
}

# modules/networking/main.tf
locals {
  common_tags = {
    Environment = var.environment
    ManagedBy   = "terraform"
    Module      = "networking"
  }
}

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

  tags = merge(local.common_tags, {
    Name = "${var.environment}-vpc"
  })
}

# modules/networking/outputs.tf
output "vpc_id" {
  value       = aws_vpc.main.id
  description = "VPC ID"
}

output "private_subnet_ids" {
  value       = aws_subnet.private[*].id
  description = "Private subnet IDs"
}

# environments/prod/main.tf
module "networking" {
  source = "git::https://github.com/company/terraform-modules.git//networking?ref=v2.1.0"

  environment = "prod"
  vpc_cidr    = "10.0.0.0/16"
  azs         = ["us-east-1a", "us-east-1b", "us-east-1c"]
}

Environment Management

BAD - Workspaces for different environments:

terraform workspace new dev
terraform workspace new prod
terraform apply  # Which environment? State is confusing

GOOD - Directory-based environments with tfvars:

# environments/prod/terraform.tfvars
environment = "prod"
vpc_cidr    = "10.0.0.0/16"

availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]

db_instance_class = "db.r6g.xlarge"
db_allocated_storage = 100

ecs_task_cpu    = 2048
ecs_task_memory = 4096
# Deployment script
cd environments/prod
terraform init
terraform plan -out=tfplan
terraform apply tfplan

Use workspaces only for feature branches:

terraform workspace new feature-new-service
terraform apply -var="environment=dev"
terraform workspace select default
<!-- See references/advanced.md for extended examples -->