Terraform Fundamentals · Module 12 of 12

Capstone: reusable, tested infrastructure module

Combine typed inputs, for_each, child modules, outputs, state, validation, checks and terraform test into one provider-free infrastructure-as-code delivery exercise.

Learning objectives

By the end of this module, you should be able to:

  • Assemble a reusable module with stable instance identity.
  • Add validation and tests before apply.
  • Demonstrate plan, apply, no-change verification and controlled cleanup.

A good root module composes capabilities

The root module chooses environment-specific values and composes reusable child modules. Child modules focus on one capability with explicit input/output contracts.

Stable identity and validation reduce change risk

for_each keys express identity explicitly, validation rejects unsupported inputs, and tests catch contract regressions before a production plan.

The lifecycle closes with evidence and cleanup

A complete workflow includes formatting, validation, tests, plan review, apply, output inspection, no-change verification and destroy/retirement planning.

Worked examples

See the idea in practice.

Capstone architecture

variables -> locals -> module.service[for_each] -> terraform_data -> outputs
              |
              +-> terraform test -> plan -> apply -> no-change plan
  • No cloud account is required, so the learner can focus on Terraform semantics rather than provider credentials.
Hands-on lab

Lab — Build a reusable tested service catalogue

Goal: Create a root module that instantiates a child service module by stable keys and proves its contract with terraform test.

Before you start

  • Terraform 1.6+; no external providers or cloud credentials required.
STEP 1

Create child module

mkdir -p ~/gnu-terraform-labs/module12/modules/service ~/gnu-terraform-labs/module12/tests && cd ~/gnu-terraform-labs/module12
cat > modules/service/main.tf <<'EOF'
variable "name" {
  type = string
}

variable "tier" {
  type = string
}

resource "terraform_data" "this" {
  input = {
    name = var.name
    tier = var.tier
  }
}

output "descriptor" {
  value = terraform_data.this.output
}
EOF
STEP 2

Create root configuration

cat > main.tf <<'EOF'
variable "environment" {
  type    = string
  default = "dev"
  validation {
    condition     = contains(["dev", "test", "prod"], var.environment)
    error_message = "unsupported environment"
  }
}

variable "services" {
  type = map(string)
  default = {
    api    = "frontend"
    worker = "backend"
  }
}

module "service" {
  for_each = var.services
  source   = "./modules/service"
  name     = "${var.environment}-${each.key}"
  tier     = each.value
}

output "catalogue" {
  value = { for k, m in module.service : k => m.descriptor }
}

check "service_set" {
  assert {
    condition     = length(var.services) >= 2
    error_message = "at least two services expected"
  }
}
EOF
STEP 3

Add a contract test

cat > tests/catalogue.tftest.hcl <<'EOF'
run "catalogue_contract" {
  command = apply

  variables {
    environment = "prod"
  }

  assert {
    condition     = output.catalogue.api.name == "prod-api"
    error_message = "api naming contract failed"
  }

  assert {
    condition     = output.catalogue.worker.tier == "backend"
    error_message = "worker tier contract failed"
  }
}
EOF
STEP 4

Validate, test and apply

terraform init
terraform fmt -recursive
terraform fmt -check -recursive
terraform validate
terraform test
terraform plan -out=tfplan
terraform apply tfplan
STEP 5

Prove convergence

set +e
terraform plan -detailed-exitcode
rc=$?
set -e
test "$rc" -eq 0
terraform output catalogue

Verify

  • terraform test passes.
  • The applied catalogue contains api and worker keyed instances.
  • The final plan returns no changes.

Expected outcome

  • You have a provider-free capstone demonstrating the mechanics used by real Terraform modules and pipelines.

If it fails

  • If fmt -check fails, run terraform fmt -recursive.
  • If a test fails, read the assertion message before changing infrastructure.
Real-world connection

The next step is to swap the built-in training resource for a real provider module while preserving the same contracts, tests, remote-state and review discipline.

Avoid these traps

Common mistakes

  • Skipping no-change verification after apply.
  • Treating a green apply as proof that module contracts are correct.
  • Mixing unrelated infrastructure capabilities into one unbounded module.
Knowledge check

Can you explain it without looking back?

Open each answer only after you have tried to answer the question yourself.

1What does convergence mean in this capstone?

After apply, a fresh plan reports no further changes.

2Why use for_each in the service catalogue?

Service names are stable identity keys.

3What should replace terraform_data in a real deployment?

Provider-backed resources inside modules, while keeping the same safe workflow and interface principles.

My Learning

Record your progress for this module.

Progress is saved in this browser immediately. When you sign in to My Learning, it can also sync across devices.

Not started

Attempt the core lab and knowledge checks before marking the module complete.