Infrastructure as Code (IaC) has revolutionized how we manage cloud resources, bringing software development practices to infrastructure management. This comprehensive guide explores building a production-ready AWS infrastructure using Terraform with best practices for scalability, maintainability, and security.
Traditional manual infrastructure management leads to:
IaC solves these problems by treating infrastructure as software code.
terraform-aws-infrastructure/
├── environments/
│ ├── dev/
│ ├── staging/
│ └── prod/
├── modules/
│ ├── vpc/
│ ├── security-groups/
│ ├── ec2/
│ ├── rds/
│ └── s3/
├── shared/
│ ├── backend.tf
│ └── providers.tf
└── scripts/
├── deploy.sh
└── destroy.sh
# 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"
Environment = var.environment
}
}
resource "aws_internet_gateway" "main" {
vpc_id = aws_vpc.main.id
tags = {
Name = "${var.environment}-igw"
Environment = var.environment
}
}
resource "aws_subnet" "public" {
count = length(var.public_subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.public_subnet_cidrs[count.index]
availability_zone = data.aws_availability_zones.available.names[count.index]
map_public_ip_on_launch = true
tags = {
Name = "${var.environment}-public-subnet-${count.index + 1}"
Environment = var.environment
Type = "Public"
}
}
resource "aws_subnet" "private" {
count = length(var.private_subnet_cidrs)
vpc_id = aws_vpc.main.id
cidr_block = var.private_subnet_cidrs[count.index]
availability_zone = data.aws_availability_zones.available.names[count.index]
tags = {
Name = "${var.environment}-private-subnet-${count.index + 1}"
Environment = var.environment
Type = "Private"
}
}
resource "aws_nat_gateway" "main" {
count = length(aws_subnet.public)
allocation_id = aws_eip.nat[count.index].id
subnet_id = aws_subnet.public[count.index].id
tags = {
Name = "${var.environment}-nat-gateway-${count.index + 1}"
Environment = var.environment
}
depends_on = [aws_internet_gateway.main]
}
resource "aws_eip" "nat" {
count = length(aws_subnet.public)
domain = "vpc"
tags = {
Name = "${var.environment}-nat-eip-${count.index + 1}"
Environment = var.environment
}
}
# modules/security-groups/main.tf
resource "aws_security_group" "web" {
name_prefix = "${var.environment}-web-"
vpc_id = var.vpc_id
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.environment}-web-sg"
Environment = var.environment
}
}
resource "aws_security_group" "app" {
name_prefix = "${var.environment}-app-"
vpc_id = var.vpc_id
ingress {
from_port = 8080
to_port = 8080
protocol = "tcp"
security_groups = [aws_security_group.web.id]
}
egress {
from_port = 0
to_port = 0
protocol = "-1"
cidr_blocks = ["0.0.0.0/0"]
}
tags = {
Name = "${var.environment}-app-sg"
Environment = var.environment
}
}
resource "aws_security_group" "database" {
name_prefix = "${var.environment}-db-"
vpc_id = var.vpc_id
ingress {
from_port = 3306
to_port = 3306
protocol = "tcp"
security_groups = [aws_security_group.app.id]
}
tags = {
Name = "${var.environment}-db-sg"
Environment = var.environment
}
}
# modules/ec2/main.tf
resource "aws_launch_template" "app" {
name_prefix = "${var.environment}-app-"
image_id = var.ami_id
instance_type = var.instance_type
vpc_security_group_ids = [var.security_group_id]
user_data = base64encode(templatefile("${path.module}/user-data.sh", {
environment = var.environment
}))
tag_specifications {
resource_type = "instance"
tags = {
Name = "${var.environment}-app-instance"
Environment = var.environment
}
}
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 = [aws_lb_target_group.app.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"
}
tag {
key = "Name"
value = "${var.environment}-app-asg"
propagate_at_launch = false
}
tag {
key = "Environment"
value = var.environment
propagate_at_launch = true
}
}
# Application Load Balancer
resource "aws_lb" "app" {
name = "${var.environment}-app-lb"
internal = false
load_balancer_type = "application"
security_groups = [var.alb_security_group_id]
subnets = var.public_subnet_ids
enable_deletion_protection = var.environment == "prod" ? true : false
tags = {
Name = "${var.environment}-app-lb"
Environment = var.environment
}
}
resource "aws_lb_target_group" "app" {
name = "${var.environment}-app-tg"
port = 8080
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
}
tags = {
Name = "${var.environment}-app-tg"
Environment = var.environment
}
}
resource "aws_lb_listener" "app" {
load_balancer_arn = aws_lb.app.arn
port = "80"
protocol = "HTTP"
default_action {
type = "forward"
target_group_arn = aws_lb_target_group.app.arn
}
}
# 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"
Environment = var.environment
}
}
resource "aws_db_parameter_group" "main" {
family = "mysql8.0"
name = "${var.environment}-db-params"
parameter {
name = "innodb_buffer_pool_size"
value = "{DBInstanceClassMemory*3/4}"
}
tags = {
Name = "${var.environment}-db-params"
Environment = var.environment
}
}
resource "aws_db_instance" "main" {
identifier = "${var.environment}-database"
engine = "mysql"
engine_version = "8.0"
instance_class = var.instance_class
allocated_storage = var.allocated_storage
max_allocated_storage = var.max_allocated_storage
storage_type = "gp2"
storage_encrypted = true
db_name = var.database_name
username = var.username
password = var.password
vpc_security_group_ids = [var.security_group_id]
db_subnet_group_name = aws_db_subnet_group.main.name
parameter_group_name = aws_db_parameter_group.main.name
backup_retention_period = var.backup_retention_period
backup_window = "03:00-04:00"
maintenance_window = "Sun:04:00-Sun:05:00"
multi_az = var.environment == "prod" ? true : false
publicly_accessible = false
copy_tags_to_snapshot = true
delete_automated_backups = false
deletion_protection = var.environment == "prod" ? true : false
skip_final_snapshot = var.environment != "prod"
final_snapshot_identifier = var.environment == "prod" ? "${var.environment}-db-final-snapshot-${formatdate("YYYY-MM-DD-hhmm", timestamp())}" : null
tags = {
Name = "${var.environment}-database"
Environment = var.environment
}
}
# environments/prod/main.tf
terraform {
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "environments/prod/terraform.tfstate"
region = "us-west-2"
encrypt = true
}
}
module "vpc" {
source = "../../modules/vpc"
environment = "prod"
vpc_cidr = "10.0.0.0/16"
public_subnet_cidrs = ["10.0.1.0/24", "10.0.2.0/24"]
private_subnet_cidrs = ["10.0.10.0/24", "10.0.20.0/24"]
}
module "security_groups" {
source = "../../modules/security-groups"
environment = "prod"
vpc_id = module.vpc.vpc_id
}
module "app_servers" {
source = "../../modules/ec2"
environment = "prod"
ami_id = "ami-0c02fb55956c7d316"
instance_type = "t3.medium"
min_size = 2
max_size = 10
desired_capacity = 4
vpc_id = module.vpc.vpc_id
private_subnet_ids = module.vpc.private_subnet_ids
public_subnet_ids = module.vpc.public_subnet_ids
security_group_id = module.security_groups.app_security_group_id
alb_security_group_id = module.security_groups.web_security_group_id
}
module "database" {
source = "../../modules/rds"
environment = "prod"
instance_class = "db.t3.medium"
allocated_storage = 100
max_allocated_storage = 1000
database_name = "myapp"
username = "admin"
password = var.db_password
private_subnet_ids = module.vpc.private_subnet_ids
security_group_id = module.security_groups.database_security_group_id
backup_retention_period = 7
}
# environments/dev/main.tf
terraform {
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "environments/dev/terraform.tfstate"
region = "us-west-2"
encrypt = true
}
}
module "vpc" {
source = "../../modules/vpc"
environment = "dev"
vpc_cidr = "10.1.0.0/16"
public_subnet_cidrs = ["10.1.1.0/24"]
private_subnet_cidrs = ["10.1.10.0/24"]
}
module "security_groups" {
source = "../../modules/security-groups"
environment = "dev"
vpc_id = module.vpc.vpc_id
}
module "app_servers" {
source = "../../modules/ec2"
environment = "dev"
ami_id = "ami-0c02fb55956c7d316"
instance_type = "t3.micro"
min_size = 1
max_size = 2
desired_capacity = 1
vpc_id = module.vpc.vpc_id
private_subnet_ids = module.vpc.private_subnet_ids
public_subnet_ids = module.vpc.public_subnet_ids
security_group_id = module.security_groups.app_security_group_id
alb_security_group_id = module.security_groups.web_security_group_id
}
module "database" {
source = "../../modules/rds"
environment = "dev"
instance_class = "db.t3.micro"
allocated_storage = 20
max_allocated_storage = 100
database_name = "myapp_dev"
username = "admin"
password = var.db_password
private_subnet_ids = module.vpc.private_subnet_ids
security_group_id = module.security_groups.database_security_group_id
backup_retention_period = 1
}
# shared/backend.tf
terraform {
backend "s3" {
bucket = "my-terraform-state-bucket"
key = "global/s3/terraform.tfstate"
region = "us-west-2"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
# Create S3 bucket for state storage
resource "aws_s3_bucket" "terraform_state" {
bucket = "my-terraform-state-bucket"
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 = "AES256"
}
}
}
# DynamoDB table for state locking
resource "aws_dynamodb_table" "terraform_locks" {
name = "terraform-state-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
name: Terraform Infrastructure
on:
push:
branches: [main]
paths: ['terraform/**']
pull_request:
branches: [main]
paths: ['terraform/**']
env:
AWS_REGION: us-west-2
jobs:
terraform:
name: Terraform
runs-on: ubuntu-latest
strategy:
matrix:
environment: [dev, staging, prod]
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Setup Terraform
uses: hashicorp/setup-terraform@v2
with:
terraform_version: 1.5.0
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v2
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}
- name: Terraform Init
working-directory: ./terraform/environments/${{ matrix.environment }}
run: terraform init
- name: Terraform Validate
working-directory: ./terraform/environments/${{ matrix.environment }}
run: terraform validate
- name: Terraform Plan
working-directory: ./terraform/environments/${{ matrix.environment }}
run: terraform plan -var="db_password=${{ secrets.DB_PASSWORD }}"
- name: Terraform Apply
if: github.ref == 'refs/heads/main' && matrix.environment != 'prod'
working-directory: ./terraform/environments/${{ matrix.environment }}
run: terraform apply -auto-approve -var="db_password=${{ secrets.DB_PASSWORD }}"
- name: Terraform Apply (Production)
if: github.ref == 'refs/heads/main' && matrix.environment == 'prod'
working-directory: ./terraform/environments/${{ matrix.environment }}
run: |
echo "Production deployment requires manual approval"
# Manual approval step would go here
# IAM role for EC2 instances
resource "aws_iam_role" "ec2_role" {
name = "${var.environment}-ec2-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" "ec2_policy" {
name = "${var.environment}-ec2-policy"
role = aws_iam_role.ec2_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = [
"s3:GetObject",
"s3:PutObject"
]
Resource = [
"${aws_s3_bucket.app_bucket.arn}/*"
]
},
{
Effect = "Allow"
Action = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
Resource = "arn:aws:logs:*:*:*"
}
]
})
}
resource "aws_cloudwatch_metric_alarm" "high_cpu" {
alarm_name = "${var.environment}-high-cpu"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = "2"
metric_name = "CPUUtilization"
namespace = "AWS/EC2"
period = "120"
statistic = "Average"
threshold = "80"
alarm_description = "This metric monitors ec2 cpu utilization"
dimensions = {
AutoScalingGroupName = aws_autoscaling_group.app.name
}
alarm_actions = [aws_sns_topic.alerts.arn]
tags = {
Environment = var.environment
}
}
resource "aws_sns_topic" "alerts" {
name = "${var.environment}-alerts"
tags = {
Environment = var.environment
}
}
locals {
common_tags = {
Environment = var.environment
Project = "my-application"
Owner = "platform-team"
ManagedBy = "terraform"
CostCenter = "engineering"
CreatedDate = formatdate("YYYY-MM-DD", timestamp())
}
}
# Apply tags to all resources
resource "aws_instance" "app" {
# ... other configuration
tags = merge(local.common_tags, {
Name = "${var.environment}-app-server"
Type = "application"
})
}
Infrastructure as Code with Terraform provides a robust foundation for managing AWS resources at scale. The modular approach demonstrated here enables:
Start with a simple setup and gradually add complexity as your requirements grow. The investment in proper IaC practices pays dividends in operational efficiency and reliability.
Need help implementing Infrastructure as Code in your organization? Contact me to discuss your infrastructure automation strategy.