Back to Blog
DevOps

Infrastructure as Code with Terraform: AWS Multi-Environment Setup

July 28, 2024
10 min read
By Oscar M. Cabrisses
TerraformAWSInfrastructure as CodeDevOpsAutomationCI/CD

Infrastructure as Code with Terraform: AWS Multi-Environment Setup

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.

Why Infrastructure as Code?

Traditional manual infrastructure management leads to:

  • Configuration Drift: Environments diverge over time
  • No Version Control: Changes are not tracked or reversible
  • Manual Errors: Human mistakes in complex configurations
  • Slow Provisioning: Time-consuming manual processes

IaC solves these problems by treating infrastructure as software code.

Project Structure

terraform-aws-infrastructure/
├── environments/
│   ├── dev/
│   ├── staging/
│   └── prod/
├── modules/
│   ├── vpc/
│   ├── security-groups/
│   ├── ec2/
│   ├── rds/
│   └── s3/
├── shared/
│   ├── backend.tf
│   └── providers.tf
└── scripts/
    ├── deploy.sh
    └── destroy.sh

Core Infrastructure Modules

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"
    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
  }
}

Security Groups Module

# 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
  }
}

Application Layer

Auto Scaling Group with Launch Template

# 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
  }
}

Database Layer

RDS with Multi-AZ

# 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
  }
}

Environment Configuration

Production 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
}

Development Environment

# 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
}

State Management and CI/CD

Remote State Configuration

# 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"
  }
}

GitHub Actions Workflow

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

Security Best Practices

IAM Policies and Roles

# 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:*:*:*"
      }
    ]
  })
}

Monitoring and Alerting

CloudWatch Integration

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
  }
}

Cost Optimization

Resource Tagging Strategy

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"
  })
}

Best Practices Summary

  1. Module Design: Create reusable, parameterized modules
  2. State Management: Use remote state with locking
  3. Environment Separation: Separate state files per environment
  4. Security: Follow least privilege principle
  5. Version Control: Pin Terraform and provider versions
  6. Documentation: Document variables and outputs
  7. Testing: Validate configurations before applying
  8. Cost Control: Implement proper tagging and monitoring

Conclusion

Infrastructure as Code with Terraform provides a robust foundation for managing AWS resources at scale. The modular approach demonstrated here enables:

  • Consistency across environments
  • Version control for infrastructure changes
  • Automated deployment through CI/CD
  • Cost optimization through proper resource management

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.