Skip to main content

Module Reuse vs Inline

What This Concept Is

A Terraform module is a folder of .tf files with inputs (variables), outputs, and a clear boundary of responsibility. The question this concept answers is: for a capstone maintained by one person, when do you extract a module vs keep things inline in the root?

Three patterns, in order of increasing indirection:

  • Inline: resources sit directly in the root main.tf. Short, easy to grep, zero indirection. Every resource is immediately visible in terraform plan output.
  • Local module: a ./modules/<name> folder inside the same repo, called from the root. Good for splitting network, db, and api responsibilities along blast-radius boundaries.
  • Published/remote module (Terraform Registry, github.com/org/repo source): imported by URL with a version pin. Avoid for capstone unless you are certain you need cross-repo reuse or you are adopting a community module that encodes non-trivial configuration.

The rule for a solo project: do not abstract the second time; abstract the third time, and only if it hurts. Abstraction in IaC has the same cost curve as abstraction in code -- premature extraction produces indirection without payoff and makes every future change more expensive.

Why It Matters Here (In the Capstone)

Premature abstraction in a solo-operated capstone is a real failure mode. You write a "reusable" module, use it once, and pay every week with indirection whenever something breaks. Meanwhile no one else will call your module, because no one else works on your repo. The module exists to serve an audience that does not exist.

But some split is worth the cost -- the network + db + api split -- because it reflects the natural blast-radius boundaries of a three-tier system and makes code review of terraform plan output much easier. "15 lines of diff in modules/api/ plus a db_url output change" is easier to reason about than "170 lines of diff in root main.tf."

The question is where the threshold sits for your project, and the answer is lower than the Terraform blog posts suggest for a team, higher than they suggest for a solo capstone.

Concrete Example(s)

A capstone repo with three local modules, each corresponding to a blast-radius boundary:

terraform/
main.tf <-- root: providers, backend, module calls
variables.tf
outputs.tf
backend.tf
modules/
network/ <-- VPC, subnets, firewall
main.tf
variables.tf
outputs.tf
db/ <-- Cloud SQL instance, user, backups
main.tf
variables.tf
outputs.tf
api/ <-- Cloud Run service, IAM bindings
main.tf
variables.tf
outputs.tf

A typical root call site:

module "network" {
source = "./modules/network"
project = var.project_id
region = var.region
}

module "db" {
source = "./modules/db"
network_id = module.network.id
tier = var.db_tier
deletion_protection = var.environment == "prod"
}

module "api" {
source = "./modules/api"
image = var.api_image
db_url = module.db.conn_url
min_instances = var.environment == "prod" ? 1 : 0
}

And a counter-example, worth not doing:

modules/
cloud-run-service/ # wraps one resource with 14 variables

Wrapping a single cloud resource in a module that mirrors its schema one-to-one is overhead without benefit. Just use the resource directly in the api module.

A published-module case that does earn its complexity -- a registry module with version pin:

module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 20.0"
cluster_name = "capstone"
cluster_version = "1.30"
# ... 5-10 inputs that replace ~400 lines of raw EKS boilerplate
}

If your capstone is on EKS, the community module is almost always the right call. If it is on Cloud Run or Fly, there is no comparable boilerplate saved.

Common Confusion / Misconceptions

  • "Real engineers always extract modules." Real engineers extract modules when the cost of indirection is less than the cost of duplication. On a team of ten across five services, that threshold is low. On a capstone of one across one service, that threshold is high. Matching the pattern to team size is the real skill.
  • "A module is needed to parameterize per env." No. Variables and *.tfvars files parameterize per env from the root. Modules are for encapsulating logical groupings, not for env switching.
  • "I'll pin to main of my own module repo." Pulling main of anything -- your own module repo included -- means every terraform init might bring new code. Pin to a tag or commit SHA, always.
  • "Published modules solve security for me." They encapsulate someone else's opinions. Read the source once before using; review the changelog when you bump versions; run terraform plan carefully after upgrade.

How To Use It (In Your Capstone)

  1. Start with inline. Write all resources directly in the root; commit and apply.
  2. When the root exceeds ~150 lines, split by blast-radius boundary (network, data, compute). Not by resource type, not by file extension.
  3. If you copy a block of resources a third time, then extract a local module. Two copies is a coincidence; three is a pattern.
  4. Do not reach for published modules on the registry unless they cover something hard (EKS cluster with addons, GKE Autopilot, AWS landing zone).
  5. For any published module, pin to a specific version and read the source once before using.
  6. Keep module inputs narrow: if a module has >10 variables it is doing too much; split it.
  7. Write each module's one-line purpose at the top of its main.tf -- "this module exists because ___."

When Published Modules Do Earn Their Keep

A registry or community module earns its complexity when:

  • it encodes non-trivial configuration (EKS cluster with add-ons, GKE Autopilot, AWS landing zone, multi-AZ RDS with proxy)
  • the alternative is hundreds of lines of boilerplate you must own
  • the module is widely used and has active maintainers (recent releases, open PRs getting reviewed)

Even then, pin to a specific version and read the source once before using. Do not pull main of a random repo as a module source; the main branch can change without warning and a terraform init on a fresh machine will bring new code.

See also (integrative)

Check Yourself

  1. How many modules does your root call, and what is each one's one-line purpose?
  2. Which of those modules has only one caller? Could it be inlined without losing clarity?
  3. Which resources in the root would you move first if the file doubled in size? Which boundary would you use?
  4. If you used a published module, what version is it pinned to and when did you last review its changelog?
  5. How many variables does each module expose? Any over 10? Any modules that could be split?
  6. What is the one-line purpose of your deepest level of indirection, and is it carrying its weight?

Mini Drill or Application (Capstone-scoped)

  1. Purpose audit. For each module in your capstone Terraform, write the sentence "this module exists because ___ ." If the answer is "because I thought I should split things up," inline it this sprint.
  2. plan size check. Run terraform plan -no-color | wc -l on your root before and after a trivial change. If the output is over ~200 lines, you are at the natural threshold to extract a module; if under ~50, keep inline.
  3. Refactor rehearsal. Take one module from inline to local. Use moved { } blocks so no resources are destroyed/recreated; verify with plan. Commit the refactor as its own PR.

Source Backbone

Capstone deployment applies cloud, delivery, and operations material. These books are the source backbone for the delivery decisions.