This repository contains a reusable Terraform MVP that provisions a production-ready three-tier web application on AWS. It bootstraps networking, load balancing, compute, and data resources with opinionated defaults and exposes variables so teams can easily customize the stack for their workloads.
Internet -> ALB (public subnets) -> EC2 ASG (private app subnets) -> RDS (isolated DB subnets)
| |
+----- NAT Gateway ------+
- Network tier – Creates a VPC with public, application, and database subnets across multiple AZs, plus internet and NAT gateways, routing tables, and tags.
- Application tier – Deploys an internet-facing Application Load Balancer, target group, HTTP/optional HTTPS listeners, IAM instance profile, launch template, and Auto Scaling Group of EC2 instances (Amazon Linux 2023) that serve a simple Nginx landing page via user data.
- Data tier – Provisions an Amazon RDS instance (PostgreSQL by default) in isolated subnets with security-group access restricted to the application tier. Encryption, automated backups, deletion protection, and multi-AZ are toggled through variables.
Each tier is encapsulated in its own Terraform module so you can reuse them independently or extend them for additional environments.
| Module | Description | Key resources |
|---|---|---|
modules/network |
Base networking fabric | aws_vpc, aws_subnet, aws_nat_gateway, routing |
modules/application |
Load balanced compute tier | aws_lb, aws_autoscaling_group, IAM roles, security groups |
modules/database |
Managed database tier | aws_db_instance, subnet group, security group |
- Terraform v1.5+ installed and on your
PATH. - AWS CLI (optional but recommended) for authenticating to your account.
- AWS credentials available to Terraform via one of the supported mechanisms (environment variables,
~/.aws/credentials, SSO session, etc.). Setaws_profileinterraform.tfvarsif you rely on a named profile. - An S3 bucket and DynamoDB table for the remote backend defined in
versions.tf. Update that file or supplyterraform init -backend-config=...arguments so the backend points at your infrastructure before the first init.
-
Clone this repository or copy the files into your own infrastructure repo.
-
Review
variables.tfto understand all configurable inputs, including backend-related helpers. -
Create a
terraform.tfvars(start fromterraform.tfvars.example) and customize the values for your environment, including sensitive items likedb_password. -
Update the
terraform { backend "s3" { ... } }block inversions.tf(or pass-backend-configflags) so it references your S3 bucket/DynamoDB table before running init. -
Initialize Terraform:
terraform init # or, to override values without editing versions.tf: # terraform init -backend-config="bucket=my-bucket" -backend-config="key=my/key.tfstate" -backend-config="dynamodb_table=my-locks"
-
(Optional) Review execution plan:
terraform plan
-
Apply the stack:
terraform apply
-
After a successful apply, note the outputs for the ALB DNS name, ASG name, and (optionally) the database endpoint.
- Networking – Control CIDRs, AZ count, and even provide explicit subnet CIDRs. If unspecified, sensible CIDRs are auto-calculated from the VPC range.
- Application tier – Adjust instance type, Auto Scaling min/max/desired capacity, ALB ingress CIDRs, health check path, and whether to attach an HTTPS listener. Provide a custom user data script or rely on the baked-in Nginx sample plus optional extra shell commands.
- Database tier – Toggle the entire tier (
enable_database), switch engines/versions, change storage sizes, enforce encryption, enable multi-AZ, and configure deletion protection. Setdb_skip_final_snapshottofalsein production to keep a backup on destroy.
All variables are documented in variables.tf and within each module. You can also compose the modules individually in your own root module if you need more control.
- Ingress to the ALB defaults to
0.0.0.0/0; restrict withalb_ingress_cidrs. - Database access is locked to the application security group by default, keeping the DB private.
- IAM roles include SSM managed instance core, enabling Session Manager access without SSH keys.
- Consider remote backend storage (S3/DynamoDB) and CI/CD integration before using this in production.
- Review AWS service limits (Elastic IPs, NAT gateways, RDS instance types) for your target region.
Destroy the environment when you are done to avoid ongoing costs:
terraform destroyAlways confirm there are no leftover stateful resources (RDS snapshots, EBS volumes, etc.) before concluding cleanup. If db_skip_final_snapshot is false, you must manually handle the retained snapshot.
| Name | Version |
|---|---|
| terraform | >= 1.5.0 |
| aws | >= 5.0 |
No providers.
| Name | Source | Version |
|---|---|---|
| application | ./modules/application | n/a |
| database | ./modules/database | n/a |
| network | ./modules/network | n/a |
No resources.
| Name | Description | Type | Default | Required |
|---|---|---|---|---|
| additional_tags | Optional additional tags to apply to supported resources. | map(string) |
{} |
no |
| alb_ingress_cidrs | CIDR blocks allowed to reach the public load balancer. | list(string) |
[ |
no |
| app_desired_capacity | Desired number of application instances. | number |
2 |
no |
| app_health_check_path | Health check path for the target group. | string |
"/" |
no |
| app_instance_type | EC2 instance type for the application tier. | string |
"t3.micro" |
no |
| app_max_size | Maximum number of application instances in the ASG. | number |
4 |
no |
| app_min_size | Minimum number of application instances in the ASG. | number |
1 |
no |
| app_port | Port where the application listens inside the instance. | number |
80 |
no |
| app_subnet_cidrs | Optional list of CIDR blocks for application subnets. Leave null to auto-calculate. | list(string) |
null |
no |
| app_user_data | Optional shell user data script for compute instances. Leave null to use the module default sample app. | string |
null |
no |
| aws_profile | Optional AWS CLI profile name to use for authentication. | string |
null |
no |
| aws_region | AWS region to deploy the stack into. | string |
n/a | yes |
| az_count | Number of availability zones to span. | number |
2 |
no |
| certificate_arn | ACM certificate ARN for the HTTPS listener. Required when enable_https_listener is true. | string |
null |
no |
| db_allocated_storage | Initial storage allocation for the database in GB. | number |
20 |
no |
| db_apply_immediately | Apply database changes immediately instead of waiting for the next maintenance window. | bool |
true |
no |
| db_backup_retention | Backup retention period in days. | number |
7 |
no |
| db_deletion_protection | Protect the database from accidental deletion. | bool |
false |
no |
| db_engine | Database engine (postgres, mysql, etc.). | string |
"postgres" |
no |
| db_engine_version | Database engine version. | string |
"15.3" |
no |
| db_instance_class | Instance class for the RDS instance. | string |
"db.t3.micro" |
no |
| db_max_allocated_storage | Maximum storage autoscaling size for the database in GB. | number |
100 |
no |
| db_multi_az | Enable multi-AZ deployment for RDS. | bool |
false |
no |
| db_password | Master password for the database. | string |
n/a | yes |
| db_port | Port for the database listener. Defaults to standard engine port. | number |
5432 |
no |
| db_publicly_accessible | Whether the DB instance has a public endpoint. Keep false for production. | bool |
false |
no |
| db_skip_final_snapshot | Skip final snapshot on destroy. Disable to retain a backup. | bool |
true |
no |
| db_storage_encrypted | Enable storage encryption for the DB instance. | bool |
true |
no |
| db_subnet_cidrs | Optional list of CIDR blocks for database subnets. Leave null to auto-calculate. | list(string) |
null |
no |
| db_username | Master username for the database. | string |
"appuser" |
no |
| enable_database | Set to false to skip provisioning the database tier. | bool |
true |
no |
| enable_https_listener | Whether to create an HTTPS listener on the ALB. | bool |
false |
no |
| environment | Environment label (dev, staging, prod, etc.). | string |
n/a | yes |
| extra_app_user_data | Additional shell commands appended to the default user data script. | string |
"" |
no |
| project_name | Short name for the project used in tags and resource names. | string |
n/a | yes |
| public_subnet_cidrs | Optional list of CIDR blocks for public subnets. Leave null to auto-calculate. | list(string) |
null |
no |
| vpc_cidr | CIDR block for the VPC. | string |
"10.0.0.0/16" |
no |
| Name | Description |
|---|---|
| alb_dns_name | DNS name of the application load balancer. |
| app_autoscaling_group_name | Name of the application Auto Scaling Group. |
| database_endpoint | Writer endpoint of the database (if created). |
| public_subnet_ids | IDs of the public subnets. |
| vpc_id | ID of the provisioned VPC. |