Mission 5.3 · Stage 5 — Modules
Module Sources, Versions, and the Public Registry
Goal: ShopLite's modules are consumed by pinned git tag instead of a moving local path, and you can evaluate a community Registry module against your own before choosing.
By the end of this mission
- Use local, git, and Registry module sources correctly
- Version your own modules with tags and upgrade consumers deliberately
- Read
.terraform/modules/modules.jsonto see what's installed - Weigh community modules against writing your own
Part 1
Understand it first
Three kinds of source
LOCAL PATH (../modules/network): always the current files on disk. Great while developing a module alongside its only consumer; dangerous once several environments or teams use it, because a change to the module hits every consumer on their next plan, whether they wanted it or not.
GIT (git::https://github.com/acme/shoplite.git//modules/network?ref=network-v1.0.0): a specific tag or commit of a repository. // separates the repo from the subdirectory; ?ref= pins the version. REGISTRY (terraform-aws-modules/vpc/aws with version = "~> 6.0"): modules published to the public or a private Registry, with proper version constraints.
Modules are software: version them
Once prod consumes a module, a module change is a release. Tag it (network-v1.1.0) following semantic versioning: patch for fixes, minor for backward-compatible additions (a new optional variable), major for breaking changes (a renamed variable, a resource that will be replaced). Then each environment upgrades by changing its ref, runs terraform init, and reviews the plan. Dev can run v1.1.0 while prod stays on v1.0.0 until the upgrade has been proven.
Community modules: buy vs build
terraform-aws-modules/* (VPC, EKS, RDS, ...) are widely used, well maintained, and cover edge cases you haven't hit yet. The costs: large interfaces with dozens of variables to understand, upgrades that occasionally rename resources, and a dependency on someone else's decisions.
A reasonable rule: use well-known community modules for undifferentiated, complex infrastructure (VPCs, EKS clusters) when your team doesn't want to own that complexity; write your own for things specific to how your company runs services (like ecs-service). Either way, pin the version.
Part 2
Your project after this mission · 3 files change
- app/
- worker.js
- infra/
- alb.tf
- backend.tf
- checks.tf
- database.tf
- ecr.tf
- ecs.tfmodified
- iam.tf
- locals.tf
- logs.tf
- main.tfmodified
- outputs.tf
- probe.tf
- providers.tf
- refactors.tf
- security.tf
- storage.tf
- terraform.tfvars
- uploads.tf
- variables.tf
- versions.tf
- modules/
- ecs-service/
- main.tf
- outputs.tf
- variables.tf
- versions.tf
- network/
- main.tf
- outputs.tf
- variables.tf
- versions.tf
- scratch/
- registry-vpc/
- main.tfnew
Part 3
Build it, step by step
- 1
Tag the current modules
In a monorepo, prefix tags with the module name so each module versions independently. Push the tags; git sources can only reference what's on the remote.
terminal$ git tag network-v1.0.0 && git tag ecs-service-v1.0.0git push origin network-v1.0.0 ecs-service-v1.0.0── expected output ──* [new tag] network-v1.0.0 -> network-v1.0.0* [new tag] ecs-service-v1.0.0 -> ecs-service-v1.0.0 - 2
Consume by tag
Swap the local paths for pinned git sources. The module code is identical, so after
terraform initthe plan has no changes, but now editingmodules/on disk no longer changes what dev deploys until you bumpref.infra/main.tfadd to filehcl Same for module.api and module.worker with ecs-service-v1.0.0.
module "network" { source = "git::https://github.com/you/shoplite.git//modules/network?ref=network-v1.0.0" # ...inputs unchanged }terminal$ terraform init && terraform plan── expected output ──Initializing modules...Downloading git::https://github.com/you/shoplite.git?ref=network-v1.0.0 for network...- network in .terraform/modules/network/modules/network...No changes. Your infrastructure matches the configuration. - 3
See what's installed
modules.jsonrecords every module call, its source, and where it was downloaded. When someone asks 'which version is prod on?', this and therefin code answer it.terminal$ jq '.Modules[] | {Key, Source}' .terraform/modules/modules.json── expected output ──{ "Key": "", "Source": "" }{ "Key": "api", "Source": "git::https://github.com/you/shoplite.git//modules/ecs-service?ref=ecs-service-v1.0.0" }{ "Key": "network", "Source": "git::https://github.com/you/shoplite.git//modules/network?ref=network-v1.0.0" }{ "Key": "worker", "Source": "git::https://github.com/you/shoplite.git//modules/ecs-service?ref=ecs-service-v1.0.0" } - 4
Evaluate the community VPC module (plan only)
In a scratch folder with a LOCAL backend (never touching ShopLite's state), configure
terraform-aws-modules/vpc/awsto match your design and plan. Compare the resource count and read its variables. It does more than yours (flow logs, IPv6, NACLs, and many more options), and that's both the appeal and the cost.scratch/registry-vpc/main.tfwhole filehcl provider "aws" { region = "ap-south-1" } module "vpc" { source = "terraform-aws-modules/vpc/aws" version = "~> 6.0" name = "registry-compare" cidr = "10.99.0.0/16" azs = ["ap-south-1a", "ap-south-1b"] public_subnets = ["10.99.0.0/24", "10.99.1.0/24"] private_subnets = ["10.99.10.0/24", "10.99.11.0/24"] database_subnets = ["10.99.20.0/24", "10.99.21.0/24"] enable_nat_gateway = true single_nat_gateway = true }terminal$ cd ../scratch/registry-vpc && terraform init && terraform plan | tail -n 1── expected output ──Downloading registry.terraform.io/terraform-aws-modules/vpc/aws 6.x.x for vpc......Plan: 23 to add, 0 to change, 0 to destroy.Your count may differ by module version. Don't apply; delete the scratch folder when done.
Checkpoint — you should now have
- ✓Tags
network-v1.0.0andecs-service-v1.0.0exist on the remote. - ✓All three module calls use pinned git sources, and the plan shows no changes.
- ✓You've compared your network module with
terraform-aws-modules/vpc/awsand can argue for either.
Part 4
Break it on purpose
Make each change, run the command, and read the error before revealing the diagnosis. Recognising these messages on sight is what makes you fast on a real team. Undo the change afterwards.
Break #1
Change a module's ref without init
Bump the network module to ?ref=network-v1.1.0 (create the tag first, or just pretend) and run terraform plan.
Break #2
Put a version constraint on a git module
Add version = "~> 1.0" to the git-sourced network module and run terraform init.
Part 5
Interview questions from this mission
How do you version and roll out changes to a shared internal module?
When would you use a community module like terraform-aws-modules/vpc instead of writing your own?
What's the risk of consuming a module by local path from multiple environments?