Build a Reusable Terraform VPC Module

Build a tested, scannable AWS VPC module with mocked CI gates and ADRs.

Introduction

30 Second Summary

The US grid runs on an estimated 60 to 80 million transformers, and a 2024 presidential advisory report counted over 80,000 distinct types with no canonical part identifier. A utility cannot borrow a neighbour's spare, or tell whether they have one, because the two inventory records share no key.

In this project, you will build a reusable Terraform network module that passes a mocked test matrix with zero cloud spend, clears a policy scan, and ships through a CI gate on a GitHub Actions workflow. Every design decision is recorded with the alternative it beat and the tradeoff it accepted.

What You'll Build

You can hand an architect a single repo link, they run one command, and three mocked test cases pass in seconds with a clean policy scan and a green CI badge. The project board is empty, the release is tagged, and three decision records explain every choice.

By the end of this project, you'll have:

  • A reusable AWS VPC module where you run terraform test and watch three cases pass in under a second. No cloud account, no credentials, no cost.
  • A proven CI gate that visibly rejects a bad commit (red) and then accepts the fix (green), so you can show anyone the gate catches real problems.
  • A complete delivery trail with a GitHub Project board worked branch-per-story through pull requests, three Architecture Decision Records, a PM readout framed for two audiences, and a tagged release.
  • Secret Mission: Record the measured feasibility (wall-clock from clean clone) and score your AI agent's test proposals against a sealed answer key, making rubber-stamping visible.

Are there any prerequisites?

You need Git installed, a free GitHub account, Docker Desktop installed, a code editor (VS Code or Cursor), and a flat-rate AI coding client (Claude Code, Cursor, Codex, or Gemini) or Ollama + Aider as an open-source fallback.

Docker Desktop and any Ollama model pulls should be done before you start. The 45-minute clock begins once the devcontainer is ready.

Before We Start

Before you touch any code, lock in what you are about to build and why it matters. The after-action review at the end of this project measures against a baseline, and that baseline is the commitment you make right now.

Set Up the Repo, Board, and First Failure

This project's starter repo pre-stages everything that teaches nothing. The skeleton files, pinned versions, workflow, and devcontainer are already in place so your 45 minutes go to the test matrix and the decisions.

A devcontainer normalizes the environment so Terraform, Python, and Checkov work identically on macOS, Windows, and Linux. By the end of this step, you will see a real test failure. That failure is the gap the rest of the project closes.

In this step, get ready to:
  • Clone the starter repo and confirm tools inside the devcontainer.
  • Create the GitHub Project board with one epic and five story tickets.
  • Run terraform test and observe the seeded failure.
Set up your accounts and tools

Set up each tool first. Run the check, install only if it fails.

  • Terraform 1.7 or newer, where provider mocking landed: terraform version prints 1.7 or higher. Install from HashiCorp releases or your package manager.
  • Git: git --version prints a version.
  • Editor: open the cloned repo.
  • Checkov, the policy scanner, and the GitHub Actions workflow are pre-staged in the starter repo, so nothing to install. Nothing is ever applied to a real cloud provider.
  • GitHub account: sign in at github.com, or create one free.
  • GitHub organization: create one, once ever. A repo in a personal account scatters. An org makes a portfolio.
  • The repo: create it Private inside the org.
  • The Project board: open it with the epic and its stories before writing any code.

✔️ I already have a GitHub account

You're set. Move on to the editor check below.

ⓧ I need to create one

  • Go to github.com in your browser.
  • Click Sign up in the top-right corner.
  • Follow the prompts to create your free account.
  • Verify your email address when GitHub sends the confirmation link.

You need a code editor that supports devcontainers. Pick one of these three and install it:

VS Code

  • Download VS Code from code.visualstudio.com and run the installer.
  • Open VS Code.
  • Open the Extensions panel by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
  • Search for Dev Containers and install the extension published by Microsoft.

Cursor

  • Download Cursor from cursor.com and run the installer.
  • Open Cursor.
  • Open the Extensions panel by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
  • Search for Dev Containers and install the extension published by Microsoft.

Antigravity

  • Download Antigravity from antigravity.google/download and run the installer.
  • Open Antigravity.
  • Open the Extensions panel by pressing Cmd+Shift+X (macOS) or Ctrl+Shift+X (Windows).
  • Search for Antigravity Dev Containers and install the extension published by Google.

Docker runs the devcontainer that gives you Terraform, Python, and Checkov without installing them directly. Open your terminal to check if Docker is ready.

  • Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal and press Enter to open it.
  • Check if Docker is installed by running this command:
docker --version

✔️ I see a version number

Docker is ready. Make sure Docker Desktop is running (you should see the whale icon in your menu bar or system tray). Move on to the Git check.

ⓧ Command not found

Install Docker Desktop for your platform:

After installing, open Docker Desktop and wait for it to finish starting. Then run docker --version again to confirm.

  • Check if Git is installed by running this command:
git --version

✔️ I see a version number

Git is ready. You have all the tools you need.

ⓧ Command not found

Install Git for your platform:

  • macOS: Run xcode-select --install in your terminal and follow the prompts.
  • Windows: Download from git-scm.com and run the installer. Accept the defaults.
  • Linux: Run sudo apt install git (Debian/Ubuntu) or sudo dnf install git (Fedora).

After installing, run git --version again to confirm.

Create a GitHub organization

A GitHub organization groups all your repositories under one shared account. Instead of scattering project repos across your personal profile, you will keep them organized in a dedicated org.

  • On GitHub, click your profile picture in the top-right corner, then click Settings.
  • In the Access section of the left sidebar, click Organizations.
  • Next to the "Organizations" header, click New organization.
  • Under "Choose a plan", click Choose GitHub Free.
  • Under "Organization name", enter a name for your org (for example nextwork-yourname).
  • Under "Contact email", enter your email address.
  • Click Next.
  • Skip the "Add organization members" page and click Complete setup.

You should land on your new organization's dashboard page. The org name appears in the top-left of the page.

Why create an organization?

An organization gives you a single place to find every repo from your NextWork projects. When you create repos under your org instead of your personal account, your project work stays grouped and easy to share with reviewers or future employers.

Create your repo and open the devcontainer

You will create a new repository inside your organization. This gives you a clean space to push changes and use GitHub Actions freely.

  • In your browser, go to github.com/new to create a new repository.
  • In the Owner dropdown, select your new organization (not your personal account).
  • In the Repository name field, enter network-module.
  • Set the visibility to Private.
  • Check the Add a README file checkbox.
  • Click Create repository.

GitHub redirects you to your new repo. Your repository URL is now YOUR_REPO_URL. Paste your actual URL above so you can reference it later.

  • On your new repository page, click the green Code button.
  • Copy the HTTPS URL shown.
  • Back in your terminal, move to your Desktop and clone your repo by running these commands:
cd ~/Desktop
git clone [[REPO_URL="https://github.com/your-org-name/network-module.git"]]
cd network-module

You should see Git download the repository and your terminal prompt should now show network-module as the current directory.

Clone failing?

  • If you see "repository not found", double-check the URL you copied from your repo page. It should match https://github.com/your-org-name/network-module.git with your actual organization name.
  • If git is not found, go back to the Git check in the prerequisites above and install it.
  • If you see a credentials prompt, type your GitHub username and use a personal access token (not your password) as the password.

Help me troubleshoot my git clone error

  • Open the network-module folder in your code editor (VS Code, Cursor, or Antigravity). Click File in the top menu bar.
  • Click Open Folder.
  • Select the network-module folder on your Desktop and click Open.

You should see project folders including modules/, tests/, .devcontainer/, and .github/ in the file explorer sidebar.

  • Open the Command Palette by pressing Cmd+Shift+P (macOS) or Ctrl+Shift+P (Windows).
  • Type Reopen in Container and select Dev Containers: Reopen in Container.

What does the devcontainer provide?

The .devcontainer/devcontainer.json file tells your editor to build a Linux container with Terraform 1.15.8, Python 3.12, and Checkov pre-installed. It uses a multi-arch image that runs on both Apple Silicon (arm64) and Intel (amd64).

The .gitattributes file forces LF line endings on all text files. This prevents scripts from breaking inside the Linux container when the host machine is Windows (which defaults to CRLF).

The container takes 1-2 minutes to build on first launch. Once the integrated terminal appears at the bottom of your editor, the environment is ready.

  • Confirm Terraform is installed at the pinned version by running:
terraform version

You should see Terraform v1.15.8 in the output.

  • Confirm Checkov is installed by running:
checkov --version

You should see a version starting with 3. (for example 3.3.7).

Version command not found?

  • If terraform or checkov is not found, you are likely still on your host machine (not inside the container). Check the bottom-left corner of your editor. It should show a green icon with the text Dev Container.
  • If the container build failed, open the Command Palette and run Dev Containers: Rebuild and Reopen in Container to retry.

Help me fix my devcontainer build

Create the GitHub Project board

An architecture board does not review code without a delivery trail. You will track this module build as one epic with five stories, each worked through its own branch and pull request.

  • Open your repository on GitHub in a browser.
  • Click the Issues tab.
  • Click New issue.
  • Enter Decision Records as the title and click Submit new issue.
  • Repeat for four more issues with titles: Module Logic, Test Matrix, Policy and Gate, and Readout and Close.

You should now see five open issues in the Issues tab.

  • Click your profile picture in the top-right corner of GitHub, then click Your projects.
  • Click New project.
  • Under "Start from scratch", click Table.
  • In the Project name field, type Network Module v1.0.
  • Click Create project.
  • In your new project table, click the + button at the bottom of the table to add an item.
  • Type # and search for your repository to find the five issues you just created.
  • Add all five issues to the project.

Your project board should now show five rows in table view, one per story.

Why no branch protection?

Branch protection (requiring status checks to pass before merge) is available on GitHub Pro ($4/mo) for private repos. On the free tier, the GitHub Actions workflow still runs on every push and PR. You can visually confirm pass or fail, but enforcement at the merge button is a stretch goal, not an assumption.

Run terraform test and observe the seeded failure

The test file in tests/network.tftest.hcl already contains assertions expecting computed subnet CIDRs. The module outputs are stubs that return placeholder values. This mismatch is the designed gap you will close over the next steps.

  • In the devcontainer terminal, initialize the Terraform working directory by running:
terraform init

You should see output ending with "Terraform has been successfully initialized!" along with a note about the provider being mocked for tests.

Before you run the test command, think about what will happen. The test file expects real CIDR calculations, but the module outputs are stubs. What do you predict the test result will be?

  • Run the test suite by executing:
terraform test

You will see assertion errors. The tests fail because the module outputs return placeholder strings instead of the computed CIDR values the assertions expect. This is intentional.

What does this failure prove?

The test file is working correctly. It catches the fact that module outputs do not match expected values. This is the same mechanism that will catch a bad commit later when you prove the CI/CD gate goes red on a failing change.

Your job in the next steps is to fill the module with real logic so these assertions pass. Every assertion checks a hand-computed value, not a vague "something exists" check.

  • If you see "No configuration files found", confirm you are in the network-module root directory (not inside modules/ or tests/).
  • If terraform init failed, check your internet connection. The init step downloads provider plugins.

Help me debug my terraform test error

Your environment is ready and the gap is visible. Next up, you will write the Architecture Decision Records that explain why you chose Terraform, why mocked tests, and where idempotence is honestly not proven.

This is the step that does the most work against the backstory, and it is the easiest to rush. A transformer with no part identifier is not a bad transformer. It is an undocumented decision.

When you write each record, name the alternative you rejected, the tradeoff you accepted, and the condition that would make you reverse it. A decision with no named alternative was a default, not a design. That third field is the one you cannot fill by habit, because saying what would change your mind forces you to have actually reasoned.

Write the Decision Records

In the previous step, you saw terraform test fail on the stubbed module. That failure is honest. Before you fix it, you need to decide how you will fix it and why.

This is the step that does the most work against the backstory, and it is the easiest to rush. A transformer with no part identifier is not a bad transformer. It is an undocumented decision.

When you write each record, name the alternative you rejected, the tradeoff you accepted, and the condition that would make you reverse it. A decision with no named alternative was a default, not a design.

In this step, get ready to:
  • Write three ADRs covering tool choice, test strategy, and idempotence honesty.
  • Push the branch, open a PR that auto-closes the ticket, and merge to main.
  • Confirm the CI gate runs on the PR (test step still fails because the module is stubbed).
Create the branch and write ADR-001

Each story on the board becomes its own branch and PR. This keeps changes small and reviewable.

  • Create a new branch for story 1 by running this command:
git checkout -b story/01-decision-records
  • Create the decisions directory by running:
mkdir -p docs/decisions
  • Confirm the directory exists by running:
ls docs/

You should see decisions listed in the output.

The first ADR records why you chose Terraform over OpenTofu. Both tools support mock_provider testing. The deciding factor is ecosystem size and job-market signal.

  • Create docs/decisions/001-iac-tool-choice.md with the following content:
# ADR-001: IaC Tool Choice

## Status
Accepted

## Context
We need an IaC tool supporting native test mocking (no cloud account) for a reusable network module.

## Decision
Terraform (>= 1.7, currently 1.15.x) under BSL 1.1.

## Alternative Considered
OpenTofu (>= 1.8) under MPL 2.0 (Linux Foundation, CNCF Sandbox since April 2025).

## Deciding Factor
Terraform is the reference implementation with larger ecosystem (~3,900 providers vs ~3,000) and stronger job-market signal.

## Tradeoff Accepted
BSL 1.1 is not OSI-approved. Restricts competitive SaaS use. Internal use and CI/CD are explicitly permitted.

## Reversal Trigger
If the team builds a managed IaC platform product competing with HCP Terraform, or if policy requires OSI-approved licenses, re-evaluate for OpenTofu.

What makes a good ADR?

Every section answers a specific question. Context states the constraint. Alternative Considered names what you did not pick. Tradeoff Accepted names the cost you knowingly pay. Reversal Trigger tells a future reader when to re-open the decision.

Without these sections, you have a note. With them, you have an auditable design record.

  • Save the file.
  • Confirm it exists by running:
ls docs/decisions/

You should see 001-iac-tool-choice.md in the output.

Don't see the file?

Make sure you created the docs/decisions/ directory first. If you saved the file in the wrong location, move it with mv or recreate it in the correct path.

Help me find my ADR file in the repo

Write ADR-002 and ADR-003

The second record justifies your test approach. The third is the hardest to write because it names something you deliberately do not claim.

  • Create docs/decisions/002-test-strategy.md with the following content:
# ADR-002: Test Strategy

## Status
Accepted

## Context
The module needs a test approach that proves logic without cloud spend.

## Decision
Account-free mocked tests using Terraform native test framework with mock_provider.

## Alternative Considered
LocalStack emulation or a real sandbox AWS account.

## Deciding Factor
Zero cost, zero credentials, sub-second execution, no network dependency.

## Tradeoff Accepted
Mocked tests prove module logic and output math but not live provider integration or API-level compatibility.

## Reversal Trigger
If the module reaches production and needs an integration-test tier, add a sandbox account stage.

Why zero-cost testing matters here

LocalStack emulation requires Docker networking, a paid Pro tier for some AWS services, and real seconds of wall-clock time per test run. The mock_provider block runs entirely in memory with no credentials. That means anyone who clones this repo can run the tests immediately without an AWS account.

  • Save the file.
  • Create docs/decisions/003-idempotence-honesty.md with the following content:
# ADR-003: Idempotence Honesty

## Status
Accepted

## Context
Idempotence (a second plan reports no changes) is a common IaC claim.

## Decision
Idempotence is not asserted in this project's mocked tests.

## Alternative Considered
Claiming idempotence based on mocked test determinism.

## Deciding Factor
The test tool runs plan against in-memory state with a mocked provider; a repeated run proves the mock is deterministic, not that the module is idempotent against real infrastructure.

## Tradeoff Accepted
The module's idempotence claim is limited to mocked runs. Real idempotence is only proven against real state in a sandbox or production account.

## Reversal Trigger
If Terraform adds a built-in no-drift assertion for mocked runs, revisit.

Why name something you do not prove?

Most module READMEs claim idempotence without evidence. A mocked test re-run proves the mock is deterministic. That is not the same as proving the module produces no drift against real state. Naming the limit protects you in a review because you disclosed it rather than letting someone discover it.

  • Save the file.
  • Confirm all three ADRs exist by running:
ls docs/decisions/

You should see three files: 001-iac-tool-choice.md, 002-test-strategy.md, and 003-idempotence-honesty.md.

Missing one of the files?

Check for typos in the filename. The numbering prefix must be three digits (001, 002, 003). If you accidentally created the file at the repo root, move it into docs/decisions/.

Help me fix my ADR file paths

✔️ Awesome, I've got all three files

Great. Make sure all three are saved. You are ready to push.

ⓧ I'd like to double check the full code

Scroll up and confirm each file matches the code blocks above, in order:

  • docs/decisions/001-iac-tool-choice.md
  • docs/decisions/002-test-strategy.md
  • docs/decisions/003-idempotence-honesty.md

Each file should have the seven sections: Status, Context, Decision, Alternative Considered, Deciding Factor, Tradeoff Accepted, and Reversal Trigger.

Push, open a PR, and merge

With the three records written, you can push this branch and let the CI gate run. The PR description will auto-close the ticket when it merges.

  • Stage and commit your changes by running:
git add docs/decisions/
git commit -m "Add ADRs for tool choice, test strategy, and idempotence honesty"
  • Push the branch to GitHub by running:
git push -u origin story/01-decision-records
  • Open your repository on GitHub.
  • Click the Compare & pull request banner that appears for the branch you just pushed.
  • In the PR description, type Closes #1 so that merging auto-closes the Decision Records ticket.
  • Click Create pull request.

How does Closes #1 work?

GitHub recognises keywords like Closes, Fixes, and Resolves followed by an issue number in a PR description. When the PR merges into the default branch, GitHub automatically closes the linked issue. This keeps your project board in sync without manual ticket management.

Before you check the Actions tab, what do you think the gate will report? The module is still stubbed. The test step should fail.

  • Wait for the GitHub Actions run to appear in the Checks section of the PR.
  • Confirm the workflow fails at the Terraform Test step with assertion errors. This is expected because the module still returns placeholder values.
  • Merge the PR anyway by clicking Merge pull request, then Confirm merge.

Why merge when the gate is red?

On GitHub Free, branch protection (which blocks merging on red checks) is not available for private repos. The gate is informational here. You can see it fail and choose to merge because the ADR files do not affect the test result. The module fix comes in story 2.

  • After the merge, check your GitHub Project board. The Decision Records ticket (issue 1) should now show as Closed.

You should see one closed ticket and four open stories remaining on the board.

Ticket not closing automatically?

The Closes #1 keyword only works when the PR targets the default branch (main). Check that your PR was merged into main, not another branch. If the issue is still open, close it manually from the issue page.

Help me auto-close my GitHub issue

Three decisions are now recorded, each with a named alternative, a tradeoff, and a reversal trigger. The architecture board has something to read before they see a single line of HCL. Next up, you will fill the module with real CIDR logic and build the test matrix that proves it works.

Note what the mocked tests buy beyond speed. A module that needs a cloud account to verify is a module most people will copy and edit instead of reuse, because editing your own copy feels safer than trusting someone else’s untested work.

That instinct is exactly how nine slightly different versions appear. Tests anyone can run in seconds, on any laptop, with no credentials, remove the excuse.

Build the Module Logic and Test Matrix

Your ADRs documented the decisions. Now it is time to turn those decisions into working code.

A module that needs a cloud account to verify is a module most people will copy and edit instead of reuse, because editing your own copy feels safer than trusting someone else's untested work.

That instinct is exactly how nine slightly different versions appear. A module with tests and an ADR stating the tradeoff makes the next rebuild reviewable.

The module follows a standard structure: inputs validated in variables.tf so a bad value fails closed at plan time, CIDR derivation logic in main.tf, computed outputs in outputs.tf, and a caller example showing how to invoke the module with provider aliases.

You will use your AI agent to draft the code in parallel, then verify the CIDR math by hand before merging. The test matrix proves the module rejects bad input and computes correct subnet addresses across two regions.

In this step, get ready to:
  • Fill the module logic (variables, resources, outputs, example) using your AI agent.
  • Fill the test file with three mocked cases (standard 3-AZ, multi-alias, malformed CIDR rejection).
  • Verify the CIDR math by hand and confirm terraform validate passes.
Check out the module logic branch

Each story on your board gets its own branch and PR. Story 2 covers the module logic.

  • Create and switch to the story/02-module-logic branch by running this command:
git checkout -b story/02-module-logic
Prompt your AI agent to fill the module

Your AI agent drafts the boilerplate. Your job is to review and verify.

  • Prompt your AI agent to fill the four stub files. Tell it the module must:
  • Validate the input CIDR in variables.tf with a condition that rejects malformed values and prefix lengths outside /8 to /24.
  • Use cidrsubnet() with newbits=4 to derive public and private subnet CIDRs from the base (one per AZ).
  • Set map_public_ip_on_launch = false on public subnets (required for Checkov CKV_AWS_130).
  • Merge a default tag map (ManagedBy = "terraform", Module = "network") with the caller's tags.
  • Expose vpc_id, subnet IDs, subnet CIDRs, subnet_count_per_type, and merged_tags as outputs.
  • Fill examples/complete/main.tf showing a caller invoking the module with two provider aliases (aws for us-east-1 and aws.secondary for eu-west-1).

Why AI-then-review?

The module structure is well-documented boilerplate. Your AI agent handles the typing. Your value is verifying the CIDR math, checking that the validation actually rejects bad input, and confirming the outputs match what the test file expects.

Review the validation block in variables.tf

The validation block is the safety mechanism that rejects malformed CIDRs at plan time, before they propagate into a broken apply.

  • Confirm your modules/network/variables.tf matches this code:
variable "vpc_cidr" {
  description = "The base CIDR block for the VPC (e.g., 10.0.0.0/16)"
  type        = string

  validation {
    condition     = can(cidrhost(var.vpc_cidr, 0)) && tonumber(split("/", var.vpc_cidr)[1]) >= 8 && tonumber(split("/", var.vpc_cidr)[1]) <= 24
    error_message = "vpc_cidr must be a valid CIDR block with a prefix length between /8 and /24."
  }
}

variable "azs" {
  description = "List of availability zone suffixes (e.g., [\"a\", \"b\", \"c\"])"
  type        = list(string)
  default     = ["a", "b", "c"]
}

variable "region" {
  description = "AWS region for resource naming"
  type        = string
  default     = "us-east-1"
}

variable "tags" {
  description = "Map of tags to apply to all resources"
  type        = map(string)
  default     = {}
}

What does the validation block do?

  • can(cidrhost(var.vpc_cidr, 0)) checks whether the string is a syntactically valid CIDR. If cidrhost errors, can() returns false and the plan fails closed.
  • The second and third conditions extract the prefix length after the / and reject anything outside /8 to /24. A /33 (impossible) or /28 (too small to subnet with newbits=4) both fail.
Review the CIDR logic in main.tf

The locals block is where the subnet math lives. This is the code you verify by hand.

  • Confirm the locals block in modules/network/main.tf matches:
locals {
  az_count = length(var.azs)
  newbits  = 4
  default_tags = {
    ManagedBy = "terraform"
    Module    = "network"
  }
  merged_tags = merge(local.default_tags, var.tags)

  public_subnet_cidrs  = [for i in range(local.az_count) : cidrsubnet(var.vpc_cidr, local.newbits, i)]
  private_subnet_cidrs = [for i in range(local.az_count) : cidrsubnet(var.vpc_cidr, local.newbits, i + local.az_count)]
}

What does cidrsubnet do here?

cidrsubnet("10.0.0.0/16", 4, 0) adds 4 bits to the /16 prefix, producing a /20. The third argument (netnum) selects which /20 block within the /16. Each /20 is 4,096 addresses (16 * 256).

For a /16 base with 3 AZs: public subnets get netnums 0, 1, 2. Private subnets get netnums 3, 4, 5. That produces six non-overlapping /20 blocks.

  • Confirm the resources section in modules/network/main.tf includes the VPC, internet gateway, and subnets:
resource "aws_vpc" "this" {
  cidr_block           = var.vpc_cidr
  enable_dns_support   = true
  enable_dns_hostnames = true
  tags                 = merge(local.merged_tags, { Name = "${var.region}-vpc" })
}

resource "aws_internet_gateway" "this" {
  vpc_id = aws_vpc.this.id
  tags   = merge(local.merged_tags, { Name = "${var.region}-igw" })
}

resource "aws_subnet" "public" {
  count                   = local.az_count
  vpc_id                  = aws_vpc.this.id
  cidr_block              = local.public_subnet_cidrs[count.index]
  availability_zone       = "${var.region}${var.azs[count.index]}"
  map_public_ip_on_launch = false
  tags                    = merge(local.merged_tags, { Name = "${var.region}-public-${var.azs[count.index]}" })
}

resource "aws_subnet" "private" {
  count             = local.az_count
  vpc_id            = aws_vpc.this.id
  cidr_block        = local.private_subnet_cidrs[count.index]
  availability_zone = "${var.region}${var.azs[count.index]}"
  tags              = merge(local.merged_tags, { Name = "${var.region}-private-${var.azs[count.index]}" })
}
  • Confirm the routing section at the bottom of main.tf matches:
resource "aws_route_table" "public" {
  vpc_id = aws_vpc.this.id
  tags   = merge(local.merged_tags, { Name = "${var.region}-public-rt" })
}

resource "aws_route" "public_internet" {
  route_table_id         = aws_route_table.public.id
  destination_cidr_block = "0.0.0.0/0"
  gateway_id             = aws_internet_gateway.this.id
}

resource "aws_route_table_association" "public" {
  count          = local.az_count
  subnet_id      = aws_subnet.public[count.index].id
  route_table_id = aws_route_table.public.id
}
Review outputs.tf and the caller example
  • Confirm modules/network/outputs.tf exposes the computed values the test file asserts against:
output "vpc_id" {
  description = "The ID of the VPC"
  value       = aws_vpc.this.id
}

output "public_subnet_ids" {
  description = "List of public subnet IDs"
  value       = aws_subnet.public[*].id
}

output "private_subnet_ids" {
  description = "List of private subnet IDs"
  value       = aws_subnet.private[*].id
}

output "public_subnet_cidrs" {
  description = "List of public subnet CIDR blocks"
  value       = local.public_subnet_cidrs
}

output "private_subnet_cidrs" {
  description = "List of private subnet CIDR blocks"
  value       = local.private_subnet_cidrs
}

output "subnet_count_per_type" {
  description = "Number of subnets per type (public or private)"
  value       = local.az_count
}

output "merged_tags" {
  description = "The merged tag map applied to all resources"
  value       = local.merged_tags
}
  • Confirm examples/complete/main.tf shows a caller invoking the module with two provider aliases:
provider "aws" {
  region = "us-east-1"
}

provider "aws" {
  alias  = "secondary"
  region = "eu-west-1"
}

module "network_primary" {
  source = "../../modules/network"

  vpc_cidr = "10.0.0.0/16"
  azs      = ["a", "b", "c"]
  region   = "us-east-1"
  tags     = { Environment = "production", Team = "platform" }

  providers = {
    aws           = aws
    aws.secondary = aws.secondary
  }
}

output "primary_vpc_id" {
  value = module.network_primary.vpc_id
}

output "primary_public_cidrs" {
  value = module.network_primary.public_subnet_cidrs
}

What is the provider alias pattern?

The module declares configuration_aliases = [aws.secondary] in versions.tf. This tells Terraform the module expects a second provider configuration passed in from the caller. The caller's providers block maps both the default and the aliased provider.

This pattern lets one module serve multiple regions or accounts without duplicating code.

Validate and verify the CIDR math

Before you push, confirm the configuration is syntactically valid and the subnet math is correct.

  • Run terraform validate from the repo root:
terraform validate

Before you press Enter, predict: will this pass or fail? You replaced four stub files with real logic. If any resource references a missing variable or output, validate will catch it.

You should see:

Success! The configuration is valid.

Seeing a validation error?

  • Check that variables.tf declares all four variables (vpc_cidr, azs, region, tags) that main.tf references.
  • Confirm outputs.tf references aws_vpc.this.id and aws_subnet.public[*].id exactly as shown. A typo like aws_subnet.public.id (missing the splat) will fail.
  • help me debug a terraform validate error in my network module
  • Verify the CIDR math by hand for a 10.0.0.0/16 base with 3 AZs and newbits=4:

Hand-check: expected /20 subnets

  • Public subnet 0 (netnum 0): 10.0.0.0/20
  • Public subnet 1 (netnum 1): 10.0.16.0/20
  • Public subnet 2 (netnum 2): 10.0.32.0/20
  • Private subnet 0 (netnum 3): 10.0.48.0/20
  • Private subnet 1 (netnum 4): 10.0.64.0/20
  • Private subnet 2 (netnum 5): 10.0.80.0/20

Each /20 block is 4,096 addresses. The offset per netnum is 16 in the third octet (16 * 256 = 4,096). If your AI generated different values, the logic is wrong. Fix it before merging.

Push and merge story 2
  • Stage, commit, and push the branch by running:
git add .
git commit -m "feat: fill module logic with validated inputs and CIDR derivation"
git push -u origin story/02-module-logic
  • Open a pull request on GitHub. In the PR description, type Closes #2 to auto-close story ticket 2 on merge.
  • Merge the PR to main.

Why does the gate still fail?

The Actions workflow runs terraform test as part of the gate. Your module logic is now real, but the test file still has the starter assertions that reference the old stubs. The test step will pass once you fill the test matrix in the next story.

Check out the test matrix branch
  • Switch to main and pull the merged changes, then create story 3's branch by running:
git checkout main
git pull
git checkout -b story/03-test-matrix
Prompt your AI to fill the test file

The test file uses mock_provider to fake the AWS provider, so assertions run in memory with no account and no credentials.

  • Prompt your AI agent to fill tests/network.tftest.hcl with three run blocks:
  • Case 1 (standard_three_az): 10.0.0.0/16, 3 AZs, asserts all 6 subnet CIDRs plus subnet_count_per_type and merged_tags.
  • Case 2 (multi_provider_alias): 172.16.0.0/16, 2 AZs, passes both aws and aws.secondary via the providers attribute.
  • Case 3 (malformed_cidr_rejected): 10.0.0.0/33 with expect_failures = [var.vpc_cidr], proving the validation rejects it.
  • Tell the AI to use command = plan for all run blocks (no apply needed with mocked providers).
Review the test assertions against hand calculation

Every assertion must check a value you can verify by hand. Reject any AI-proposed case that checks a value you cannot compute yourself.

  • Confirm the mock provider declarations and Case 1 variables in your test file match:
mock_provider "aws" {}

mock_provider "aws" {
  alias = "secondary"
}

# Case 1: Standard 3-AZ deployment
run "standard_three_az" {
  command = plan

  variables {
    vpc_cidr = "10.0.0.0/16"
    azs      = ["a", "b", "c"]
    region   = "us-east-1"
    tags     = { Environment = "test" }
  }
  • Confirm Case 1's assertions match the hand-computed values:
  assert {
    condition     = output.public_subnet_cidrs[0] == "10.0.0.0/20"
    error_message = "First public subnet CIDR should be 10.0.0.0/20"
  }

  assert {
    condition     = output.public_subnet_cidrs[1] == "10.0.16.0/20"
    error_message = "Second public subnet CIDR should be 10.0.16.0/20"
  }

  assert {
    condition     = output.public_subnet_cidrs[2] == "10.0.32.0/20"
    error_message = "Third public subnet CIDR should be 10.0.32.0/20"
  }

  assert {
    condition     = output.private_subnet_cidrs[0] == "10.0.48.0/20"
    error_message = "First private subnet CIDR should be 10.0.48.0/20"
  }

  assert {
    condition     = output.private_subnet_cidrs[1] == "10.0.64.0/20"
    error_message = "Second private subnet CIDR should be 10.0.64.0/20"
  }

  assert {
    condition     = output.private_subnet_cidrs[2] == "10.0.80.0/20"
    error_message = "Third private subnet CIDR should be 10.0.80.0/20"
  }
  • Confirm the tag assertions and closing brace for Case 1:
  assert {
    condition     = output.subnet_count_per_type == 3
    error_message = "Subnet count per type should be 3"
  }

  assert {
    condition     = output.merged_tags["ManagedBy"] == "terraform"
    error_message = "Default tag ManagedBy should be terraform"
  }

  assert {
    condition     = output.merged_tags["Environment"] == "test"
    error_message = "Caller tag Environment should be test"
  }
}
  • Confirm Case 2 (multi_provider_alias) matches:
# Case 2: Multi-provider alias path exercised
run "multi_provider_alias" {
  command = plan

  providers = {
    aws           = aws
    aws.secondary = aws.secondary
  }

  variables {
    vpc_cidr = "172.16.0.0/16"
    azs      = ["a", "b"]
    region   = "eu-west-1"
    tags     = { Team = "platform" }
  }

  assert {
    condition     = output.public_subnet_cidrs[0] == "172.16.0.0/20"
    error_message = "First public subnet CIDR should be 172.16.0.0/20 in secondary region"
  }

  assert {
    condition     = output.subnet_count_per_type == 2
    error_message = "Subnet count per type should be 2 for 2-AZ deployment"
  }

  assert {
    condition     = output.merged_tags["Team"] == "platform"
    error_message = "Caller tag Team should be platform"
  }
}
  • Confirm Case 3 (malformed_cidr_rejected) matches:
# Case 3: Malformed CIDR rejected
run "malformed_cidr_rejected" {
  command = plan

  variables {
    vpc_cidr = "10.0.0.0/33"
    azs      = ["a"]
    region   = "us-east-1"
    tags     = {}
  }

  expect_failures = [var.vpc_cidr]
}

What does expect_failures prove?

The expect_failures attribute tells Terraform this run block is supposed to fail. If the variable validation does NOT reject 10.0.0.0/33, the test itself fails. This is the negative test: it proves bad input cannot sneak through.

Push and merge story 3
  • Stage, commit, and push by running:
git add .
git commit -m "test: add three-case mocked test matrix"
git push -u origin story/03-test-matrix
  • Open a pull request on GitHub. In the PR description, type Closes #3 to auto-close story ticket 3 on merge.
  • Merge the PR to main.

Note for your readout

The readout you write later will frame two failure modes for engineering leadership: (a) an output nobody checked, solved by assertions on every computed value, and (b) a bad input that slips through, solved by the validation block and the negative test case. Keep these in mind for Step 5.

Your module logic is real and your test matrix is ready. Next up, you will run the tests for real, scan with Checkov, and prove the CI gate catches a bad commit before accepting the fix.

Run Tests, Scan Policies, and Prove the Gate

Your module logic is written, your test file has three cases, and terraform validate says the configuration is valid. All of that is "it should work."

In this step, you turn that into "it does work." You will run every assertion against the mocked provider, scan the module for security policy violations, and prove the CI gate visibly rejects a bad commit before accepting the fix.

In this step, get ready to:
  • Run the three-case test matrix and confirm all assertions pass.
  • Scan the module with Checkov and confirm zero high/critical findings.
  • Prove the gate goes red on a bad commit and green on the fix.
Run the test matrix

The test file contains three run blocks, each checking a different scenario against your module logic. Time to see whether the math holds.

  • Check out the story branch by running this command:
git checkout -b story/04-policy-and-gate
  • Initialize Terraform (idempotent if already done) and then run the test suite:
terraform init

Before you run the tests, what do you expect? Will all three cases pass on the first try, or will any assertion fail against the CIDR values your module computes?

  • Run the full test matrix by running this command:
terraform test

You should see output ending with:

Success! 3 passed, 0 failed.

What do these three cases prove?

  • standard_three_az confirms the CIDR math: a /16 base with newbits=4 produces six /20 subnets at the exact addresses you computed by hand.
  • multi_provider_alias exercises the configuration_aliases path with a different base CIDR and 2 AZs, proving the module generalizes.
  • malformed_cidr_rejected feeds 10.0.0.0/33 and asserts the validation block rejects it at plan time rather than letting it propagate.

💡 Expected CIDR values for reference

For 10.0.0.0/16 with 3 AZs and newbits=4:

  • Public subnets: 10.0.0.0/20, 10.0.16.0/20, 10.0.32.0/20.
  • Private subnets: 10.0.48.0/20, 10.0.64.0/20, 10.0.80.0/20.

🙋‍♀️ Seeing an assertion failure?

If a test case fails, read the error message closely. It names the expected value and the value your module produced. Compare against the CIDR table above.

  • A wrong subnet CIDR usually means the newbits or the index calculation in main.tf does not match the test assertion.
  • A wrong tag value means merge() order in locals may be reversed (caller tags should override defaults, not the other way around).

Fix the mismatch in your module and re-run terraform test until all three pass.

Help me debug my terraform test assertion failure.

Scan with Checkov and close story 4

Passing tests prove your logic is correct. A policy scan proves your resources follow security best practices without you having to remember every rule.

  • Run Checkov against your module by running this command:
checkov -d modules/network --framework terraform

You should see a results summary with zero failed checks at HIGH or CRITICAL severity.

Why does the module pass CKV_AWS_130?

Check CKV_AWS_130 flags subnets that auto-assign public IPs on launch. Your module sets map_public_ip_on_launch = false on every public subnet, so instances are not publicly exposed by default.

🙋‍♀️ Seeing a HIGH or CRITICAL failure?

Read the check ID and the resource it flags. Common fixes:

  • If a subnet check fails, confirm map_public_ip_on_launch = false is set on the aws_subnet resource in modules/network/main.tf.
  • LOW and MEDIUM findings (like VPC flow logging) are informational for this module skeleton. Focus on HIGH and CRITICAL only.

Help me fix a Checkov finding on my Terraform module.

  • Stage and commit your work (no file changes are needed if tests and scan already pass, but commit the branch for the PR):
git add -A
git commit -m "story-04: confirm test and policy pass"
git push origin story/04-policy-and-gate
  • Open a pull request on GitHub from story/04-policy-and-gate into main.
  • In the PR body, type Closes #4 to auto-close story ticket 4 on merge.
  • Wait for the Actions workflow to finish on the PR (it should pass), then merge the pull request.
Prove the gate catches a bad commit

A green gate on good code is only half the proof. You need to see it go red on bad code to trust it.

  • Switch back to the main branch and confirm the Actions tab on GitHub shows a green workflow run.
git checkout main
git pull origin main
  • Open the Actions tab in your GitHub repository. You should see a green checkmark on the latest commit to main.

The seeded branch bad-cidr-no-validation was pre-staged in the starter repo. It removes the validation block from variables.tf, so the malformed_cidr_rejected test case has nothing to catch. What do you think happens when the gate runs against that branch?

  • Push the seeded bad branch to trigger the gate:
git push origin bad-cidr-no-validation
  • Return to the Actions tab on GitHub.
  • Click on the workflow run for the bad-cidr-no-validation branch.

You should see a red X. The Terraform Test step fails because the validation block is gone and the malformed CIDR passes through instead of being rejected.

Why does this matter?

Seeing a green badge is reassuring. Seeing a red badge on a known-bad commit is proof. It means the gate catches real problems, not just passes on good code.

The gate.yml workflow triggers on branches matching bad-* specifically so you can demonstrate this without polluting main.

🙋‍♀️ Gate still showing green on the bad branch?

  • Confirm the branch name starts with bad-. The workflow only triggers on branches matching [main, "bad-*"].
  • Check that the push actually reached GitHub by refreshing the repository page and looking for the branch in the branch dropdown.
  • If the workflow did not trigger at all, check that .github/workflows/gate.yml exists on the bad-cidr-no-validation branch itself.

Help me understand why my GitHub Actions workflow did not trigger on my bad branch.

Story 4 is closed, the gate is proven in both directions, and your project board shows four tickets done. Next up, you will write the PM readout, the teach-back page, and close the repo with a tagged release.

Write the Readout, Teach-Back, and Close the Repo

Your module works. The tests pass. The gate catches bad code. But the deliverable is not code alone.

A PM asked for a readout. The architecture board wants a teach-back page. The after-action review is what separates a senior engineer from someone who ships and forgets. In this step, you close every open thread: docs, measurements, tickets, and a tagged release.

In this step, get ready to:
  • Write the five-slide PM readout and teach-back page.
  • Record the after-action review, feasibility measurement, and AI score.
  • Close story 5, tag release v1.0.0, and confirm the board is empty.
Write the PM readout and teach-back page

Engineering leadership and the architecture board need different views of the same module. The readout frames it in five slides aimed at both rooms.

  • Check out a new branch by running this command:
  • Create docs/readout.md with the following heading structure:
git checkout -b story/05-readout-and-close
# PM Readout: Network Module v1.0

## Slide 1: Module Boundary

## Slide 2: Test Matrix and Failure Modes Designed Out

## Slide 3: What This Does Not Prove

## Slide 4: Tool Decision Table

## Slide 5: Running Cost and Adoption Claim

What goes in each slide?

  • Slide 1: A diagram of the module inputs (vpc_cidr, azs, tags) and outputs (vpc_id, subnet IDs, subnet CIDRs). One picture.
  • Slide 2: Name the two failure modes designed out: an output nobody checked (solved by assertions on every computed value) and a bad input that slips through (solved by the validation block and the negative test case).
  • Slide 3: Idempotence is not asserted. Live integration is not tested. State these boundaries for the architecture board.
  • Slide 4: A table comparing Terraform vs OpenTofu across license, governance, test mocking, ecosystem size, and job-market signal.
  • Slide 5: Running cost is zero (tests never touch a real provider). The plain claim: the next network rebuild is a review, not a rewrite.
  • Fill in each slide section with the content described above. Write in clear prose or bullet points. Draw from the ADRs you already wrote.
  • Save docs/readout.md.

Next, create the teach-back page. This is the document you would hand a new team member so they understand your design decisions.

  • Create docs/teach-back.md with this heading structure:
# Teach-Back: Network Module Design

## Tool-Decision Table

## Defense of Key Design Decisions

### Why mock, not emulate?

### Why alias, not multi-cloud?

### Why not claim idempotence?

## Framework Named

Standard module structure with validated inputs that fail closed.

What does each section cover?

  • Tool-Decision Table: replicate the Slide 4 table from your readout. Add a row for the deciding factor.
  • Defense sections: for each question, name the alternative, state why you chose differently, and cite the tradeoff from your ADRs.
  • Framework Named: explicitly state the pattern (standard module structure, validated inputs that reject bad values at plan time).
  • Fill in each section with your reasoning. Reference your ADRs where appropriate.
  • Save docs/teach-back.md.
  • Confirm both files exist by running:
ls docs/readout.md docs/teach-back.md

You should see both filenames printed without errors.

File not found?

  • Make sure you created the files inside the docs/ directory (not in the repo root).
  • Check for typos in the filename. The names are all lowercase.

Need help with the readout structure? Help me frame my PM readout for two audiences.

Record the after-action review and measurements

Honest reflection is the highest-leverage documentation. The after-action review asks four questions that force you to name what actually happened.

  • Create docs/after-action.md with this structure:
# After-Action Review

## 1. What was meant to happen

## 2. What actually happened

## 3. Why the gap

## 4. The one thing to change next time

How do I fill this in honestly?

Section 1: state the target (three tests passing, Checkov clean, gate proven, five stories closed, tagged release, all under 45 minutes).

Section 2: name what actually happened. Which tests passed first try? Which needed iteration? How long did each phase take?

Section 3: there is always a gap. Common gaps include CIDR math off-by-one, an unexpected Checkov finding, or a slow devcontainer first pull.

Section 4: one concrete action, not "be better". For example, "pin the devcontainer image digest so first boot is deterministic."

  • Fill in each section with your real experience from this project.
  • Save docs/after-action.md.
  • Create docs/feasibility.md to record your wall-clock measurement:
# Feasibility Record

## Environment

- OS:
- Machine:
- Docker Desktop version:
- Devcontainer rebuild from scratch: yes/no

## Wall-Clock

- Clone to first `terraform test` failure:
- First failure to all tests green:
- All tests green to tagged release:
- Total clone to tagged release:

## Notes
  • Fill in the Environment section with your OS and machine details.
  • Fill in the Wall-Clock section with real measured times (in minutes and seconds).
  • Save docs/feasibility.md.
  • Create docs/ai-score.md to record how the AI performed against the sealed answer key:
# AI Accuracy Score

## Counts

- Cases the AI got right and I kept:
- Cases the AI got wrong and I rejected:
- Cases the sealed key holds that the AI missed:

## Coverage

Coverage = (correct kept) / (total in answer key) =

How do I score the AI?

Open scoring/answer-key.yaml and compare it to the test cases your AI proposed in Step 3. Count how many the AI got right (and you kept), how many the AI got wrong (and you rejected), and how many the answer key holds that the AI missed entirely.

A score of 100% kept with 0% rejected is not a perfect AI. It is a flag that the learner accepted without review. The scoring is designed to make rubber-stamping visible.

  • Fill in the three counts and compute the coverage fraction.
  • Save docs/ai-score.md.
  • Confirm all five docs exist by running:
ls docs/readout.md docs/teach-back.md docs/after-action.md docs/feasibility.md docs/ai-score.md

You should see all five filenames listed without errors.

Missing a file?

Double-check that each file was saved inside the docs/ directory with the exact lowercase filename shown above.

Stuck on the after-action or AI score? Help me write my after-action review honestly.

Close the repo with a tagged release

A genuinely closed repo means every PR merged, every ticket closed, a release tagged, and one-command docs present so a teammate can run it.

  • Update README.md so it includes a one-command run instruction. Replace the placeholder content with:
# Network Module

Reusable AWS VPC module with mocked tests and CI gate.

## Quick Start

```bash
git clone <repo-url> && cd <repo-name> && terraform test
```
  • Replace <repo-url> and <repo-name> with your actual repository URL and directory name.
  • Save README.md.
  • Stage, commit, and push your branch by running these commands:
git add .
git commit -m "Add readout, teach-back, after-action, feasibility, AI score, and README"
git push -u origin story/05-readout-and-close
  • Open a pull request on GitHub with the body Closes #5 (where #5 is your story 5 issue number).
  • Wait for the Actions gate to pass (green check).
  • Merge the PR. Confirm story ticket 5 is now closed on your GitHub Projects board.

Why include Closes #5 in the PR body?

GitHub automatically closes the linked issue when the PR merges. This keeps your project board clean without manual ticket management.

  • Switch to main and pull the merged changes by running:
git checkout main
git pull origin main
  • Tag the release and push the tag by running:
git tag -a v1.0.0 -m "Network Module v1.0.0"
git push origin v1.0.0

What does this command do?

The -a flag creates an annotated tag (stores your name, date, and message). The -m flag provides the tag message inline so Git does not open an editor.

Tags are not pushed by default. The second command explicitly pushes v1.0.0 to origin so it appears on the GitHub release page.

Before you check the release page, what do you expect to see there?

  • Open your repository on GitHub and navigate to the Releases page (right sidebar or the tags link).
  • Confirm v1.0.0 appears with the tag message "Network Module v1.0.0".
  • Navigate to your GitHub Project board. Confirm all five story tickets are closed and no open items remain.

Your board should show zero open items. The epic "Network Module v1.0" is complete.

Tag not showing on GitHub?

  • Remember that git push alone does not push tags. You must explicitly run git push origin v1.0.0.
  • Confirm you are on the main branch before tagging. Run git branch to check.

Still stuck? Help me push a tag to my remote repo.

Your repo is closed. Every PR merged, every ticket done, release tagged, and plain-language docs present so a teammate can run it in one command. That is the module nobody can argue with.

Secret mission

Record Real Numbers, Not Estimates

Your docs/feasibility.md and docs/ai-score.md have sections waiting for data. The difference between a senior engineer and someone who ships and forgets is whether those fields contain measured numbers or comfortable guesses. This mission makes rubber-stamping visible.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs entirely locally inside a devcontainer with mocked tests, so there are no ongoing cloud costs.

Resources you used:

  • Private GitHub repository (includes the GitHub Project board, Actions workflow runs, issues, and tagged release). Uses free-tier Actions minutes at approximately 2 minutes per run from the 2,000/month quota.
  • Local project files (the cloned repository on your Desktop).

Keep everything running

No action needed. Choose this if you want to keep the repo on GitHub for portfolio use or future reference.

  • The repository stays on GitHub at zero ongoing cost. Mocked tests never touch a real cloud provider, so nothing is accumulating charges.
  • The GitHub Actions free tier gives private repos 2,000 minutes per month. Each gate run uses approximately 2 minutes, so normal use stays well within the quota.
  • Your local project files remain on your Desktop for quick access.

Pause - I'll come back to this later

Archive the repository to make it read-only. Your code, issues, releases, and project history are preserved. You can unarchive later to resume work.

  • Open your repository on GitHub.
  • Click Settings under the repository name.
  • Scroll down to the Danger Zone section.
  • Click Archive this repository.
  • Type the repository name to confirm, then click I understand the consequences, archive this repository.
  • Your local files can stay on disk. They will not change or cost anything.

Delete - I don't want to use this again

Remove all project resources and start fresh. This permanently deletes the repository, its issues, releases, Actions history, and the Project board.

Delete the GitHub repository:

  • Open your repository on GitHub.
  • Click Settings under the repository name.
  • Scroll down to the Danger Zone section.
  • Click Delete this repository.
  • Type the repository name to confirm, then click I understand the consequences, delete this repository.

Delete the GitHub Project board:

  • Navigate to your Project board on GitHub.
  • Click the menu icon in the top-right corner, then click Settings.
  • In the Danger zone section, click Delete this project.
  • Type the project name to confirm, then click I understand the consequences, delete this project.

Remove local project files:

  • Delete the cloned repository folder from your Desktop by running the appropriate command for your OS.

macOS

cd ~/Desktop
rm -rf network-module

Windows

cd ~\Desktop
Remove-Item -Recurse -Force network-module

Nice Work!

Nice Work!

You just built a production-grade Terraform module that an architecture board can review without a single question left unanswered. The repo proves itself: tests pass, policy clears, the gate catches bad commits, and every decision is documented.

You've learned how to:

  • Build a reusable AWS VPC module following the standard module structure with validated inputs that fail closed, CIDR derivation logic, provider-alias support, and a three-case mocked test matrix that passes with zero cloud spend.
  • Prove a CI gate catches real problems by showing it go red on a seeded bad branch and green on the fix, backed by Checkov policy scanning with zero high/critical findings.
  • Deliver a complete audit trail through standard tools: three Architecture Decision Records with named alternatives and reversal triggers, a five-slide PM readout for two audiences, a GitHub Project board with every ticket closed, and a tagged release.
  • Secret Mission: Measure real feasibility (wall-clock from clean clone, OS named) and AI accuracy (scored against a sealed answer key) as recorded numbers, with rubber-stamping made visible.

Close the loop on the story before you move on. You did not fix a grid, and this project makes no claim that better infrastructure code affects anyone’s spare-parts inventory. What you fixed is the pattern underneath: a component rebuilt slightly differently each time, with the reasoning lost, becomes impossible to match later.

The one line worth keeping: every reusable component starts as somebody’s one-off, and what decides whether it stays reusable is not the code, it is whether the reasoning was written down beside it.

Ready to quiz yourself?