# Modules

## 1. What a Module Is

Every directory of `.tf` files is a module. The directory you run Terraform in is the **root module**; it can call **child modules** to reuse groups of resources:

```hcl
module "vpc" {
  source = "./modules/vpc"            # local path
  name   = local.name_prefix
  cidr   = var.vpc_cidr
  azs    = local.azs
}

module "eks" {
  source  = "terraform-aws-modules/eks/aws"   # public registry
  version = "~> 20.0"                         # ALWAYS pin registry modules
  cluster_name = "${local.name_prefix}-eks"
  vpc_id       = module.vpc.vpc_id             # use the child module's outputs
  subnet_ids   = module.vpc.private_subnet_ids
}
```

A module's **interface** is its variables (inputs) and outputs; its resources are an implementation detail.

## 2. Module Sources

| Source | Example | Notes |
|---|---|---|
| local path | `./modules/vpc` | same repo, versioned with the caller |
| Terraform Registry | `terraform-aws-modules/vpc/aws` + `version` | semantic versions |
| Git | `git::https://github.com/acme/tf-modules.git//vpc?ref=v1.4.0` | pin a tag or commit |
| private registry | `app.terraform.io/acme/vpc/aws` | HCP Terraform / Artifactory |
| S3 / GCS / HTTP archives | `s3::https://…/vpc.zip` | less common |

Run `terraform init` (or `init -upgrade`) after changing sources/versions.

## 3. Designing a Good Module

```
modules/service/
  main.tf         # resources
  variables.tf    # inputs with types, descriptions, validation, sane defaults
  outputs.tf      # what callers need (ids, arns, urls)
  versions.tf     # required_providers (no provider block inside reusable modules!)
  README.md       # usage example (terraform-docs can generate inputs/outputs tables)
  examples/basic/ # a runnable example, also used by tests
```

Guidelines:

- **One clear responsibility** at a useful level of abstraction ("a service with its ALB target group, ECS service, log group and alarms"), not a thin wrapper around a single resource.
- **Minimal, typed inputs** with secure defaults (encryption on, public access off); escape hatches (`extra_tags`, `additional_policies`) instead of exposing every argument.
- **Don't configure providers** in reusable modules — the caller passes them (use `configuration_aliases` when a module needs multiple regions/accounts).
- **Outputs for everything a caller might wire up**.
- **Composition over nesting**: root modules compose several flat modules; deep module trees are hard to debug.

## 4. Repeating Modules

```hcl
module "service" {
  for_each = var.services                 # one module instance per service
  source   = "./modules/service"
  name     = each.key
  image    = each.value.image
  cluster  = aws_ecs_cluster.main.id
  vpc      = module.vpc
}
# addresses: module.service["api"].aws_ecs_service.this
```

## 5. Versioning and Releasing Modules

- Shared modules live in their own repo (or a monorepo folder) with **semantic version tags**; consumers pin versions and upgrade deliberately.
- Breaking changes (renamed inputs, resources that would be replaced) → major version; include `moved` blocks in the module so callers upgrading don't destroy resources.
- Test modules before release (`09-testing_and_policy_as_code/`).

## Under the Hood: Modules Are Namespaces, Not Boundaries

When Terraform loads a configuration, it **flattens** all modules into one graph: `module.vpc.aws_subnet.private[0]` is just a resource with a longer address. Module inputs are evaluated in the caller's scope and become variables inside the module; outputs become references the caller can use. Dependencies cross module boundaries at the level of **individual values**, so a resource in module B can start as soon as the specific output of module A it uses is known — not after all of module A. A module adds no runtime isolation: all its resources share the root module's state file, providers and apply. Real isolation (blast radius, separate locks) comes only from separate **root modules / states** (`03-state_and_remote_backends/` Part 2).

![A root module calls child modules for the VPC, database and one service instance per entry with for_each, wiring outputs to inputs; Terraform flattens them into a single graph and state, while shared modules are versioned and pinned](diagrams/01-module-composition.svg)

## Common Pitfalls

1. **Unpinned registry/Git modules** — surprise changes on `init -upgrade`.
2. **Provider blocks inside reusable modules**.
3. **God modules** with 80 inputs mirroring every resource argument.
4. **Wrapper modules around one resource** with no added value.
5. **Breaking changes without `moved` blocks** — callers' resources get replaced.
6. **Deeply nested modules** — hard to read plans and debug.

## Quick Reference

```
module "x" { source = "…", version = "~> 1.2", inputs… } → module.x.output_name
sources: local · registry (+version) · git::…?ref=tag · private registry
module layout: main.tf, variables.tf, outputs.tf, versions.tf, README, examples/
no provider blocks in reusable modules · secure defaults · for_each over modules
semver releases + moved blocks for refactors
