DevOps Peach

Advanced Terraform · Modules and Code Structure · Lesson 1 of 5

Designing Reusable Modules

A module is a folder of .tf files; the root module calls child modules, passes inputs as variables and reads their outputs.

Practice interactively →

Every Terraform configuration is a module. The folder where you run terraform plan is the root module; folders it calls with a module block are child modules. A good child module does one job (a network, a container service, a bucket with its policy) and hides the details behind a small interface.

flowchart LR
  R["Root module envs/dev"] --> N["module.network"]
  R --> A["module.app"]
  N -->|"output network_id"| A
  A --> D["docker_container x2"]
  V["dev.tfvars"] --> R
  %% step R: The root module is where you run plan and apply; it owns the backend and the provider settings.
  %% step V: Values for the root module come from tfvars files, -var flags or TF_VAR_ environment variables.
  %% step N: A child module receives inputs through variables and creates its own resources.
  %% step A: Outputs of one module become inputs of another, which also tells Terraform the order.
  %% step D: Resources inside a module are addressed as module.app.docker_container.web[0].

The standard layout

modules/app/
  main.tf        # resources
  variables.tf   # inputs, with type, description and defaults
  outputs.tf     # values the caller may use
  versions.tf    # required_version and required_providers
  README.md
module "app" {
  source   = "../../modules/app"
  name     = "shop-api"
  replicas = 2
  network  = module.network.network_name   # output of another module
}

output "app_urls" {
  value = module.app.urls
}

Inside the module, the caller's values are var.name, var.replicas. The caller can only read what the module exposes in outputs.tf: module.app.urls. Everything else is private.

Design rules that age well

Rule Why
One purpose per module Small modules are easier to test, version and reuse
Typed variables with descriptions Callers get a clear error instead of a broken plan
Few required inputs, sensible defaults The common case needs three lines, not thirty
No provider blocks inside reusable modules The caller configures providers; the module only declares required_providers
Output IDs and names others need Outputs are the module's public API
Avoid thin wrappers A module that only renames one resource's arguments adds indirection without value

Composition, not nesting

Prefer a flat root module that wires several child modules together over modules that call modules that call modules. Deep nesting makes addresses long, outputs must be passed up level by level, and a change to the inner module ripples everywhere.

Commands

Command Meaning
terraform init Downloads providers and installs modules into .terraform/modules
terraform get -update Re-installs modules only (after you changed a source)
terraform output -json All root outputs as JSON for scripts

Traps: a child module's outputs are not visible from outside the root unless the root re-exports them; and changing a module's source requires terraform init (or terraform get) before plan.

Practice interactively →