Module 3: The Iac Pipeline Fmt Validate Plan

1. Module introduction: what runs before merging

Description

In the two previous modules you built the tool and learned its language: you installed act, ran workflows end to end, dissected on/jobs/steps/runs-on/uses/with/env, and learned to simulate events and secrets without depending on a real GitHub account. But so far, no workflow in this guide has touched Andes Cargo's business infrastructure — hello-andes-cargo.yml (Module 2) only confirmed that the job's container can reach LocalStack, it never ran terraform plan or terraform apply against the real bucket, table, roles, or function. This module closes that gap: you're going to build half of Andes Cargo pipeline's CI — the industry-standard pattern where terraform fmt, terraform validate, and terraform plan run automatically on every Pull Request, and the resulting plan becomes the artifact someone reviews before merging. By the end of this module you're going to have ci.yml, this guide's first workflow that actually executes Terraform, inside an ephemeral runner, against the same infrastructure terraform-and-iac-guide taught you to declare by hand.

Connection to the module

Lessons 2 and 3 are the pattern's why and how: what HashiCorp's official tutorial says about separating "review" from "apply" into two distinct workflows (lesson 2), and how an ephemeral runner installs Terraform on every run, leaving no trace between one run and the next (lesson 3). Lessons 4, 5, and 6 are pure hands-on, building ci.yml piece by piece: fmt/validate as the first two steps that can fail the job red (lesson 4), the network connection to LocalStack through host.docker.internal (lesson 5), and the first real terraform plan running inside that runner (lesson 6). Lesson 7 closes the review loop: publishing that plan as evidence, with the real GitHub pattern ($GITHUB_STEP_SUMMARY, executed) and the market pattern (commenting on the PR, shown). Lesson 8 —the project— runs the complete ci.yml, end to end, with act pull_request -e pr-event.json, against a real change to Andes Cargo's HCL.


What you take from Modules 1 and 2 (and what's missing)

By the end of Module 2 you knew how to write a complete workflow, simulate any event with act -e, and pass it secrets without ever committing them. What you still didn't know —and is exactly what this module solves—:

  • What Terraform runs inside CI, not on your laptop: terraform fmt, terraform validate, terraform plan, each with a distinct purpose and a distinct reason to fail the job.
  • Why the pattern separates plan from apply into two distinct workflows, instead of one with a condition — a security decision, not a convenience one, cited directly from HashiCorp's official tutorial.
  • How to install Terraform inside a runner that persists nothing between one run and the next — unlike your laptop, where terraform-and-iac-guide installed the binary once and it stayed there.
  • How that ephemeral runner reaches LocalStack, which runs on your host, not inside the job's container — the same networking piece you tested with awslocal in Module 2, now in service of a real terraform plan.
  • What "reviewing a plan before merging" means, in practice: it's not just a phrase — it's a text artifact, generated by a machine, that a person reads before saying yes.

This module's map: the 8 lessons

#LessonWhat you practice
1Introduction (this one)The module's map, what's missing from Modules 1-2, why the plan is the review point
2The HashiCorp/GitHub pattern: plan on PR, apply on mergeThe industry-standard pattern, cited from the official tutorial; why separating into two workflows is safer
3Installing Terraform inside an ephemeral runnerhashicorp/setup-terraform@v3, pinned version; why the runner installs Terraform on every run
4Hands-on: fmt and validate as steps that can fail the jobExecuted: both steps running inside act, an error introduced on purpose, seen failing red, fixed
5Connecting the runner to LocalStack through the hostExecuted: host.docker.internal:4566, extended .actrc, verified with awslocal s3 ls
6Hands-on: terraform plan running inside CIExecuted: this guide's first real terraform plan, inside an act job
7Publishing the plan as review evidenceExecuted: $GITHUB_STEP_SUMMARY; Representative: commenting the plan on the PR with actions/github-script
8Project: Andes Cargo's ci.ymlExecuted: the complete workflow, end to end, with act pull_request -e pr-event.json

The module's analogy: quality control before boarding, not the customs signature

In terraform-and-iac-guide, every time you were about to change Andes Cargo's infrastructure, you ran terraform plan in your terminal, read it with your own eyes, and only then ran terraform apply — all under your direct control, one step after another, with nobody else seeing it before it happened. That flow works when you're the only one who can touch the infrastructure. It stops working the moment a second, a third, or a tenth teammate can also propose changes: if each one runs their own plan on their own laptop, with their own credentials, nobody else sees that plan before it gets applied — the equivalent of every port worker deciding, on their own, which container goes onto the ship, with no inspector reviewing it before sealing the hold.

This module builds exactly that inspector: an automatic process that, as soon as someone proposes a change (a Pull Request), generates the same plan you'd generate by hand — but does it somewhere anyone with access to the repository can read it before anything gets applied. The plan isn't a formality signed without looking at customs; it's the cargo manifest an inspector checks container by container, before the ship sails — not a stamp applied after it already sailed. That's, literally, what "the plan is the review point, not the apply" means: by the time someone runs apply, there shouldn't be any surprises left — everything that apply is going to do was already read, already discussed, already approved, in the plan that preceded it.


Why the plan is the review point, not the apply

It's worth being explicit about this before writing a single line of YAML, because it's the idea that organizes the entire module. There are two ways to build an infrastructure pipeline, and only one is safe:

Wrong path: a single step that runs terraform apply directly as soon as someone proposes a change, with nobody reviewing it first. Fast, but equivalent to the manual apply Module 1 already identified as the original problem — only now the lack of review is automated, not solved.

This guide's path (and the industry's): terraform plan runs automatically on every Pull Request, before the possibility of merging even exists. The plan is a read-only artifact — it changes nothing, it touches no real resource, it only calculates and shows what would change if someone ran apply. That makes it the perfect place to put a human in the middle: someone reads that plan, confirms it does what the Pull Request says it does, and only then approves the merge. The real apply —the one that does touch real infrastructure— arrives afterward, triggered by the merge itself, not by the proposal. You're going to build that second half in Module 5; this module builds the first, the one that makes there be something to review at all.


What does NOT change in this module

You still don't spend a cent, and the Andes Cargo project remains, line for line, the same HCL terraform-and-iac-guide left finished in its capstone: bucket andes-cargo-shipment-docs, table Shipments, roles LambdaManifestProcessorRole and AppServerRole, function process-shipment-manifest. This module doesn't declare a single new business resource — all the work is building the pipeline that runs that HCL, not expanding what that HCL declares. Same account 000000000000, same region us-east-1.

What does change: for the first time in this guide, a workflow is actually going to execute Terraform. No previous module did — hello-andes-cargo.yml (Module 2) only confirmed a network path with awslocal, an AWS CLI command, not Terraform.

Before and after this module

   BEFORE (end of Module 2)                      AFTER (end of Module 3)

   "andes-cargo-infra/ has real HCL,             "Every Pull Request
    but no workflow touches it                    automatically triggers fmt +
    yet."                                          validate + plan on that
                                                    same HCL."

   "I know the job's container can               "I know that same network
    reach LocalStack, because I tested             path sustains a real
    it with an AWS CLI command."                   terraform plan, not just
                                                     an AWS CLI command."

   "I don't have any artifact                    "I have a readable plan,
    someone can review before a                   published in the job's
    change gets applied."                          summary, ready for
                                                     someone to review before
                                                     merging."

Common mistakes

Thinking "CI for Terraform" means running apply automatically (conceptual, this module's central confusion). What happens: someone, familiar with application code CI (where "passing CI" sometimes means "it can already deploy on its own"), assumes an infrastructure pipeline's CI half should also apply the changes. Why it happens: in many application pipelines, CI and CD are more fused than this module teaches. How to spot it: if your first instinct on seeing ci.yml is to ask "so where's the apply?" How to fix it: remember M1.3's table — CI validates, CD-delivery leaves it ready with a human gate, CD-deployment applies on its own. This module exclusively builds the CI part: fmt/validate/plan, never apply. Automatic apply, triggered by the merge to main, is Module 5's content — a deliberate decision, not a scope mistake.

Assuming a "clean" plan (no errors) is the same as an "approved" plan (process-based). What happens: someone sees ci.yml finish green and assumes that's equivalent to the change already being approved to merge. Why it happens: a green job feels like "everything went fine," without distinguishing between "the plan was generated with no technical errors" and "a person read it and decided the change is correct." How to fix it: a green plan only confirms Terraform was able to calculate what would change — it says nothing about whether that change is the right one. That's why Module 6 (branch protection) requires a human to review and approve before the merge is even possible, even with ci.yml green.


Exercises

Exercise 1 — Order this module's three stages. Without looking at the map above, order these three things in the order ci.yml is going to execute them within the same job: (a) terraform plan, (b) terraform fmt -check, (c) terraform validate. Justify the order.

See solution

The correct order is (b) fmt -check → (c) validate → (a) plan. The reason is increasing cost and dependency: fmt -check is instant and needs no providers downloaded — it only checks the HCL text's style. validate does need providers initialized (terraform init), but only checks syntax and internal consistency, without touching any AWS/LocalStack service. plan is the most expensive and the only one that might need to talk to a real provider — it makes sense to leave it last, so as not to spend that cost if the file doesn't even have the correct format. Each earlier step acts as a cheap filter before the expensive one.

Exercise 2 — Explain the port inspector analogy in your own words. Without using the word "inspector," explain in two sentences why reviewing the plan before merging is different from reviewing the infrastructure code after it's already been applied.

See solution

A complete answer sounds, roughly, like this: "Reviewing the plan before merging is like reviewing the cargo manifest before the ship sails — there's still time to fix something at no cost. Reviewing the infrastructure after it's applied is like opening the hold at the destination and discovering something shouldn't be there — by then, it's already sailed, and fixing it costs much more than catching it in time."

Exercise 3 — Anticipate why ci.yml is separated from apply.yml. Without having read lesson 2 yet, propose a security reason (not a file-organization one) why it would be dangerous for a single workflow, triggered by pull_request, to have both the plan and the apply in the same file, with a condition deciding which one runs.

See solution

A Pull Request can come from anyone with permission to propose changes —including someone outside the core team, in a repository with external collaborators. If the apply lives in the same file as the plan, triggered by the same event (pull_request), the only barrier between "someone proposed a change" and "that change was actually applied" is a condition inside the YAML —code that, in theory, could also be manipulated from the very Pull Request being evaluated. Separating apply.yml into a different file, triggered only by push to main (an event that only happens after someone with permission merged the change), eliminates that ambiguity at the root: there's no condition deciding whether the apply runs, there's a completely different event that doesn't even exist until the merge has already happened. Lesson 2 develops this in depth, cited from HashiCorp's official tutorial.


Summary and next step

In this lesson you saw the complete map of this module's 8 lessons, and the central idea organizing all of them: the plan —not the apply— is the point where a human reviews an infrastructure change before it becomes irreversible. The quality-control-before-boarding analogy —reviewing the cargo manifest before the ship sails, not after it arrives— is the idea you're going to see applied, in real YAML, in every lesson that follows.

Before moving on you should be able to: name this module's 8 lessons and what each one builds; explain why "CI for Terraform" never means "automatic apply"; and anticipate, even in general terms, why separating plan from apply into two distinct workflows is a security decision.

Lesson 2 opens with the exact pattern organizing all of this — quoted, word for word where it applies, from HashiCorp's official tutorial for automating Terraform with GitHub Actions.

Resources

  1. HashiCorp Developer — Automate Terraform with GitHub Actions — the official tutorial defining the pattern this module implements, gone into depth in lesson 2.
  2. GitHub Docs — Understanding GitHub Actions — a review of a workflow's anatomy, already covered in depth in Module 2.
  3. terraform-and-iac-guide (NIEVA) — the andes-cargo-infra/ project this module automates without rewriting, and the manual plan/apply cycle this module replaces with a pipeline.