IaC in AWS using Terraform
· 4 min read

This post i will try on multiple available terraform modules to automate multiple things in AWS
What Is Terraform
Terraform is an open-source Infrastructure-as-Code (IaC) tool developed by HashiCorp. It allows us to define and manage your infrastructure in a declarative way, using configuration files written in HashiCorp Configuration Language (HCL) or JSON. Instead of manually clicking through the AWS console or using scripts, we can describe the desired state of your infrastructure in code, and Terraform takes care of provisioning and managing it for us.
Pre-Requisites
- IDE that support Terraform extension. I'm using Cursor and Terraform extension
- AWS Account
- Terraform Cloud Account (optional)
Goals for this experiment
- Spin up a baseline network (VPC + private/public subnets).
- Put a public ALB in front of an ECS Fargate service.
- Add a small Postgres RDS for stateful needs.
- Serve static assets via S3 + CloudFront.
- Keep modules reusable with minimal locals and variables.
High-level architecture
- 1 VPC with 2 AZs.
- Public subnets: ALB, NAT gateways.
- Private subnets: ECS tasks, RDS.
- CloudFront CDN in front of an S3 bucket for static assets.
- Parameter Store for app secrets; optional ACM for HTTPS.
Modules used
terraform-aws-modules/vpc/awsterraform-aws-modules/alb/awsterraform-aws-modules/ecs/aws(plusterraform-aws-modules/ecs/aws//modules/service)terraform-aws-modules/rds/awsterraform-aws-modules/s3-bucket/awsterraform-aws-modules/cloudfront/aws
Minimal snippets
VPC
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = "demo"
cidr = "10.0.0.0/16"
azs = ["ap-southeast-1a", "ap-southeast-1b"]
public_subnets = ["10.0.0.0/20", "10.0.16.0/20"]
private_subnets = ["10.0.32.0/20", "10.0.48.0/20"]
enable_nat_gateway = true
single_nat_gateway = true
}
ALB
module "alb" {
source = "terraform-aws-modules/alb/aws"
version = "~> 9.0"
name = "demo-alb"
load_balancer_type = "application"
vpc_id = module.vpc.vpc_id
subnets = module.vpc.public_subnets
security_groups = [aws_security_group.alb.id]
http_tcp_listeners = [{
port = 80
protocol = "HTTP"
target_group_index = 0
}]
target_groups = [{
name_prefix = "api-"
backend_protocol = "HTTP"
backend_port = 80
target_type = "ip"
health_check = {
path = "/healthz"
}
}]
}
ECS Fargate service
module "ecs" {
source = "terraform-aws-modules/ecs/aws"
version = "~> 5.0"
cluster_name = "demo-cluster"
}
module "ecs_svc" {
source = "terraform-aws-modules/ecs/aws//modules/service"
version = "~> 5.0"
name = "demo-api"
cluster_arn = module.ecs.cluster_arn
launch_type = "FARGATE"
desired_count = 2
cpu = 256
memory = 512
subnet_ids = module.vpc.private_subnets
security_group_ids = [aws_security_group.ecs.id]
assign_public_ip = false
load_balancer = {
target_group_arn = module.alb.target_group_arns[0]
container_name = "app"
container_port = 80
}
container_definitions = jsonencode([
{
name = "app"
image = "public.ecr.aws/docker/library/nginx:stable"
essential = true
portMappings = [{ containerPort = 80, hostPort = 80 }]
}
])
}
RDS (small Postgres)
module "db" {
source = "terraform-aws-modules/rds/aws"
version = "~> 6.0"
identifier = "demo-db"
engine = "postgres"
engine_version = "14"
instance_class = "db.t4g.micro"
allocated_storage = 20
db_subnet_group_name = module.vpc.database_subnet_group
vpc_security_group_ids = [aws_security_group.db.id]
username = "appuser"
password = var.db_password
skip_final_snapshot = true
}
S3 + CloudFront (static assets)
module "assets_bucket" {
source = "terraform-aws-modules/s3-bucket/aws"
version = "~> 4.0"
bucket = "demo-assets-${var.account_id}"
acl = "private"
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
module "cdn" {
source = "terraform-aws-modules/cloudfront/aws"
version = "~> 3.0"
aliases = ["assets.example.com"]
origin = {
s3 = {
domain_name = module.assets_bucket.s3_bucket_bucket_regional_domain_name
}
}
default_cache_behavior = {
target_origin_id = "s3"
viewer_protocol_policy = "redirect-to-https"
}
}
Variables and state
- Keep secrets (db password, API keys) in SSM Parameter Store or AWS Secrets Manager, referenced via data sources.
- Use remote state (Terraform Cloud or S3 + DynamoDB lock) for team safety.
- Pin provider and module versions to avoid accidental upgrades.
Apply flow
terraform init
terraform plan -out plan.tfplan
terraform apply plan.tfplan
# when done
terraform destroy
Cost and safety notes
- NAT Gateway is pricey; consider 1 NAT in a dev account or use VPC endpoints for S3/ECR.
t4g.microRDS is cheap but still incurs hourly + storage; stop/destroy after testing.- ECS Fargate billing is per vCPU/GB per hour; keep desired_count low for demos.
- Use budgets/alerts to catch runaway costs.
What worked well
- Modules drastically cut boilerplate; wiring ALB target group to ECS service is a few lines.
- VPC module outputs are convenient (public/private subnets, route tables, SG helpers).
- CloudFront + S3 module pairing made CDN setup straightforward.
Things to improve later
- Add HTTPS via ACM + ALB listener rule, plus Route53 records.
- Add autoscaling policies for ECS (CPU/Memory) and ALB target tracking.
- Pipeline the modules with GitHub Actions/Terraform Cloud.
- Add canary deployments with CodeDeploy or blue/green via ECS.
