linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
Intro
Lessons
Footer
linuxlab-TutorialsPricingAboutPrivacy & cookies
Copyright © 2026 LinuxLab. All rights reserved.
linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
  • Introduction
  • Lessons
  • How it works
  • Knowledge base
  • Cheat sheet
  • Capstone
  • Interview prep
Cluster

← все кластеры

Modules: inputs, outputs, sources, versions

A module's boundary of responsibility, sources (local, registry, git, s3), version pinning and semver, composition vs a flat root, the usual antipatterns. What a lead wants to hear: where to draw the boundary and why.

6 вопросов · ~23 мин чтения

Questions

На этой странице

  1. 01How do you draw a module's boundary? What goes into inputs, what into outputs?
  2. 02What module sources exist, and how do you pin a version?
  3. 03Module composition vs a flat root: when do you split, and when not?
  4. 04Use a public registry module, or write your own?
  5. 05What module antipatterns have you run into?
  6. 06How do you make a breaking change in a module that 20 teams use?

#module-boundary-inputs-outputs

juniorчасто

How do you draw a module's boundary? What goes into inputs, what into outputs?

Что отвечать

A module is a black box with a contract. Inputs are whatever changes between uses (the bucket name, the region, tags). Outputs are whatever the outside needs for other resources (an ARN, an endpoint, an id). Inside the module live the locals and the implementation. The antipattern is a module with 40 inputs where half of them mirror fields of an AWS resource. Then the module does not simplify, it adds a layer. A good module hides decisions, it does not pass through every provider option one for one.

Что хотят услышать

A senior should: - name what makes a good input: it changes between calls and has no reasonable default in the module's context - say that an output should return what neighboring resources need, not "everything just in case." Each output is part of the public API, and removing one is a breaking change - separate the three levels: variable (input), local (an internal constant), output (output). A local is not an output, it does not leak out - mention that `description` is required on every variable and output, especially for shared modules. It is documentation, and colleagues read it through `terraform-docs`

Подводные камни

  • ✗ Making an input for every attribute of an AWS resource. The module turns into an alias for the resource, with nothing added
  • ✗ Giving no defaults to inputs where a reasonable default exists. Every caller writes the same thing
  • ✗ Exposing a sensitive value through an output without `sensitive = true`. You leak it into the CI logs by accident

Follow-up

  • ? How does `variable` differ from `local` conceptually?
  • ? Can you give a default to an `object()` input? What goes inside it?
  • ? When should an output be `sensitive = true`?

Глубина в базе знаний

  • Module: a reusable piece of infrastructure
  • The module contract: input variables and outputs
  • locals: computed internal names
  • The output block: what Terraform exposes to the outside
tags: modules, fundamentals

#module-sources-and-pinning

intermediateчасто

What module sources exist, and how do you pin a version?

Что отвечать

Sources: a local path (`./modules/x`), the Terraform registry (`hashicorp/vpc/aws`), git (`git::https://github.com/...`), an HTTP archive, S3, GCS. Pinning: the registry supports `version = "~> 4.0"` through semver. Git pins through `ref=v1.2.3` in the URL (a tag, a branch, a sha). `~> 4.0` means "4.x where x >= 0," and `~> 4.2.0` means "4.2.x." The antipattern is git with no `ref` or with `ref=main`: different machines get different versions, and Terraform will not even warn you.

Что хотят услышать

A senior should: - note that registry modules resolve their version through a `~>` constraint, picking the highest patch version automatically - for git, prefer `ref=<tag>` or `ref=<sha>`: a tag can be rewritten, a sha is immutable - explain why `~>` (pessimistic) is handier than `>=` (open-ended): patches arrive on their own, while a major needs a deliberate upgrade - mention `terraform get -update` or `terraform init -upgrade` to recompute the pinned versions inside a constraint - note that `terraform init` pulls modules into `.terraform/modules` and pins them there; the lockfile pins ONLY provider versions, not modules

Подводные камни

  • ✗ Using `ref=main` or a branch. It worked yesterday, today you get a surprise diff
  • ✗ Thinking `.terraform.lock.hcl` pins module versions. It does not, only providers
  • ✗ Setting `>= 4.0` with no upper bound. 5.0 arrives on its own and breaks compatibility

Follow-up

  • ? What exactly do `~> 4.2` and `~> 4.2.0` mean, and how do they differ?
  • ? Why do you need `terraform init -upgrade` after changing `version`?
  • ? How do modules differ from providers in the lockfile strategy?

Глубина в базе знаний

  • Module sources: local, git, registry, archive, S3
  • [[tf-module-versioning]]
  • Version constraints in Terraform: required_version and providers
  • .terraform.lock.hcl: pinning provider versions
tags: modules, versioning

#module-composition-vs-flat

intermediateиногда

Module composition vs a flat root: when do you split, and when not?

Что отвечать

A flat root keeps everything in one `main.tf`, or in a few files of one root module. Composition assembles the root from child modules, each doing its own part (vpc, eks, rds). A flat root works up to about 30-50 resources; past that the problems start: plan takes longer, the blast radius grows, git conflicts get more frequent. Composition splits the infrastructure into pieces, each with its own state. But composition is no cure-all by itself: modules that are too small ("a module per resource") create noise without value.

Что хотят услышать

A senior should: - name the criteria for splitting: how independent the piece is (a VPC lives without EKS), how often it changes, the blast radius - say there is no point making a module out of one or two resources; a module should encapsulate a decision, not a group of aliases - mention that each root module has its own state. Composition inside one root is just files; splitting state means separate root modules with outputs through remote_state or ssm parameters - name the root module as the "unit of deployment": whatever one person rolls out with one command should live in one root

Подводные камни

  • ✗ Making 'a module per resource' in the name of 'clean code.' Composition complexity shoots up and debugging gets worse
  • ✗ Splitting a root into dozens of small state files for no reason. The outputs between them through remote_state become a bottleneck
  • ✗ Not separating the stable layer (network, IAM) from the frequently changing one (apps). The blast radius covers the whole project

Follow-up

  • ? When does `terragrunt` help with composition?
  • ? What is wrong with 'a module per resource'?
  • ? How do you split state across environments: one root plus workspaces, or several roots?

Глубина в базе знаний

  • Module composition: a module of modules and passing providers
  • Module: a reusable piece of infrastructure
  • [[tf-stacks]]
tags: modules, composition, architecturebook: mastering.terraform.epub:ch8

#module-where-public-or-local

intermediateиногда

Use a public registry module, or write your own?

Что отвечать

It depends. A ready-made module (`terraform-aws-modules/vpc/aws`) saves weeks, covers edge cases you would not foresee, and has community support. The downside: 100+ inputs "for every occasion," hard to read, and your use case usually wants a subset. Your own module fits your team better and is documented for your conventions, but writing and maintaining it is on you. The middle ground is to wrap someone else's module in a thin facade of your own with a trimmed-down API.

Что хотят услышать

A senior should: - note that ready-made modules are good at the start: VPC, RDS, EKS are already shaken out by thousands of users - cover the wrapper approach: your module as a facade over the public one, where you fix only the inputs you need and add your tagging convention - warn that any dependency on an external module means reading the changelog on upgrade. There are breaking changes between majors - mention that for critical infrastructure (security boundaries) your own module is better, since someone else's can quietly change defaults between versions

Подводные камни

  • ✗ Using a community module with `version = "~> 1.0"` and forgetting it. A year later 1.20 arrives with new defaults
  • ✗ Forking a public module and forgetting to sync. In a couple of years your fork is legacy with no security patches
  • ✗ Writing your own VPC module from scratch 'because the other one is bulky.' You spend weeks reproducing what already exists

Follow-up

  • ? Which is better: using `terraform-aws-modules/vpc/aws` directly or wrapping it in your own?
  • ? How would you pin the version of a community module in production?
  • ? When is forking someone else's module justified?

Глубина в базе знаний

  • Module sources: local, git, registry, archive, S3
  • Module: a reusable piece of infrastructure
  • [[tf-when-not-to-use]]
tags: modules, decisions

#module-antipatterns

seniorредко

What module antipatterns have you run into?

Что отвечать

The most common ones: a module with count=0/1 for conditional creation (now you use `for_each` or a toggle through an input flag); a module that wraps a single resource without adding value; modules with Terraform logic in a tangled `for_each`-of-`for_each` structure; "universal" modules with 80 inputs where half are never used; resource names hardcoded instead of taken from a variable.

Что хотят услышать

A senior should: - point at the "wrapper module with no value": if inside there is one resource and five inputs mapping one for one to its attributes, the module does not simplify and can go - name "module bloat": 80+ inputs means the module is solving too many problems, so split it - cover count for conditional creation as the legacy approach. for_each with `for_each = var.enabled ? toset(["x"]) : toset([])` reads better - mention that you must not commit the `.terraform/` directory along with the module, since it holds provider binaries built for the local OS

Подводные камни

  • ✗ Writing `count = var.create ? 1 : 0`. You get a resource at address `module.x.aws_s3_bucket.demo[0]`, which breaks refactoring
  • ✗ Using `null_resource` inside a module as a hack for a side effect. It behaves oddly and idempotency erodes
  • ✗ Hardcoding tags = { Project = "my-app" } inside a module. It is not reusable; tags should be a variable with a merge

Follow-up

  • ? How do you make an optional resource inside a module without `count = 0/1`?
  • ? Why is `null_resource` more often an antipattern than a solution?
  • ? When does a module become 'too large' in terms of inputs?

Глубина в базе знаний

  • Module: a reusable piece of infrastructure
  • count and for_each: many resources from one block
  • [[tf-when-not-to-use]]
tags: modules, antipatternsbook: Packt.Terraform.Cookbook.pdf:ch5

#module-versioning-breaking-changes

seniorиногда

How do you make a breaking change in a module that 20 teams use?

Что отвечать

The main rule: a breaking change is a major version, no exceptions. Tag `v2.0.0`, put a migration guide in the README, write what broke in the CHANGELOG. Teams upgrade at their own pace. If you can make the change backward compatible through a new optional input, do that and mark the old one as deprecated in its `description`. Inside the module, use the `moved {}` block (since 1.1+) to move resources in state. It lets you rename a resource or pull it into a submodule without destroy and create.

Что хотят услышать

A senior should: - name semver as the contract: patch = bugfix, minor = new behavior with the old one as the default, major = breaking - cover the `moved {}` block as a painless way to restructure the module's internals: the user's state moves automatically on their next apply - mention deprecation through `description = "DEPRECATED: use bar instead"`, not a strict mechanism but visible in terraform-docs - note that shared modules need CI on the module itself: terraform test, a backward-compatibility check through scenarios with different input combinations

Подводные камни

  • ✗ Renaming a resource inside a module without a `moved {}` block. Every consumer gets destroy and create on their next apply
  • ✗ Shipping a minor with a breaking change. 20 teams wake up to broken CI
  • ✗ Naming a tag without the `v` prefix (`1.2.3` instead of `v1.2.3`). Terraform will not read the version constraint correctly

Follow-up

  • ? How does the `moved {}` block help with module refactoring without destroy?
  • ? How does the pessimistic constraint `~> 2.0` help consumers during a breaking change?
  • ? What do you put in the CHANGELOG for a module's major release?

Глубина в базе знаний

  • [[tf-module-versioning]]
  • The moved block: rename without destroy
  • Refactoring patterns: count to for_each, split files, extract module
  • Native test framework: .tftest.hcl, run, and assert
tags: modules, versioning, breaking-changesbook: mastering.terraform.epub:ch9
Footer
linuxlab-
Copyright © 2026 LinuxLab. All rights reserved.
Tutorials
Pricing
About
Privacy & cookies