DevOps
AWS Infrastructure Project
Build a complete AWS infrastructure using Terraform: VPC, EC2, RDS, ALB, Auto Scaling, and IAM with best practices and state management.
By TechCoder TeamLast updated: 2026-06-02
In a Nutshell
Build a complete AWS infrastructure using Terraform: VPC, EC2, RDS, ALB, Auto Scaling, and IAM with best practices and state management. This hands-on tutorial focuses on practical implementation of aws infrastructure project concepts.
Project 3: AWS Infrastructure with Terraform
Build production-ready AWS infrastructure using Infrastructure as Code principles.
Project Overview
┌─────────────────────────────────────────────────────────────────┐
│ AWS Infrastructure │
├─────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ VPC (10.0.0.0/16) │ │
│ │ │ │
│ │ ┌───────────────┐ ┌───────────────┐ │ │
│ │ │ Public Subnet│ │ Public Subnet│ │ │
│ │ │ 10.0.1.0/24 │ │ 10.0.2.0/24 │ │ │
│ │ │ │ │ │ │ │
│ │ │ • ALB │ │ • NAT Gateway │ │ │
│ │ │ • Bastion │ │ │ │ │
│ │ └───────┬───────┘ └───────────────┘ │ │
│ │ │ │ │
│ │ ┌───────┴────────┐ ┌───────────────┐ │ │
│ │ │ Private Subnet│ │ Private Subnet│ │ │
│ │ │ 10.0.3.0/24 │ │ 10.0.4.0/24 │ │ │
│ │ │ │ │ │ │ │
│ │ │ • EC2 (App) │ │ • EC2 (App) │ │ │
│ │ │ • RDS Primary │◄─────>│ • RDS Standby │ │ │
│ │ └────────────────┘ └────────────────┘ │ │
│ │ │ │
│ └────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────┐ ┌────────────────┐ │
│ │ Auto Scaling │ │ S3 (Backups) │ │
│ │ Group │ │ │ │
│ └────────────────┘ └────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Project Structure
terraform-aws-infra/
├── environments/
│ ├── dev/
│ │ ├── main.tf
│ │ ├── variables.tf
│ │ └── terraform.tfvars
│ ├── staging/
│ └── production/
├── modules/
│ ├── vpc/
│ ├── alb/
│ ├── ec2/
│ ├── rds/
│ ├── iam/
│ └── s3/
├── backend.tf
├── provider.tf
└── README.md
Part 1: Backend Configuration
1.1 Remote State (S3 + DynamoDB)
# backend.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
backend "s3" {
bucket = "mycompany-terraform-state"
key = "production/infrastructure.tfstate"
region = "us-east-1"
encrypt = true
kms_key_id = "arn:aws:kms:us-east-1:123456789:key/terraform-state"
dynamodb_table = "terraform-locks"
}
}
provider "aws" {
region = var.aws_region
default_tags {
tags = {
Environment = var.environment
ManagedBy = "Terraform"
Project = "aws-infrastructure"
}
}
}
1.2 State Resources
# backend-resources.tf (run once manually)
resource "aws_s3_bucket" "terraform_state" {
bucket = "mycompany-terraform-state"
lifecycle {
prevent_destroy = true
}
}
resource "aws_s3_bucket_versioning" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "terraform_state" {
bucket = aws_s3_bucket.terraform_state.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "aws:kms"
kms_master_key_id = aws_kms_key.terraform.arn
}
}
}
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-locks"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
Part 2: VPC Module
# modules/vpc/main.tf
resource "aws_vpc" "main" {
cidr_block = var.vpc_cidr
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.environment}-vpc"
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.environment}-igw"
}
}
# Public Subnets
resource "aws_subnet" "public" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index)
availability_zone = var.availability_zones[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.environment}-public-${count.index + 1}"
Type = "Public"
}
}
# Private Subnets
resource "aws_subnet" "private" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
cidr_block = cidrsubnet(var.vpc_cidr, 8, count.index + length(var.availability_zones))
availability_zone = var.availability_zones[count.index]
tags = {
Name = "${var.environment}-private-${count.index + 1}"
Type = "Private"
}
}
# NAT Gateways (one per AZ for HA)
resource "aws_eip" "nat" {
count = var.enable_nat_gateway ? length(var.availability_zones) : 0
domain = "vpc"
tags = {
Name = "${var.environment}-nat-${count.index + 1}"
}
}
resource "aws_nat_gateway" "main" {
count = var.enable_nat_gateway ? length(var.availability_zones) : 0
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = {
Name = "${var.environment}-nat-${count.index + 1}"
}
}
# Route Tables
resource "aws_route_table" "public" {
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
gateway_id = aws_internet_gateway.main.id
}
tags = {
Name = "${var.environment}-public-rt"
}
}
resource "aws_route_table" "private" {
count = length(var.availability_zones)
vpc_id = aws_vpc.main.id
route {
cidr_block = "0.0.0.0/0"
nat_gateway_id = aws_nat_gateway.main[count.index].id
}
tags = {
Name = "${var.environment}-private-rt-${count.index + 1}"
}
}
# Route Table Associations
resource "aws_route_table_association" "public" {
count = length(var.availability_zones)
subnet_id = aws_subnet.public[count.index].id
route_table_id = aws_route_table.public.id
}
resource "aws_route_table_association" "private" {
count = length(var.availability_zones)
subnet_id = aws_subnet.private[count.index].id
route_table_id = aws_route_table.private[count.index].id
}
# modules/vpc/outputs.tf
output "vpc_id" {
description = "ID of the VPC"
value = aws_vpc.main.id
}
output "public_subnet_ids" {
description = "IDs of public subnets"
value = aws_subnet.public[*].id
}
output "private_subnet_ids" {
description = "IDs of private subnets"
value = aws_subnet.private[*].id
}
output "nat_gateway_ids" {
description = "IDs of NAT Gateways"
value = aws_nat_gateway.main[*].id
}
Part 3: ALB Module
# modules/alb/main.tf
resource "aws_security_group" "alb" {
name_prefix = "${var.environment}-alb-"
description = "Security group for ALB"
vpc_id = var.vpc_id
ingress {
description = "HTTP from internet"
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
description = "HTTPS from internet"
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
description = "Allow all outbound"
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.environment}-alb-sg"
}
lifecycle {
create_before_destroy = true
}
}
resource "aws_lb" "main" {
name = "${var.environment}-alb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.alb.id]
subnets = var.public_subnet_ids
enable_deletion_protection = var.environment == "production"
enable_http2 = true
access_logs {
bucket = var.logs_bucket
prefix = "alb-logs"
enabled = true
}
tags = {
Name = "${var.environment}-alb"
}
}
resource "aws_lb_target_group" "app" {
name = "${var.environment}-app-tg"
port = 80
protocol = "HTTP"
vpc_id = var.vpc_id
health_check {
enabled = true
healthy_threshold = 2
interval = 30
matcher = "200"
path = "/health"
port = "traffic-port"
protocol = "HTTP"
timeout = 5
unhealthy_threshold = 2
}
deregistration_delay = 30
stickiness {
type = "lb_cookie"
cookie_duration = 86400
enabled = true
}
tags = {
Name = "${var.environment}-app-tg"
}
}
resource "aws_lb_listener" "http" {
load_balancer_arn = aws_lb.main.arn
port = "80"
protocol = "HTTP"
default_action {
type = "redirect"
redirect {
port = "443"
protocol = "HTTPS"
status_code = "HTTP_301"
}
}
}
resource "aws_lb_listener" "https" {
load_balancer_arn = aws_lb.main.arn
port = "443"
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
certificate_arn = var.certificate_arn
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}
Part 4: EC2 Auto Scaling Module
# modules/ec2/main.tf
resource "aws_security_group" "app" {
name_prefix = "${var.environment}-app-"
description = "Security group for application servers"
vpc_id = var.vpc_id
ingress {
description = "HTTP from ALB"
from_port = 80
to_port = 80
protocol = "tcp"
security_groups = [var.alb_security_group_id]
}
ingress {
description = "SSH from bastion"
from_port = 22
to_port = 22
protocol = "tcp"
cidr_blocks = var.bastion_cidr_blocks
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.environment}-app-sg"
}
lifecycle {
create_before_destroy = true
}
}
data "aws_ami" "amazon_linux" {
most_recent = true
owners = ["amazon"]
filter {
name = "name"
values = ["amzn2-ami-hvm-*-x86_64-gp2"]
}
}
resource "aws_launch_template" "app" {
name_prefix = "${var.environment}-app-"
image_id = data.aws_ami.amazon_linux.id
instance_type = var.instance_type
key_name = var.key_name
vpc_security_group_ids = [aws_security_group.app.id]
user_data = base64encode(templatefile("${path.module}/user_data.sh", {
environment = var.environment
}))
iam_instance_profile {
name = aws_iam_instance_profile.app.name
}
metadata_options {
http_endpoint = "enabled"
http_tokens = "required"
http_put_response_hop_limit = 1
}
tag_specifications {
resource_type = "instance"
tags = {
Name = "${var.environment}-app"
}
}
lifecycle {
create_before_destroy = true
}
}
resource "aws_autoscaling_group" "app" {
name = "${var.environment}-app-asg"
vpc_zone_identifier = var.private_subnet_ids
target_group_arns = [var.target_group_arn]
health_check_type = "ELB"
health_check_grace_period = 300
min_size = var.min_size
max_size = var.max_size
desired_capacity = var.desired_capacity
launch_template {
id = aws_launch_template.app.id
version = "$Latest"
}
instance_refresh {
strategy = "Rolling"
preferences {
min_healthy_percentage = 50
}
triggers = ["tag"]
}
tag {
key = "Name"
value = "${var.environment}-app"
propagate_at_launch = true
}
dynamic "tag" {
for_each = var.tags
content {
key = tag.key
value = tag.value
propagate_at_launch = true
}
}
}
resource "aws_autoscaling_policy" "target_tracking" {
name = "${var.environment}-cpu-tracking"
autoscaling_group_name = aws_autoscaling_group.app.name
policy_type = "TargetTrackingScaling"
target_tracking_configuration {
predefined_metric_specification {
predefined_metric_type = "ASGAverageCPUUtilization"
}
target_value = 60.0
}
}
Part 5: RDS Module
# modules/rds/main.tf
resource "aws_db_subnet_group" "main" {
name = "${var.environment}-db-subnet-group"
subnet_ids = var.private_subnet_ids
tags = {
Name = "${var.environment}-db-subnet-group"
}
}
resource "aws_security_group" "rds" {
name_prefix = "${var.environment}-rds-"
description = "Security group for RDS"
vpc_id = var.vpc_id
ingress {
description = "PostgreSQL from app servers"
from_port = 5432
to_port = 5432
protocol = "tcp"
security_groups = var.allowed_security_group_ids
}
tags = {
Name = "${var.environment}-rds-sg"
}
lifecycle {
create_before_destroy = true
}
}
resource "aws_db_instance" "main" {
identifier = "${var.environment}-postgres"
engine = "postgres"
engine_version = "15.4"
instance_class = var.instance_class
allocated_storage = var.allocated_storage
max_allocated_storage = var.max_allocated_storage
storage_type = "gp3"
storage_encrypted = true
db_name = var.database_name
username = var.master_username
password = var.master_password
multi_az = var.multi_az
db_subnet_group_name = aws_db_subnet_group.main.name
vpc_security_group_ids = [aws_security_group.rds.id]
backup_retention_period = var.backup_retention_period
backup_window = "03:00-04:00"
maintenance_window = "Mon:04:00-Mon:05:00"
skip_final_snapshot = var.environment != "production"
deletion_protection = var.environment == "production"
enabled_cloudwatch_logs_exports = ["postgresql"]
tags = {
Name = "${var.environment}-postgres"
}
}
Part 6: Main Configuration
# environments/production/main.tf
module "vpc" {
source = "../../modules/vpc"
environment = "production"
vpc_cidr = "10.0.0.0/16"
availability_zones = ["us-east-1a", "us-east-1b", "us-east-1c"]
enable_nat_gateway = true
}
module "alb" {
source = "../../modules/alb"
environment = "production"
vpc_id = module.vpc.vpc_id
public_subnet_ids = module.vpc.public_subnet_ids
certificate_arn = var.certificate_arn
logs_bucket = module.s3.logs_bucket_id
}
module "ec2" {
source = "../../modules/ec2"
environment = "production"
vpc_id = module.vpc.vpc_id
private_subnet_ids = module.vpc.private_subnet_ids
alb_security_group_id = module.alb.security_group_id
target_group_arn = module.alb.target_group_arn
instance_type = "t3.medium"
min_size = 2
max_size = 10
desired_capacity = 4
key_name = var.key_name
}
module "rds" {
source = "../../modules/rds"
environment = "production"
vpc_id = module.vpc.vpc_id
private_subnet_ids = module.vpc.private_subnet_ids
instance_class = "db.t3.medium"
allocated_storage = 100
multi_az = true
database_name = "appdb"
master_username = var.db_username
master_password = var.db_password
allowed_security_group_ids = [module.ec2.security_group_id]
}
Deployment Steps
# 1. Initialize Terraform
cd environments/production
terraform init
# 2. Validate configuration
terraform validate
# 3. Plan changes
terraform plan -out=tfplan
# 4. Apply changes
terraform apply tfplan
# 5. Verify outputs
terraform output
# 6. Destroy (if needed)
terraform destroy
Verification
- VPC: Check subnets, route tables, IGW, NAT Gateways
- ALB: Verify target group health checks
- EC2: Confirm instances in Auto Scaling group
- RDS: Check Multi-AZ configuration
- Security Groups: Validate rules
- IAM: Review instance profiles and policies
Deliverables
- [ ] Modular Terraform configuration
- [ ] VPC with public/private subnets
- [ ] ALB with HTTPS and health checks
- [ ] EC2 Auto Scaling with target tracking
- [ ] RDS PostgreSQL with Multi-AZ
- [ ] S3 bucket for logs/backups
- [ ] Remote state configuration
- [ ] Documentation and architecture diagram
Next Steps
Proceed to the interview preparation module.