Terraform AWS Resource Examples for IAM, SQS, SNS, Lambda, S3, Route53, ALB, ElastiCache, ECS, RDS, CloudFront, Auto Scaling
Table of Contents
Terraform AWS Resource Examples
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"
},
]
})
}
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"
}
}
}
]
})
}
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"
}
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
}
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}/*"
},
]
})
}
Route53 Record
resource "aws_route53_record" "example_record" {
zone_id = "ZONE_ID"
name = "example.com"
type = "A"
ttl = 300
records = ["192.0.2.1"]
}
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
}
}
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]
}
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]
}
}
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]
}
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
}
}
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
}
}
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="#f5f5f5", 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="#369";
fontcolor="#369"; style="rounded";
root [label="main.tf\nproviders + module calls", fillcolor="#eaf2fb"];
subgraph cluster_net {
label="module \"network\""; labeljust="l"; color="#693";
fontcolor="#693"; 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="#d63";
fontcolor="#d63"; 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="#639";
fontcolor="#639"; 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="#d36";
fontcolor="#d36"; 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="#963", fontcolor="#963"];
s3 -> ddb [style=dashed, label="lease", color="#d36",
fontcolor="#d36", fontsize=9];
}
// Init/plan/apply flow
subgraph cluster_flow {
label="Workflow"; labeljust="l"; color="#963";
fontcolor="#963"; style="rounded";
init [label="terraform init", fillcolor="#fff7da"];
plan [label="terraform plan", fillcolor="#fff7da"];
apply [label="terraform apply", fillcolor="#fff7da"];
init -> plan -> apply [color="#963"];
}
// Composition: outputs flow forward
root -> net_in;
net_out -> cmp_in [label="vpc_id, subnet_ids", fontsize=9, color="#693"];
net_out -> data_in [label="vpc_id, subnet_ids", fontsize=9, color="#693"];
cmp_out -> data_in [style=dotted, label="security_group_id",
fontsize=9, color="#d63"];
// State file gates the workflow
init -> s3 [label="backend config", fontsize=9, color="#369"];
plan -> s3 [label="read state", fontsize=9, color="#369"];
plan -> ddb [label="acquire lock", fontsize=9, color="#d36"];
apply -> ddb [label="hold lock", fontsize=9, color="#d36"];
apply -> s3 [label="write state", fontsize=9, color="#369"];
// 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.
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.
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.
