Project challenges / verified progress
Beacon: build the ground it stands on

The engineering notebook

Compose without hiding

How do modules make Terraform reusable without becoming a pile of knobs?

Loading statusStage 4 of 9

  • Workspace not ready
  • Agent not ready
Focus25:00
A small focus ritual

0 focus sessions completed. Every fourth session offers a longer break. Start each phase when you are ready.

Study time never unlocks verified lesson progress.

Loading...

Loading verified progress...

Loading GitHub account...
Phase 2 — Shape reusable infrastructure

Step 01 of 06

Learn the concept

A module is not a folder you made because main.tf got long. It is an interface. Good modules make a decision once and expose the few choices callers should really have; bad modules forward thirty variables and call that abstraction.

ONE MODULE, THREE SURFACESChild moduleone bounded decisionInputstyped choices from callervalidateResourcesprivate implementationhiddenOutputsfacts caller may usestable
The caller should not need to know every resource inside the child module. If it does, the module is not an abstraction; it is a longer path to the same complexity.
Step 01

The ideas this is made of

Variables are an API, not a form

Every variable you expose becomes something callers can set incorrectly and something you must support later. Types catch shape errors. Validation catches domain errors such as an empty environment name or an invalid CIDR. Defaults should encode policy only when the module truly owns that policy. Otherwise the default becomes a surprise production decision hiding in a reusable package.

Locals keep naming close to meaning

Locals are named expressions. They do not make values configurable; they make repeated or derived values readable. A local such as name_prefix = "beacon-${var.environment}" keeps resource names consistent without asking every caller to pass the same prefix. Use locals when a value is a consequence of inputs, not when it is a choice the caller owns.

Outputs are the narrow return path

A module output should expose the facts other code needs: a VPC ID, subnet IDs, a cluster endpoint, a security group ID. It should not expose every internal attribute because that freezes implementation details. The best output names describe intent rather than provider trivia. private_subnet_ids ages better than aws_subnet_a_id.

Module sources need versions

Local module paths are fine inside one repository. Shared modules should be pinned to a registry version or a Git tag. A branch source such as main makes every plan depend on whatever changed upstream since yesterday. Infrastructure review needs stable inputs. Version bumps should be explicit diffs, not surprise dependency updates during a Friday apply.

Validate a tiny naming module
variable "environment" {
  type        = string
  description = "Short environment name used in resource names."

  validation {
    condition     = can(regex("^[a-z][a-z0-9-]{1,14}$", var.environment))
    error_message = "Use 2-15 lowercase letters, digits or hyphens, starting with a letter."
  }
}

locals {
  name_prefix = "beacon-${var.environment}"
}

output "name_prefix" {
  value = local.name_prefix
}

The module is almost comically small, which makes the API visible. The caller chooses an environment; the module owns how that becomes a prefix.

Good module interface or bad wrapper

SignalGood moduleBad wrapper

Variables

Few, typed, validated

Every provider argument

Outputs

Intent-shaped facts

Internal resource dump

Policy

Encoded once

Caller must remember

Versioning

Pinned release

Floating branch

What these are called on the job

  • Root module — The Terraform configuration in the working directory where commands are run.

  • Child module — A module called by another module using a module block.

  • Output — A named value returned by a module for humans or other modules to consume.

  • Validation block — A rule attached to an input variable that rejects invalid values before planning proceeds.