Capstone: A Complete Multi-Tier Web Infrastructure
You've reached the end
Nineteen articles have built each piece: provider, state, variables, modules, multi-environment, lifecycle, CI/CD, testing. The final article assembles them into something complete β a multi-tier web infrastructure that nearly every real project has in a similar shape: a load balancer in front of a group of elastic servers, a database behind them, and an object store. We organize it into three connected modules, apply for real, access the application, then clean up.
Goal
Build a complete web infrastructure by combining modules, see every technique in the series working together, verify it actually runs, and tear it down cleanly.
π° Cost
This stack has an ALB and RDS β two things billed by the hour (ALB about $0.025/hour, RDS db.t3.micro about $0.017/hour), plus an EC2 t3.micro. Stand it up then destroy within fifteen minutes and the total cost is just a few cents. The key point: destroy right after you're done looking, don't leave it overnight.
Architecture
Internet
β HTTP :80
βββββββββββΌβββββββββββ
β Application LB β public subnet a + b
β (SG: accept from Net)
βββββββββββ¬βββββββββββ
β target group
βββββββββββΌβββββββββββ
β Auto Scaling Group β nginx x2 (t3.micro)
β (SG: ONLY from ALB)
βββββββββββ¬βββββββββββ
β :5432
βββββββββββββββΌββββ βββββββββββββββββ
β RDS PostgreSQL β β S3 (assets) β
β (SG: app only) β β block public β
βββββββββββββββββββ βββββββββββββββββ
βββ all inside VPC 10.0.0.0/16 (network module) βββ
Traffic goes into the ALB, the ALB distributes to instances in the ASG, the instances talk to RDS. Each tier has its own security group, and they reference each other to tighten things: instances only accept HTTP from the ALB (not open straight to the Internet), RDS only accepts 5432 from the instances' security group. This is the basic layered security model, built entirely with references between SGs.
Three modules
The structure has three modules, each wrapping one tier:
20-capstone/
modules/
network/ # VPC, subnet, IGW, route table (reused from Article 14)
web/ # ALB, target group, launch template, ASG, 2 SG
data/ # RDS PostgreSQL, S3, DB subnet group, SG
main.tf # root: connects the three modules
The network module is reused as-is from Article 14, exactly the benefit of modules: write once, use many places. The web module wraps the ALB and ASG: the launch template installs nginx via user_data (the alternative to a provisioner from Article 17), the ASG attaches to the ALB's target group. The data module wraps RDS and S3, where the DB password uses password_wo β the write-only argument from Article 8 so the password doesn't leak into state.
The root connects the three modules with the exact composition technique from Article 13 β one module's output becomes another's input:
module "network" {
source = "./modules/network"
name = var.name
public_subnets = {
(data.aws_availability_zones.available.names[0]) = cidrsubnet("10.0.0.0/16", 8, 0)
(data.aws_availability_zones.available.names[1]) = cidrsubnet("10.0.0.0/16", 8, 1)
}
}
module "web" {
source = "./modules/web"
vpc_id = module.network.vpc_id # network -> web
subnet_ids = module.network.public_subnet_ids
ami_id = data.aws_ami.al2023.id
}
module "data" {
source = "./modules/data"
vpc_id = module.network.vpc_id
subnet_ids = module.network.public_subnet_ids
app_security_group_id = module.web.instance_security_group_id # web -> data
db_password_wo = var.db_password
}
Notice the dependency chain: network provides the VPC and subnets to both web and data; web provides the instances' security group to data so RDS knows who to allow connections from. Terraform reads these references, builds the graph (Article 5), and executes in the right order. Nearly every concept in the series converges here: for_each and cidrsubnet in network, data sources fetching the AMI and AZs, composition between modules, password_wo for secrets, SGs referencing each other.
Apply
$ terraform plan
Plan: 19 to add, 0 to change, 0 to destroy.
$ terraform apply -auto-approve
module.data.aws_db_instance.this: Still creating... [04m30s elapsed]
Apply complete! Resources: 19 added, 0 changed, 0 destroyed.
Nineteen resources from three modules, in a single apply. RDS is the slowest thing (about four-five minutes), Terraform stands up independent branches in parallel while waiting. The output gives us the entry point:
$ terraform output
alb_dns_name = "web-2026...elb.amazonaws.com"
db_endpoint = "<db>.ap-southeast-1.rds.amazonaws.com:5432"
assets_bucket = "tf-capstone-assets-2026..."
End-to-end verification
Infrastructure stood up isn't enough β you have to be sure it runs. Access the ALB:
$ curl http://web-2026...elb.amazonaws.com/
<h1>tf-capstone - ip-10-0-0-129.ap-southeast-1.compute.internal</h1>
The nginx page comes back, along with the internal hostname of the instance serving the request. The whole chain works: the ALB receives HTTP, forwards to an instance in the ASG, the instance runs nginx (installed via user_data at boot) and returns the content. Traffic flows correctly through the tightened security groups.
Also verify that the DB password didn't leak into state, exactly as password_wo promised in Article 8:
$ grep -c 'Capstone-Demo-9182' terraform.tfstate
0
Not once β the password reached RDS at apply time but wasn't stored in state.
π§Ή Teardown
$ terraform destroy -auto-approve
Destroy complete! Resources: 19 destroyed.
Terraform tears down in reverse graph order: ASG and RDS first, then subnets and security groups, finally the VPC. One command wipes all nineteen resources clean β this is the strength of declarative infrastructure: building and tearing down both happen through the configuration itself, no resources left quietly billing. Double-check the console to be sure nothing's left over (ALB and RDS are the easiest to forget).
Series wrap-up
You've gone from "what is Terraform" to a multi-tier web infrastructure built with modules through a repeatable process. The things worth taking with you: infrastructure is declarative code, plan before apply; state is the source of truth that must be kept safe (S3 + use_lockfile, no leaked secrets thanks to password_wo); modules are the unit of reuse, connected through output/input; multiple environments separated by directory layout; and every change should go through CI/CD with quality gates (fmt/validate/tflint/Trivy/Checkov) and testing (terraform test). Throughout, we always pinned versions and always cleaned up after experimenting.
Where to go next
The series stops at a solid foundation, but there's plenty more to dig into. On large-scale organization: HCP Terraform and Stacks for orchestrating many states, or Terragrunt if you go the open-source route. On policy: policy-as-code with Sentinel or OPA to block per company rules right in the pipeline. On scope: write your own modules for the organization and publish them to an internal registry, use providers beyond AWS (Cloudflare, GitHub, Kubernetes) in the same configuration. Each direction builds on the exact foundation these nineteen articles laid β and now you have enough roots to read the documentation yourself and keep going.
You've reached the end