Why Infrastructure as Code Is Non-Negotiable
Manually clicking through AWS consoles to provision infrastructure works until it doesn't — until you need to recreate an environment after an incident, audit what changed and when, or onboard a new engineer who needs to spin up a dev environment. Infrastructure as Code makes your cloud setup reproducible, reviewable, and version-controlled. Terraform is the most widely adopted IaC tool and works across all major cloud providers.
Project Structure for Real-World Terraform
Organise Terraform code into reusable modules and environment-specific root configurations. Avoid putting all resources in a single main.tf — it becomes unmanageable past a few dozen resources.
terraform/
modules/
vpc/
main.tf
variables.tf
outputs.tf
ecs-service/
main.tf
variables.tf
outputs.tf
rds/
main.tf
variables.tf
outputs.tf
environments/
staging/
main.tf # composes modules
terraform.tfvars
backend.tf
production/
main.tf
terraform.tfvars
backend.tf
Each module is a self-contained unit with clearly defined inputs and outputs. The environment configurations consume modules with environment-specific variable values.
Remote State with S3 and DynamoDB Locking
Never store Terraform state locally. Use S3 for state storage and DynamoDB for state locking — this prevents two engineers from running terraform apply simultaneously and corrupting state.
# backend.tf
terraform {
backend "s3" {
bucket = "my-company-terraform-state"
key = "production/terraform.tfstate"
region = "us-east-1"
encrypt = true
dynamodb_table = "terraform-state-lock"
}
}
# bootstrap the backend (one-time setup)
resource "aws_s3_bucket" "tf_state" {
bucket = "my-company-terraform-state"
}
resource "aws_s3_bucket_versioning" "tf_state" {
bucket = aws_s3_bucket.tf_state.id
versioning_configuration { status = "Enabled" }
}
resource "aws_dynamodb_table" "tf_lock" {
name = "terraform-state-lock"
billing_mode = "PAY_PER_REQUEST"
hash_key = "LockID"
attribute {
name = "LockID"
type = "S"
}
}
Managing Secrets: Never in State
Terraform state is stored in plaintext in S3. Never put secrets — database passwords, API keys, TLS private keys — as Terraform variables that end up in state. Use AWS Secrets Manager or SSM Parameter Store and reference secrets by ARN.
# Store the secret outside Terraform (manually or via AWS CLI)
# Reference it in Terraform without reading the value into state
data "aws_secretsmanager_secret_version" "db_password" {
secret_id = "prod/db/password"
}
resource "aws_ecs_task_definition" "app" {
# ...
container_definitions = jsonencode([{
environment = [
{
name = "DB_PASSWORD"
value = data.aws_secretsmanager_secret_version.db_password.secret_string
}
]
}])
}
CI/CD Integration: Safe Automated Applies
Run terraform plan on every pull request and post the plan as a PR comment. Only run terraform apply after a merge to main, with a required manual approval step for production.
# .github/workflows/terraform.yml
on:
pull_request:
paths: ['terraform/**']
jobs:
plan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
- name: Terraform Plan
run: |
terraform init
terraform plan -out=tfplan
working-directory: terraform/environments/production
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
- name: Post plan to PR
uses: actions/github-script@v7
with:
script: |
github.rest.issues.createComment({
issue_number: context.issue.number,
body: `## Terraform Plan
```${planOutput}````
})
Common Pitfalls
- Destroying and recreating resources on rename: Use
movedblocks in Terraform 1.1+ to tell Terraform that a resource was renamed, not replaced. - Drift between state and reality: Run
terraform refreshor useterraform importto bring manually-created resources under Terraform management. - Everything in one workspace: Use separate state files per environment — never share state between staging and production.
- Hardcoded AMI IDs: Use data sources to look up the latest AMI dynamically so instances don't fall behind on security patches.
Terraform invested in upfront is the best insurance policy for your infrastructure. The teams that skip it pay the debt in incident recovery time, audit failures, and environment inconsistency.