mdashikjs/blog
All posts
Managing AWS Infrastructure with Terraform
DevOps

Managing AWS Infrastructure with Terraform

DevOps5 min

Managing AWS Infrastructure with Terraform

ClickOps is how infrastructure breaks silently. Terraform lets you version-control your cloud, review changes before they happen, and reproduce any environment from a single command.

TerraformAWSInfrastructure as CodeDevOps
Share:

Why Terraform over CloudFormation

CloudFormation is AWS-only and its YAML/JSON syntax is painful for anything non-trivial. Terraform's HCL is readable, supports modules, and works across providers. When we added Cloudflare DNS and Datadog monitoring, Terraform managed all three from one codebase.

Project structure that scales

infra/
├── modules/
│   ├── vpc/
│   ├── ecs/
│   └── rds/
├── environments/
│   ├── staging/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── terraform.tfvars
│   └── production/
│       ├── main.tf
│       ├── variables.tf
│       └── terraform.tfvars
└── backend.tf

Modules are reusable components. Environments compose modules with different variables. Staging and production use the same modules with different sizes and replica counts.

Remote state is mandatory

Local state files are a single point of failure. Use S3 with DynamoDB locking:

terraform {
  backend "s3" {
    bucket         = "myapp-terraform-state"
    key            = "production/terraform.tfstate"
    region         = "ap-south-1"
    dynamodb_table = "terraform-locks"
    encrypt        = true
  }
}

The DynamoDB table prevents two engineers from applying at the same time and corrupting state.

A real module example

# modules/ecs/main.tf
resource "aws_ecs_service" "api" {
  name            = var.service_name
  cluster         = var.cluster_id
  task_definition = aws_ecs_task_definition.api.arn
  desired_count   = var.desired_count

  deployment_minimum_healthy_percent = 100
  deployment_maximum_percent         = 200

  load_balancer {
    target_group_arn = var.target_group_arn
    container_name   = var.service_name
    container_port   = var.container_port
  }
}

resource "aws_ecs_task_definition" "api" {
  family                   = var.service_name
  requires_compatibilities = ["FARGATE"]
  cpu                      = var.cpu
  memory                   = var.memory
  network_mode             = "awsvpc"
  execution_role_arn       = var.execution_role_arn

  container_definitions = jsonencode([{
    name  = var.service_name
    image = var.image
    portMappings = [{ containerPort = var.container_port }]
    logConfiguration = {
      logDriver = "awslogs"
      options = {
        "awslogs-group"         = var.log_group
        "awslogs-region"        = var.region
        "awslogs-stream-prefix" = var.service_name
      }
    }
  }])
}

Plan before apply, always

terraform plan -out=tfplan
# Review the output carefully
terraform apply tfplan

Never run terraform apply without a plan. The plan shows exactly what will be created, modified, or destroyed. We run plan in CI on every PR and post the output as a comment. The apply only runs after merge.

Handling secrets

Never put secrets in .tfvars files. Use AWS Secrets Manager or SSM Parameter Store and reference them:

data "aws_secretsmanager_secret_version" "db_password" {
  secret_id = "production/db-password"
}

resource "aws_db_instance" "main" {
  password = data.aws_secretsmanager_secret_version.db_password.secret_string
}

The rule I enforce

If it exists in AWS but not in Terraform, it does not exist. No exceptions. Every manual change gets terraform import-ed or recreated through code.

MA

Written by Md Ashik

Senior Software Engineer building reliable backends. I write about the practical tradeoffs behind shipping software that holds up in production.