Terraform AWS Patterns
Table of Contents
- 1. Terraform AWS Resource Examples
- 1.1. IAM Role and Policy
- 1.2. SQS Queue and Policy
- 1.3. SNS Topic and Subscription
- 1.4. Lambda Function and CloudWatch Event Rule
- 1.5. S3 Bucket and Policy
- 1.6. Route53 Record
- 1.7. Application Load Balancer
- 1.8. ElastiCache Replication Group
- 1.9. ECS Cluster and Service
- 1.10. RDS Instance
- 1.11. CloudFront Distribution
- 1.12. Auto Scaling Target and Policy
- 2. Module composition and state
- 3. Related notes
- 4. Postscript (2026)
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"];
}
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
- Terraform – AWS Lambda configuration system – graph-style view of a single root module's resource dependencies, complementary to the module-composition view above.
- Amazon DynamoDB: NoSQL Database Design – the locking table in the diagram is a DynamoDB table; access-pattern notes there explain why a single-key conditional write is enough for state locking.
- CI/CD Pipelines – the
init → plan → applyflow is what most CI/CD pipelines wrap; GitOps and pull-request-driven plans live in that layer. - Software Design and Architecture Patterns – module composition, outputs as contracts, and ADR-style decision records are architecture concerns the snippets here only hint at.
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.