Module 8: Capstone The Andes Cargo Pipeline
2. Architecture review: the complete pipeline
Description
Before running anything in this module, it's worth having the pipeline's complete map in one place — not scattered across seven modules, but as a single diagram you can point at, piece by piece, and explain what it does and why it's there. This lesson runs no new command: it precisely assembles andes-cargo-infra/'s complete diagram as it stands at the close of Module 7 —the three workflows, the guardrail, and the networking piece that makes it possible for any of the three to talk to LocalStack— so lessons 3 and 4 run on an already-understood map, not on loose pieces you have to recall on the fly.
Connection to the module
This lesson is purely a review — every piece of the diagram you already built and ran in an earlier module; here they just get assembled into a single picture. Lesson 3 walks exactly this diagram, left to right, with a real change crossing it. Lesson 4 walks the same picture, but stopping exactly where the guardrail acts.
Analogy: a factory's complete blueprints, before the guided tour
Imagine you're about to give a guided tour of a factory you helped build, piece by piece, over several months. Before opening the doors to visitors, it makes sense to stand in front of the building's complete blueprints —not the memory of each separate week of construction, but the final plan, with every assembly-line station marked, every pipe connecting one area to another— and confirm you can point at any spot and explain, without hesitation, what happens there and why. This lesson is that moment in front of the blueprints: before lessons 3 and 4, which are the real guided tour, with material actually flowing through each station.
The complete diagram: the three workflows and the guardrail
┌─────────────────────────────────────────────────────┐
│ andes-cargo-infra/ (Git repository) │
│ │
│ .github/workflows/ │
│ ├── ci.yml (Module 3, guardrail M6) │
│ ├── apply.yml (Module 5) │
│ ├── drift.yml (Module 5) │
│ └── guardrail-demo.yml (Module 6, testing only) │
└─────────────────────────────────────────────────────┘
① pull_request (PR against main)
│
▼
┌─────────────────────── ci.yml — job: terraform-checks ───────────────────────┐
│ checkout → setup-terraform → fmt -check → init → validate → install │
│ awslocal → [confirm LocalStack, continue-on-error] → install tflocal → │
│ plan -out=tfplan → ┌─────────────────────────────────┐ → publish │
│ │ GUARDRAIL (Module 6, lesson 7) │ summary → │
│ │ grep over terraform show -json │ upload-artifact │
│ │ "actions":["delete"] on │ (terraform-plan)│
│ │ aws_dynamodb_table.shipments? │ │
│ │ YES → exit 1, job fails here │ │
│ │ NO → continues to next step │ │
│ └─────────────────────────────────┘ │
└────────────────────────────────────────────────────────────────────────────┘
│ (only if the complete job succeeded, guardrail included)
▼
② (a person reviews the plan summary, approves, merges the PR)
│
▼
③ push to main (the merge, simulated with `act push`)
│
▼
┌────────────────────── apply.yml — two jobs, in Stages ───────────────────────┐
│ Stage 0 — fetch-reviewed-plan: │
│ download-artifact (terraform-plan) → confirm tfplan arrived intact │
│ │
│ Stage 1 — terraform-apply (needs: fetch-reviewed-plan): │
│ checkout → setup-terraform → download-artifact (again, another container)│
│ → init → install tflocal → apply -auto-approve tfplan │
│ │
│ concurrency: { group: apply-andes-cargo-infra, cancel-in-progress: false } │
└────────────────────────────────────────────────────────────────────────────┘
│
▼
④ drift.yml — schedule (06:00 UTC) or workflow_dispatch, at any moment
checkout → setup-terraform (terraform_wrapper: false) → init → install
tflocal → plan -detailed-exitcode → reports 0 (no drift) / 2 (drift) /
other (failed) in $GITHUB_STEP_SUMMARY
Four pieces, one repository: ci.yml is the front door —nothing reaches main without going through it, guardrail included—; apply.yml is the only door out toward real infrastructure, and it only opens with a plan that already went through the first door; drift.yml doesn't react to any repository event, it watches on its own, on its own schedule; guardrail-demo.yml isn't part of the normal flow — it's the test bench you used in Module 6, and reuse in this module's lesson 4, to demonstrate the guardrail works with no need for a real Pull Request to trigger it.
The network: act's container talking to LocalStack on the host
Here's the piece that makes it possible for any of the steps above to reach AWS —or, in this lab, its simulation—:
YOUR MACHINE (host) act's CONTAINER (ephemeral, per job)
┌─────────────────────┐ ┌──────────────────────────────┐
│ localstack_main │ │ catthehacker/ubuntu:act- │
│ port 4566 │ │ latest │
│ (docker run, │◄───────────────────── │ │
│ Module 1, lesson 8) │ host.docker.internal │ AWS_ENDPOINT_URL= │
│ │ :4566 │ http://host.docker.internal │
│ account 000000000000 │ │ :4566 │
└─────────────────────┘ │ │
│ terraform / tflocal / awslocal│
.actrc: │ run HERE, not on the host │
-P ubuntu-latest=catthehacker/ubuntu:act-latest └──────────────────────────────┘
--container-options
"--add-host=host.docker.internal:host-gateway"
host.docker.internal is the special name Docker resolves, from inside a container, toward the host running it — without .actrc's --add-host=host.docker.internal:host-gateway flag (Module 3, lesson 5), that name doesn't resolve on Linux, even though it does by default on Docker Desktop for macOS/Windows. It's, literally, the only bridge between act's ephemeral, isolated container world and the LocalStack that persists, started just once, on your machine.
The complete sequential flow, from PR to apply
sequenceDiagram
participant Dev as Developer
participant CI as ci.yml (PR)
participant Rev as Human reviewer
participant Main as main
participant Apply as apply.yml (push)
participant LS as LocalStack
Dev->>CI: opens PR (feature/* branch)
CI->>CI: fmt, init, validate
CI->>LS: awslocal s3 ls (check, continue-on-error)
CI->>CI: terraform plan -out=tfplan
CI->>CI: guardrail: grep over plan JSON
alt guardrail detects destroy on Shipments
CI-->>Dev: job fails, NEVER uploads the artifact
else guardrail passes
CI->>CI: publishes summary, upload-artifact
CI-->>Rev: plan visible for review
Rev->>Main: approves and merges the PR
Main->>Apply: push triggers apply.yml
Apply->>Apply: download-artifact (same tfplan, SHA256 verified)
Apply->>LS: terraform apply -auto-approve tfplan
LS-->>Apply: resources created (or honest failure with no token)
end
This diagram is the executable version of the two-key safe analogy you already know from Module 4: ci.yml is the first key (calculates what would happen), a real person is the second (decides whether that should happen), and apply.yml never activates without both keys having turned in the correct order.
Confirming the inventory with act -l
Before running any scenario in lessons 3 and 4, confirm all four workflows are still exactly where they should be:
cd andes-cargo-infra
act -l
What to expect (literal, executed to write this lesson):
Stage Job ID Job name Workflow name Workflow file Events
0 fetch-reviewed-plan fetch-reviewed-plan apply apply.yml push
0 terraform-checks terraform-checks ci ci.yml pull_request
0 check-drift check-drift drift-detection drift.yml schedule,workflow_dispatch
0 destroy-shipments-check destroy-shipments-check guardrail-demo guardrail-demo.yml workflow_dispatch
1 terraform-apply terraform-apply apply apply.yml push
Five rows, four workflows: apply.yml shows up twice because it has two jobs across two different Stages —fetch-reviewed-plan in Stage 0, terraform-apply in Stage 1, chained by needs: (Module 5, lesson 3)—. drift.yml is the only one listening for two events at once (schedule and workflow_dispatch), and guardrail-demo.yml is the only one that never got triggered by any real repository event —only workflow_dispatch, by hand, exactly as befits a test bench—. If your output matches, row by row, this table, your project is in the exact state lessons 3 and 4 assume.
Common mistakes
Looking for a fifth workflow or a new job in this review (expectation-based). What happens: someone, seeing the title "architecture review," expects this lesson to reveal a piece earlier modules didn't show. How to fix it: this lesson, on purpose, has no new piece at all — its only value is assembling, into a single diagram, exactly what you already built, so lessons 3 and 4 don't have to reconstruct the map on the fly.
Confusing guardrail-demo.yml with a workflow that runs in production (conceptual, revisit Module 6). What happens: someone, seeing act -l's five rows, assumes guardrail-demo.yml is part of the normal Pull Request flow, just like ci.yml. How to spot it: check the Events column — guardrail-demo.yml only listens for workflow_dispatch, never pull_request or push. How to fix it: the real guardrail, the one that genuinely protects every Pull Request, lives inside ci.yml (the step added in Module 6, lesson 7) — guardrail-demo.yml is, exclusively, the test bench that seeds a fake state to be able to test that same guardrail with no need for real applied infrastructure.
Thinking host.docker.internal is a Terraform setting (configuration-based, revisit Module 3). What happens: someone looks for that name inside providers.tf, and doesn't find it. How to fix it: host.docker.internal:4566 lives in every workflow's env: AWS_ENDPOINT_URL (Module 3, lesson 5) and in .actrc's --container-options flag — it's a Docker/act networking piece, not Terraform configuration. providers.tf is deliberately kept minimal, ready for the endpoint to arrive from outside.
Exercises
Exercise 1 — Trace, from memory, a blocked plan's complete path. Without looking at this lesson's diagram, draw (on paper or in a text editor) which ci.yml steps DO run and which ones NEVER run when the guardrail detects a Shipments destruction.
See solution
They run, in order: checkout, setup-terraform, fmt -check, init, validate, install awslocal, confirm LocalStack (with or without success, thanks to continue-on-error), install tflocal, terraform plan, and the guardrail step, which is where the job stops with exit 1. Never run: publish the plan to the job summary nor upload the plan for apply.yml to use later — both steps live after the guardrail in the file, and GitHub Actions (just like act) doesn't execute any step after one that failed, unless that step has its own if: always() or similar, which this ci.yml doesn't use.
Exercise 2 — Explain why apply.yml has two Stages and not just one. A colleague asks why not simplify apply.yml into a single job with all the steps together, instead of two jobs separated by needs:. Answer them with Module 5's exact reason.
See solution
Splitting into two jobs (fetch-reviewed-plan and terraform-apply) makes confirming the artifact exists and arrived intact an explicit condition, with its own visible result, before any apply attempt starts — if fetch-reviewed-plan fails, terraform-apply (which depends on it with needs:) never runs at all. With a single job, an early failure downloading the artifact would interrupt the job just the same, but without the clarity of "this specific stage is the one that failed" a separate Stage provides, and without the structural guarantee that no real apply step can execute before confirming the plan arrived correctly.
Exercise 3 — Locate where each of this guide's security pieces lives in the diagram. Point out, on this lesson's complete pipeline diagram, exactly where each of these three pieces acts: secrets.AWS_ACCESS_KEY_ID (Module 4), the guardrail (Module 6), and concurrency: (Module 5).
See solution
secrets.AWS_ACCESS_KEY_ID acts in ci.yml's env: block, before any step runs — it isn't a point in the flow, it's a configuration every step in that job inherits. The guardrail acts inside ci.yml, specifically between the Terraform plan step and the Publish the plan to the job summary step — it's the only point in the whole diagram that can stop a change before it gets published or uploaded as an artifact. concurrency: acts at the level of the entire apply.yml workflow, not any specific step — it's the rule that decides whether a new apply.yml run can start immediately or has to wait for the current run to finish, regardless of which exact step that in-progress run is at.
Summary and next step
In this lesson you assembled Andes Cargo's pipeline's complete diagram: the three production workflows (ci.yml, apply.yml, drift.yml) plus the guardrail's test bench (guardrail-demo.yml), the network connecting act's container with the host's LocalStack, and the complete sequential flow of a change from opening a Pull Request to applying it. You confirmed, with act -l, that the five job rows exactly match what this review predicted.
Before moving on you should be able to: draw the complete diagram from memory, pointing out where each security piece acts; explain why guardrail-demo.yml isn't part of the normal Pull Request flow; and trace the exact path a plan blocked by the guardrail follows —or doesn't follow.
With the complete map now in your head, lesson 3 walks this same diagram end-to-end, with a real HCL change crossing it — the first of the two walkthroughs that close this guide's technical thesis.
Resources
- nektosact.com — User Guide — reference for
act -l, used in this lesson to confirm the complete inventory. - Docker Docs — Networking: use cases and network drivers — official documentation for name resolution between containers and the host, the basis for
host.docker.internal. - This guide's Module 3 (
05-connecting-the-runner-to-localstack.md) — the complete origin of the networking piece this review diagrams. - This guide's Module 6 (
07-hands-on-a-failing-guardrail-example.md) — the guardrail's origin, diagrammed here insideci.yml.