Skip to content

Terraform — Practical

infra/
modules/
vpc/
app/
rds/
live/
dev/
main.tf
backend.tf
terraform.tfvars
prod/
main.tf
backend.tf
terraform.tfvars
.terraform.lock.hcl

Or with Terragrunt for DRY:

infra/
modules/
live/
terragrunt.hcl
dev/
vpc/terragrunt.hcl
app/terragrunt.hcl
prod/
vpc/terragrunt.hcl
app/terragrunt.hcl
terraform {
required_version = ">= 1.6"
backend "s3" {
bucket = "tfstate-prod"
key = "platform/network.tfstate"
region = "eu-west-1"
dynamodb_table = "tflock"
encrypt = true
}
}

Bootstrap chicken-and-egg: create bucket+table once with local state, then migrate.

modules/app/main.tf
variable "name" { type = string }
variable "image" { type = string }
variable "subnets" { type = list(string) }
variable "tags" { type = map(string) default = {} }
resource "aws_ecs_task_definition" "t" {
family = var.name
...
}
resource "aws_ecs_service" "s" {
name = var.name
...
}
output "service_arn" { value = aws_ecs_service.s.id }
live/prod/main.tf
module "api" {
source = "../../modules/app"
name = "api"
image = "ghcr.io/org/api:1.2.3"
subnets = data.terraform_remote_state.network.outputs.private_subnets
tags = { env = "prod", team = "platform" }
}
variable "buckets" {
type = map(object({ versioning = bool }))
default = {
"logs" = { versioning = true },
"assets" = { versioning = false },
}
}
resource "aws_s3_bucket" "b" {
for_each = var.buckets
bucket = "myorg-${each.key}"
}
resource "aws_s3_bucket_versioning" "v" {
for_each = var.buckets
bucket = aws_s3_bucket.b[each.key].id
versioning_configuration {
status = each.value.versioning ? "Enabled" : "Suspended"
}
}
data "terraform_remote_state" "network" {
backend = "s3"
config = {
bucket = "tfstate-prod"
key = "platform/network.tfstate"
region = "eu-west-1"
}
}
resource "aws_security_group" "app" {
vpc_id = data.terraform_remote_state.network.outputs.vpc_id
}

Or look up via data source (no remote state coupling):

data "aws_vpc" "default" { tags = { Name = "main" } }

Refactor without destroy (TF 1.1+ moved blocks)

Section titled “Refactor without destroy (TF 1.1+ moved blocks)”
moved {
from = aws_instance.old_name
to = aws_instance.new_name
}

For modules:

moved {
from = aws_s3_bucket.b
to = module.storage.aws_s3_bucket.b
}

Import existing resources (TF 1.5+ block syntax)

Section titled “Import existing resources (TF 1.5+ block syntax)”
import {
to = aws_s3_bucket.logs
id = "company-logs-prod"
}
resource "aws_s3_bucket" "logs" {
bucket = "company-logs-prod"
}

Then terraform plan -generate-config-out=generated.tf to scaffold config.

variable "db_password" { type = string sensitive = true }
output "db_endpoint" {
value = aws_db_instance.x.endpoint
sensitive = false
}
.pre-commit-config.yaml
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.96.1
hooks:
- id: terraform_fmt
- id: terraform_validate
- id: terraform_tflint
- id: terraform_trivy
- id: terraform_docs
name: terraform
on: [pull_request, push]
jobs:
tf:
runs-on: ubuntu-latest
permissions: { id-token: write, contents: read, pull-requests: write }
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::ACCT:role/tf-ci
aws-region: eu-west-1
- uses: hashicorp/setup-terraform@v3
- run: terraform fmt -check -recursive
- run: terraform init
- run: terraform validate
- run: terraform plan -no-color -out=tfplan
if: github.event_name == 'pull_request'
- run: terraform apply -auto-approve tfplan
if: github.ref == 'refs/heads/main'

OIDC-based AWS auth → no long-lived secrets.

Terminal window
# limit plan/apply to a target
terraform plan -target=module.api
# refresh state only
terraform apply -refresh-only
# generate dependency graph
terraform graph | dot -Tsvg > graph.svg
# inspect provider plugins
terraform providers
# break monolithic state — split
terraform state mv aws_s3_bucket.x other-state.tfstate
  • Terragrunt — DRY config, dependency between stacks.
  • Atlantis — PR-driven Terraform automation.
  • Terraform Cloud / Spacelift / env0 — managed runs.
  • tflint — lint rules.
  • trivy / checkov / tfsec — security scan.
  • infracost — show cost diff per PR.
  • terraform-docs — auto-generate README from variables/outputs.
  • Editing .tfstate by hand. Almost always wrong.
  • apply against prod from a dev machine. Use CI.
  • terraform destroy on shared infra. Add prevent_destroy.
  • Long iteration loop — make modules small.
  • Storing secrets in terraform.tfvars and committing.
  • count = length(...) where the list isn’t known at plan time → “value depends on resource” cycle.