Command Palette

Search for a command to run...

Hectal

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.

35 min Free (the Registry comparison is plan-only) 4 steps 2 break-it drills

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.json to 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.

Consumers pin versions; upgrades are deliberatediagram
Rendering diagram…

Part 2

Your project after this mission · 3 files change

shoplite/
  • 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. 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.0
    git 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. 2

    Consume by tag

    Swap the local paths for pinned git sources. The module code is identical, so after terraform init the plan has no changes, but now editing modules/ on disk no longer changes what dev deploys until you bump ref.

    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. 3

    See what's installed

    modules.json records every module call, its source, and where it was downloaded. When someone asks 'which version is prod on?', this and the ref in 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. 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/aws to 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.0 and ecs-service-v1.0.0 exist 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/aws and 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.

terminal
$ terraform plan
── what you'll see ──
╷
│ Error: Module source has changed
│
│ on main.tf line 2, in module "network":
│ 2: source = "git::https://github.com/you/shoplite.git//modules/network?ref=network-v1.1.0"
│
│ The source address was changed since this module was installed. Run
│ "terraform init" to install all modules required by this configuration.
╵

Break #2

Put a version constraint on a git module

Add version = "~> 1.0" to the git-sourced network module and run terraform init.

terminal
$ terraform init
── what you'll see ──
╷
│ Error: Invalid registry module source address
│
│ Cannot apply a version constraint to module "network" (at main.tf:1)
│ because it doesn't come from a module registry.
╵

Part 5

Interview questions from this mission

01

How do you version and roll out changes to a shared internal module?

02

When would you use a community module like terraform-aws-modules/vpc instead of writing your own?

03

What's the risk of consuming a module by local path from multiple environments?

0/4 · 0%