Terraform AWS Patterns

Table of Contents

1. Terraform AWS Resource Examples

1.1. IAM Role and Policy

resource "aws_iam_role" "example_role" {
  name = "example-role"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "ec2.amazonaws.com"
        }
      }
    ]
  })
}

resource "aws_iam_role_policy" "example_policy" {
  name = "example-policy"
  role = aws_iam_role.example_role.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "s3:ListBucket",
        ]
        Effect   = "Allow"
        Resource = "arn:aws:s3:::example-bucket"
      },
    ]
  })
}

1.2. SQS Queue and Policy

resource "aws_sqs_queue" "example_queue" {
  name                      = "example-queue"
  delay_seconds             = 90
  max_message_size          = 2048
  message_retention_seconds = 86400
  receive_wait_time_seconds = 10
}

resource "aws_sqs_queue_policy" "example_queue_policy" {
  queue_url = aws_sqs_queue.example_queue.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = "*"
        Action = "sqs:SendMessage"
        Resource = aws_sqs_queue.example_queue.arn
        Condition = {
          ArnEquals = {
            "aws:SourceArn" = "arn:aws:sns:us-west-2:123456789012:example-topic"
          }
        }
      }
    ]
  })
}

1.3. SNS Topic and Subscription

resource "aws_sns_topic" "example_topic" {
  name = "example-topic"
}

resource "aws_sns_topic_subscription" "example_subscription" {
  topic_arn = aws_sns_topic.example_topic.arn
  protocol  = "email"
  endpoint  = "example@example.com"
}

1.4. Lambda Function and CloudWatch Event Rule

resource "aws_lambda_function" "example_lambda" {
  filename      = "lambda_function.zip"
  function_name = "example-lambda"
  role          = aws_iam_role.example_role.arn
  handler       = "index.handler"
  runtime       = "nodejs14.x"

  environment {
    variables = {
      EXAMPLE_VAR = "example-value"
    }
  }
}

resource "aws_cloudwatch_event_rule" "example_event_rule" {
  name        = "example-event-rule"
  description = "Trigger Lambda function every hour"

  schedule_expression = "rate(1 hour)"
}

resource "aws_cloudwatch_event_target" "example_target" {
  rule      = aws_cloudwatch_event_rule.example_event_rule.name
  target_id = "example-lambda"
  arn       = aws_lambda_function.example_lambda.arn
}

resource "aws_lambda_permission" "example_permission" {
  statement_id  = "AllowExecutionFromCloudWatch"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.example_lambda.function_name
  principal     = "events.amazonaws.com"
  source_arn    = aws_cloudwatch_event_rule.example_event_rule.arn
}

1.5. S3 Bucket and Policy

resource "aws_s3_bucket" "example_bucket" {
  bucket = "example-bucket"
}

resource "aws_s3_bucket_policy" "example_bucket_policy" {
  bucket = aws_s3_bucket.example_bucket.id

  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid       = "PublicReadGetObject"
        Effect    = "Allow"
        Principal = "*"
        Action    = "s3:GetObject"
        Resource  = "${aws_s3_bucket.example_bucket.arn}/*"
      },
    ]
  })
}

1.6. Route53 Record

resource "aws_route53_record" "example_record" {
  zone_id = "ZONE_ID"
  name    = "example.com"
  type    = "A"
  ttl     = 300
  records = ["192.0.2.1"]
}

1.7. Application Load Balancer

resource "aws_lb" "example_alb" {
  name               = "example-alb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.example_sg.id]
  subnets            = ["subnet-12345678", "subnet-87654321"]

  enable_deletion_protection = false
}

resource "aws_lb_target_group" "example_tg" {
  name     = "example-tg"
  port     = 80
  protocol = "HTTP"
  vpc_id   = "vpc-12345678"
}

resource "aws_lb_listener" "example_listener" {
  load_balancer_arn = aws_lb.example_alb.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    type             = "forward"
    target_group_arn = aws_lb_target_group.example_tg.arn
  }
}

1.8. ElastiCache Replication Group

resource "aws_elasticache_subnet_group" "example_subnet_group" {
  name       = "example-cache-subnet"
  subnet_ids = ["subnet-12345678", "subnet-87654321"]
}

resource "aws_elasticache_replication_group" "example_cache" {
  replication_group_id          = "example-cache"
  replication_group_description = "Example Redis cluster"
  node_type                     = "cache.t3.micro"
  number_cache_clusters         = 2
  port                          = 6379
  subnet_group_name             = aws_elasticache_subnet_group.example_subnet_group.name
  security_group_ids            = [aws_security_group.example_sg.id]
}

1.9. ECS Cluster and Service

resource "aws_ecs_cluster" "example_cluster" {
  name = "example-cluster"
}

resource "aws_ecs_task_definition" "example_task" {
  family                   = "example-task"
  network_mode             = "awsvpc"
  requires_compatibilities = ["FARGATE"]
  cpu                      = "256"
  memory                   = "512"

  container_definitions = jsonencode([
    {
      name  = "example-container"
      image = "nginx:latest"
      portMappings = [
        {
          containerPort = 80
          hostPort      = 80
        }
      ]
    }
  ])
}

resource "aws_ecs_service" "example_service" {
  name            = "example-service"
  cluster         = aws_ecs_cluster.example_cluster.id
  task_definition = aws_ecs_task_definition.example_task.arn
  launch_type     = "FARGATE"
  desired_count   = 2

  network_configuration {
    subnets         = ["subnet-12345678", "subnet-87654321"]
    security_groups = [aws_security_group.example_sg.id]
  }
}

1.10. RDS Instance

resource "aws_db_subnet_group" "example_subnet_group" {
  name       = "example-db-subnet"
  subnet_ids = ["subnet-12345678", "subnet-87654321"]
}

resource "aws_db_instance" "example_db" {
  identifier           = "example-db"
  engine               = "mysql"
  engine_version       = "5.7"
  instance_class       = "db.t3.micro"
  allocated_storage    = 20
  storage_type         = "gp2"
  db_name              = "exampledb"
  username             = "admin"
  password             = "PASSWORD"
  parameter_group_name = "default.mysql5.7"
  skip_final_snapshot  = true
  db_subnet_group_name = aws_db_subnet_group.example_subnet_group.name
  vpc_security_group_ids = [aws_security_group.example_sg.id]
}

1.11. CloudFront Distribution

resource "aws_cloudfront_distribution" "example_distribution" {
  enabled             = true
  default_root_object = "index.html"

  origin {
    domain_name = aws_s3_bucket.example_bucket.bucket_regional_domain_name
    origin_id   = "S3-${aws_s3_bucket.example_bucket.id}"
  }

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "S3-${aws_s3_bucket.example_bucket.id}"

    forwarded_values {
      query_string = false
      cookies {
        forward = "none"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
    min_ttl                = 0
    default_ttl            = 3600
    max_ttl                = 86400
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    cloudfront_default_certificate = true
  }
}

1.12. Auto Scaling Target and Policy

resource "aws_appautoscaling_target" "example_target" {
  max_capacity       = 4
  min_capacity       = 1
  resource_id        = "service/${aws_ecs_cluster.example_cluster.name}/${aws_ecs_service.example_service.name}"
  scalable_dimension = "ecs:service:DesiredCount"
  service_namespace  = "ecs"
}

resource "aws_appautoscaling_policy" "example_policy" {
  name               = "example-autoscaling-policy"
  policy_type        = "TargetTrackingScaling"
  resource_id        = aws_appautoscaling_target.example_target.resource_id
  scalable_dimension = aws_appautoscaling_target.example_target.scalable_dimension
  service_namespace  = aws_appautoscaling_target.example_target.service_namespace

  target_tracking_scaling_policy_configuration {
    predefined_metric_specification {
      predefined_metric_type = "ECSServiceAverageCPUUtilization"
    }
    target_value = 70.0
  }
}

2. Module composition and state

The resource snippets above belong to a single root module. In real projects they get split into reusable modules (network, compute, data) wired together by the root, with a remote backend gating every plan and apply via a state file and a lock.

// Terraform module composition + remote state — root composes
// network/compute/data; S3 + DynamoDB lock gates init/plan/apply.
digraph terraform_modules {
    rankdir=TB;
    graph [bgcolor="white", fontname="Helvetica", fontsize=11,
           pad="0.3", nodesep="0.3", ranksep="0.4"];
    node  [shape=box, style="rounded,filled", fontname="Helvetica",
           fontsize=10, fillcolor="#dbeafe", color="#888"];
    edge  [color="#aaa"];
    // Tailwind palette: #d36 red, #d63 orange, #693 green, #369 blue, #639 purple, #963 brown

    // Root module composes reusable submodules
    subgraph cluster_root {
        label="Root module (env: prod)"; labeljust="l"; color="#1d4ed8";
        fontcolor="#1d4ed8"; style="rounded";
        root [label="main.tf\nproviders + module calls", fillcolor="#eaf2fb"];

        subgraph cluster_net {
            label="module \"network\""; labeljust="l"; color="#15803d";
            fontcolor="#15803d"; style="rounded";
            net_in  [label="inputs:\ncidr, az_count"];
            net_out [label="outputs:\nvpc_id, subnet_ids", fillcolor="#eaf5ea"];
        }
        subgraph cluster_cmp {
            label="module \"compute\""; labeljust="l"; color="#b45309";
            fontcolor="#b45309"; style="rounded";
            cmp_in  [label="inputs:\nvpc_id, subnet_ids,\nimage, instance_type"];
            cmp_out [label="outputs:\nasg_name, alb_dns", fillcolor="#fbeee5"];
        }
        subgraph cluster_data {
            label="module \"data\""; labeljust="l"; color="#6b21a8";
            fontcolor="#6b21a8"; style="rounded";
            data_in  [label="inputs:\nvpc_id, subnet_ids,\nengine, size"];
            data_out [label="outputs:\ndb_endpoint, secret_arn", fillcolor="#f0eaf5"];
        }
    }

    // Side cluster: remote state + locking
    subgraph cluster_state {
        label="Remote backend"; labeljust="l"; color="#b91c1c";
        fontcolor="#b91c1c"; style="rounded";
        s3   [label="S3 bucket\nterraform.tfstate", fillcolor="#fbe6ec"];
        ddb  [label="DynamoDB table\nstate lock", fillcolor="#fbe6ec"];
        tfc  [label="(or) Terraform Cloud /\nOpenTofu remote state",
               fillcolor="#fff7da", color="#a16207", fontcolor="#a16207"];
        s3 -> ddb [style=dashed, label="lease", color="#b91c1c",
                   fontcolor="#b91c1c", fontsize=9];
    }

    // Init/plan/apply flow
    subgraph cluster_flow {
        label="Workflow"; labeljust="l"; color="#a16207";
        fontcolor="#a16207"; style="rounded";
        init  [label="terraform init", fillcolor="#fff7da"];
        plan  [label="terraform plan", fillcolor="#fff7da"];
        apply [label="terraform apply", fillcolor="#fff7da"];
        init -> plan -> apply [color="#a16207"];
    }

    // Composition: outputs flow forward
    root -> net_in;
    net_out -> cmp_in [label="vpc_id, subnet_ids", fontsize=9, color="#15803d"];
    net_out -> data_in [label="vpc_id, subnet_ids", fontsize=9, color="#15803d"];
    cmp_out -> data_in [style=dotted, label="security_group_id",
                        fontsize=9, color="#b45309"];

    // State file gates the workflow
    init  -> s3   [label="backend config", fontsize=9, color="#1d4ed8"];
    plan  -> s3   [label="read state", fontsize=9, color="#1d4ed8"];
    plan  -> ddb  [label="acquire lock", fontsize=9, color="#b91c1c"];
    apply -> ddb  [label="hold lock", fontsize=9, color="#b91c1c"];
    apply -> s3   [label="write state", fontsize=9, color="#1d4ed8"];

    // Root drives the workflow
    root -> init [style=dashed, color="#888"];
}

diagram-terraform-modules.png

The module boundary matters because each submodule's outputs become the next module's inputs. The state file is the gating artifact: a plan without the lease is a guess, and an apply without the lock risks two operators mutating the same resources in parallel.

3. Related notes

4. Postscript (2026)

The resource snippets above still type-check, but the ground around them has moved. In August 2023 HashiCorp relicensed Terraform from MPL 2.0 to the Business Source License, prompting a community fork that became OpenTofu under the Linux Foundation and reached GA 1.6 in January 2024 with a drop-in CLI and state-file compatibility (OpenTofu 1.6 GA announcement). Pulumi pushed in the opposite direction, leaning into TypeScript/Python/Go as first-class IaC languages and shipping Pulumi Copilot for natural-language stack operations. For shops staying on Terraform/OpenTofu, the conventions that matured are Terragrunt for DRY backend + tfvars wiring, internal module registries (Spacelift, env0, Terraform Cloud private registry) to replace ad-hoc Git-tagged modules, and hardened remote backends with KMS-encrypted state plus DynamoDB or native HCP locking. The 2023 terraform_remote_state data-source advisory (HashiCorp HCSEC advisories) made "remote backend + least-privilege IAM" the table-stakes default rather than an optional hardening step.