Skip to content
Back to Build Tutorials

IaC Starter

Define your cloud infra in Terraform/Pulumi so environments are reproducible and disposable.

Why

Click-ops infrastructure is unreviewable, unreproducible, and undisposable — you can't create a staging env that matches prod because nobody remembers which checkboxes were ticked. Infrastructure as Code makes every cloud resource a reviewed, versioned artifact: a new staging env is 'terraform apply -var-file=staging.tfvars' and it matches prod by construction. When prod drifts, you diff the plan instead of screenshotting a console. The break-even is one recreated environment — if you've ever spent a Friday rebuilding a VPC by hand, you already know the cost of not having IaC.

When

When you have ≥2 cloud resources or ≥2 environments. Takes 2-3 h for Terraform basics. Skip for pure static deploys to a CDN.

How

1. Choose Terraform (HCL, declarative) or Pulumi (TS/Python, programmable)
   - Terraform: larger ecosystem, simpler for pure cloud resources
   - Pulumi: better if your team already writes TS and wants loops/conditionals

2. Organize by environment: modules/ (reusable) + envs/{staging,prod}/ (composition)
   - modules/db/ defines a Postgres instance with all its flags
   - envs/staging/main.tf calls that module with staging-specific vars

3. Use a remote state backend (S3+DynamoDB lock, or Terraform Cloud)
   - Never use local state in a team — you will get concurrent-apply corruption
   - State contains secrets output values; protect the backend with encryption + IAM

4. Make every resource tagged with {Environment, Project, ManagedBy="terraform"}
   - Tags let you find unmanaged resources (drift) and cost-allocate in the billing dashboard

5. CI plan-on-PR: terraform plan on every PR, apply on merge to main
   - The PR comment shows the plan diff — reviewers see exactly what cloud changes before apply

Example

# modules/db/main.tf
resource "aws_db_instance" "main" {
  identifier         = "${var.env}-db"
  engine             = "postgres"
  instance_class     = var.instance_class
  allocated_storage  = 20
  name               = "appdb"
  username           = var.db_user
  password           = var.db_password  # from secrets manager, not plaintext
  tags = {
    Environment = var.env
    ManagedBy   = "terraform"
  }
}

# envs/staging/main.tf
module "db" {
  source        = "../../modules/db"
  env           = "staging"
  instance_class = "db.t4g.small"
}

Common Problems

  • ⚠Storing state locally and getting 'state lock' errors when two devs run apply simultaneously.
  • ⚠Putting DB passwords in plaintext variables instead of referencing a secrets manager — they end up in the state file in cleartext.
  • ⚠Manually fixing a resource in the console, so Terraform now shows perpetual drift and apply tries to 'fix' it back.
  • ⚠One giant main.tf with 500 resources instead of modules — every change risks breaking an unrelated resource.

Best Practices

  • ✓Run 'terraform fmt -recursive' and 'terraform validate' in CI before plan — formatting drift is noise in PR diffs.
  • ✓Use 'terraform import' to adopt existing hand-created resources instead of destroying and recreating them.
  • ✓Mark resources that hold data with lifecycle { prevent_destroy = true } so a bad apply can't delete prod data.
  • ✓Keep modules small (≤20 resources) and versioned — a module is a library, treat it with the same review discipline.