Terraform Associate 003 — Modules and Configuration (Objectives 5 & 8)
The traps cluster where two features look interchangeable and are not. Learn to see the seams before the question forces you to.
§ IFrame
Wednesday's drill built the machine: state in a locked remote backend, changed only through init, plan, apply. Today's drill covers the two objectives that decide whether you can write the code that machine executes. Objective 5, interact with Terraform modules. Objective 8, read, generate, and modify configuration. On the exam they are separate blueprint lines. In practice they are one skill tested from two directions, because a module is configuration behaving as a unit, and configuration is what fills a module's body.
Today's Ops and Dev lessons teach this material as engineering: the contract from outside, the expression language inside. This lesson re-cuts the same ground the way the exam asks it, and the exam asks it with a distinctive taste for traps. The traps cluster where two features look interchangeable and are not. Where version pinning works for one source type and silently fails for another. Where a flag hides a value in one place while it sits readable in a second. Most of this drill is learning to see those seams before the question forces you to.
§ IIObjective 5 — Interacting with Modules
The source decides everything else. A module block binds through its source argument, and the exam expects you to know the three families cold. A local path starts with ./ or ../ and needs no download step. A registry source is the namespace/name/provider triple, like terraform-aws-modules/vpc/aws, resolved against the public registry or a private one. A Git source uses the git:: prefix with an optional //subdir and an optional ?ref= query.
Now the trap the exam loves most. The version argument works only with registry sources. Write version = "~> 5.0" under a registry source and Terraform resolves the newest release matching the constraint. Write it under a Git source and you get an error; Git modules pin through ?ref=v5.0.0 in the source string itself. Two pinning mechanisms, chosen by source family, testing the same discipline today's Ops lesson named the versioned release. The ~> pessimistic constraint is worth reading precisely: ~> 5.0 allows 5.1 and refuses 6.0, holding you inside a major version's promise.
Modules must be fetched before use. terraform init downloads modules into .terraform/modules alongside its provider work; terraform get does only the module fetch. Add a new module block and run plan without re-initializing, and Terraform stops you. The exam phrases this as a "what must run first" question, the same shape Wednesday's drill practiced for backend changes.
Data crosses the module boundary in exactly two doors. Inputs go in as arguments in the module block. Results come out as module.<name>.<output>, and only what the module author exported as an output exists at all. There is no expression that reaches into a module and touches module.network.aws_subnet.this directly. The exam tests this as syntax; today's Ops lesson gave you the reason it is true by design. The contract is the whole interface.
§ IIIObjective 8 — Read, Generate, and Modify Configuration
Objective 8 is broad, and the exam samples it through variables, outputs, locals, functions, and the constructs today's Dev lesson worked. The reliable core:
Variables carry types, defaults, and rules. A variable block takes type, default, description, validation, and sensitive. A variable with no default must be supplied a value or Terraform prompts for it. Types span string, number, bool, the collections list, set, map, and the structural object and tuple. Typed variables fail bad callers at plan time, which the exam frames as "where is this error caught."
Value precedence is a fixed ladder. When one variable receives values from several places, the last write wins, and the ladder runs: environment variables in the form TF_VAR_name at the bottom, then terraform.tfvars, then *.auto.tfvars in filename order, then -var and -var-file command-line flags at the top. The exam gives you two or three sources and asks which value applies. Memorize the ladder once; read the question's sources against it.
Locals name computation; outputs export it. A local is a named expression private to its module, referenced as local.name. An output is public: visible in terraform output, readable across the module boundary, and the connective tissue of the composition discipline from the Ops lesson.
Functions are built-in only. HCL ships functions like merge, lookup, cidrsubnet, alltrue, one, format, and there is no way to define your own. That sentence is a recurring exam question, stated with different bait each time. When logic outgrows built-ins composed in locals, the escape hatches are modules and, at the far end, providers. terraform console opens an interactive session for testing any expression against the current state and configuration, and the exam names it as the sanctioned way to experiment.
The generation constructs. count and for_each are meta-arguments that multiply resources; dynamic blocks generate nested configuration; for expressions reshape collections. Today's Dev lesson worked all four with running code, including the count-index rename hazard. The exam compresses that hazard into a one-liner: count addresses by position, for_each by key, and questions about "what happens when an element is removed from the middle" are testing whether you know position shifts and keys hold.
§ IVThe Five Traps, Counted
Exam preparation is trap census. Five recur on this ground.
versionon a non-registry source. Registry modules take the argument; Git modules pin with?ref=; local paths pin nothing because they are the working tree itself.- User-defined functions. They do not exist. Compose built-ins in locals, or push the logic into a module.
- The precedence ladder.
-varbeats*.auto.tfvarsbeatsterraform.tfvarsbeatsTF_VAR_environment variables. Last write wins, top of the ladder last. - Reaching into a module. Only outputs cross the boundary outward. A question offering
module.x.resource.yas an answer is offering a thing that does not parse. - What
sensitiveprotects. Redaction at the CLI, and nothing more. The value still sits in plain text in the state file; protecting state is the backend's job.
§ VWorked Scenario
Build the smallest root configuration that exercises both objectives at once:
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.0"
name = var.env
cidr = var.vpc_cidr
}
module "edge" {
source = "git::https://github.com/hedronite/tf-modules.git//edge?ref=v0.3.1"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.public_subnets
}
output "edge_dns" {
value = module.edge.lb_dns_name
}
Walk it as the exam would. The first module pins through version because it is a registry source. The second pins through ?ref= because it is Git. The edge module consumes the vpc module's outputs, which is composition through the two doors and nothing else. The root's own output re-exports a value from edge, which is how a deeper value becomes visible at the top at all. Run terraform init after adding either block or nothing plans. Supply var.env from a tfvars file and override it once with -var="env=staging", and the flag wins because it stands at the top of the ladder.
§ VIConnection to Today's Ops and Dev Lessons
The trio converges tightest today. The Ops lesson's three disciplines are the exam's Objective 5 in production form: contract, composition, versioned release. The Dev lesson's four constructs are Objective 8's hardest quarter: count, for_each, for, dynamic, plus the validation expressions that make typed variables enforce themselves. What the exam adds is the trap framing, and the traps are worth respecting beyond exam day, because every one of them encodes an incident somebody had. The engineer who knows why sensitive does not protect state has already connected this drill back to Wednesday's backend lesson, and that connection is the actual competence the credential claims to verify.
§ VIIPractice Questions
A teammate adds version = "1.4.0" to a module block whose source is git::https://github.com/acme/modules.git//net. What happens, and what is the correct way to pin this module?
Terraform errors: the version argument is only valid for registry sources. Git modules pin in the source string itself, ?ref=v1.4.0, referencing a tag, branch, or commit.
A module creates three subnets internally. Your root configuration needs their IDs. Name the only mechanism that makes them available and the syntax that reads them.
The module must declare output blocks for the subnet IDs; the root reads module.<name>.<output_name>. Nothing inside a module is reachable except through its outputs.
TF_VAR_region=us-east-1 is exported in the shell; terraform.tfvars sets region = "us-west-2"; the engineer runs terraform apply -var="region=eu-west-1". Which value applies and why?
eu-west-1. Precedence runs environment variables, then terraform.tfvars, then *.auto.tfvars, then -var/-var-file flags, and the last write wins, so the command-line flag beats both earlier sources.
An exam option proposes writing a custom HCL function to normalize tags across resources. Why is the option wrong, and which two constructs answer the need within the language?
HCL has no user-defined functions; only built-ins exist. Normalize tags by composing built-ins like merge inside a local, and reuse the logic across configurations by wrapping it in a module.
A variable marked sensitive = true feeds a database password into a module. Name what the flag protects, name what it does not, and name the mechanism that actually protects the second thing.
The flag redacts the value from plan and apply CLI output. It does not encrypt the value in the state file, where it sits in plain text. State protection comes from the remote backend: encryption at rest, access control, and locking.
§ VIIIClosing
Objectives 5 and 8 reward the candidate who has stopped memorizing syntax and started seeing boundaries. A module has two doors, and everything else is wall. A version constraint is a promise read by a resolver, and each source family signs that promise differently. A variable's value is decided by a ladder you can recite in your sleep, and a sensitive flag guards exactly one surface of three. Sit with the five traps until each one reads as an incident report rather than a quiz answer. The exam is asking whether you would make the mistake; the preparation is arranging to have already understood it.
Provision the §V scenario with your own account this weekend. Then break it deliberately: move the version pin to the Git module, reference an unexported subnet, override a tfvars value from the command line. Read each error until the traps are memories instead of flashcards.
Paired lessons → Ops 01-Earth-DevOps/Synthesis-Lessons/2026-07-25-terraform-modules-... · Dev Polyglot-Dev/HCL/2026-07-25-hcl-metaprogramming-...
Filed 2026-07-25 Saturday Fajr · Cert-prep lesson · Terraform Associate 003, second sprint drill
Backward-Synergy-Reach → TA-003 state/backends/workflow (Wed 07-22) · guardrail reach-back (Fri 07-17)
Grounded in Brikman, Terraform: Up and Running 3ed · HTML shipped in-cycle per HARD DISCIPLINE · aether-accent