Module 1: Why Cicd And Gitops

8. Project: bootstrapping Andes Cargo's pipeline

Description

This is the project that closes Module 1. You're not going to write any real Andes Cargo workflow yet —that starts in Module 2— but you are going to prepare, with real, verified structure, exactly the same andes-cargo-infra/ repository you left finished in terraform-and-iac-guide to receive its first pipeline. By the end of this lesson, you're going to have that project initialized as a Git repository (if it wasn't already), with .github/workflows/ created and ready to receive its first file, an .actrc that already knows the correct runner image, and LocalStack running on your host machine — everything ready for Module 2 to write the first workflow that actually touches infrastructure.

Connection to the module

Lessons 6 and 7 gave you the tool and the complete cycle, tested on a disposable repository with no connection to Andes Cargo. This project uses that same knowledge, but on the real, permanent repository you're going to use all the way through Module 8's capstone. Module 2 opens with this same andes-cargo-infra/ already existing, and adds the first real workflow — nothing you prepare here gets redone.


Starting point: what andes-cargo-infra/ already brings

If you completed terraform-and-iac-guide, your andes-cargo-infra/ folder already exists, with the layout that guide left finished in its capstone — this project doesn't rewrite a single line of that HCL:

   andes-cargo-infra/                          — INHERITED, unchanged

   versions.tf                                  provider AWS ~> 6.0
   providers.tf                                 minimal, ready for tflocal
   variables.tf / locals.tf / outputs.tf         parameterization
   terraform.tfvars / dev.tfvars                 per-environment values
   .gitignore                                    protects *.tfstate, *.tfvars, etc.

   iam.tf                                        LambdaManifestProcessorRole, AppServerRole
   s3.tf                                          andes-cargo-shipment-docs
   lambda.tf                                      process-shipment-manifest
   dynamodb.tf                                    Shipments (PK shipmentId)

   modules/
   ├── s3-bucket/                                 reusable module
   └── iam-role/                                  reusable module

   .terraform.lock.hcl                            generated by terraform init
   terraform.tfstate                              generated by terraform apply (NEVER versioned)

What this lesson adds is exclusively at the pipeline layer, marked below with ✚ — the rest of the table already existed before you opened this guide:

   andes-cargo-infra/                          — WHEN IT'S ADDED

   (everything above)                            terraform-and-iac-guide

 ✚ .git/                                        Module 1 (this lesson, if it didn't exist)
 ✚ .actrc                                       Module 1 (this lesson)
 ✚ .github/workflows/                           Module 1 (this lesson, empty for now)

   .github/workflows/ci.yml                     Module 3
   .github/workflows/apply.yml                  Module 5
   .github/workflows/drift.yml                  Module 5
   .github/act-events/*.json                    Module 2
   .secrets                                     Module 2 (gitignored from that lesson on)

Step 1 — Confirm (or initialize) the Git repository

Stand in andes-cargo-infra/ and check whether it's already a Git repository:

cd andes-cargo-infra
git status

If your project is already a Git repository from terraform-and-iac-guide (something that guide didn't require, but that many people do out of habit), you're going to see your branch's normal status. If it's not one yet, you're going to see this real error:

What to expect (literal, if the project isn't a Git repository yet):

fatal: not a git repository (or any of the parent directories): .git

In that case, initialize it, pinning main as the default branch —the exact name every other workflow in this guide assumes in its on: push: branches: [main] block—:

git init -b main

What to expect (literal):

Initialized empty Git repository in /path/to/andes-cargo-infra/.git/
git status

What to expect (literal) — every file inherited from terraform-and-iac-guide shows up as "untracked," because the repository was just created and doesn't have any commits yet:

On branch main

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
	.gitignore
	dynamodb.tf
	iam.tf
	lambda.tf
	locals.tf
	modules/
	outputs.tf
	providers.tf
	s3.tf
	variables.tf
	versions.tf

nothing added to commit but untracked files present (use "git add" to track)

Notice a detail worth understanding now, not discovering by accident later: terraform.tfvars and dev.tfvars don't show up in this list, even though they exist in the folder. That's not an error — it's the .gitignore inherited from terraform-and-iac-guide doing exactly its job: its *.tfvars line is already protecting those files from the very first git status, even before your first commit in this repository.


Step 2 — .actrc, now in the real project

In lesson 6 you created .actrc in a disposable lab. Now repeat that exact same line, but at the root of andes-cargo-infra/ — because, as you already know, act looks for this file in the current working directory, not globally:

-P ubuntu-latest=catthehacker/ubuntu:act-latest

This file is going to grow once more in Module 3 (lesson 5), when you add a second line —a --container-options that lets the job's container reach the LocalStack running on your host—. For now, this single line is enough: it keeps pinning the runner image, avoiding lesson 6's interactive prompt, exactly like in your lab.


Step 3 — Create .github/workflows/

mkdir -p .github/workflows

This command produces no output if it works — confirm it by listing the structure:

find . -maxdepth 1

What to expect (literal, with .git/ already initialized and .actrc already created):

.
./.actrc
./.git
./.github
./.gitignore
./dev.tfvars
./dynamodb.tf
./iam.tf
./lambda.tf
./locals.tf
./modules
./outputs.tf
./providers.tf
./s3.tf
./terraform.tfvars
./variables.tf
./versions.tf

.github/workflows/ is still completely empty —it doesn't even show up as a separate entry in git status, because Git, unlike a regular file system folder, doesn't track empty directories — only files. It's going to show up in your next git status only once Module 2 adds the first .yml file inside it. That's not an error or something you need to force now: it's, literally, "ready to receive content," not "content already existing."

Confirm act already recognizes this project, even though it doesn't have any workflow yet:

act -l

What to expect (literal) — only the header, no rows, no errors, and no interactive prompt (because .actrc already resolved which image to use):

Stage  Job ID  Job name  Workflow name  Workflow file  Events

Step 4 — Commit the pipeline layer's bootstrap

git add -A
git commit -m "Bootstrap CI/CD layer: initialize git repository, .actrc, and .github/workflows/"

What to expect (literal):

[main (root-commit) ce6efa5] Bootstrap CI/CD layer: initialize git repository, .actrc, and .github/workflows/
 12 files changed, ...

The commit hash (ce6efa5 in this run) is, like the sha you saw in lesson 7, variable — yours is going to be different, because it depends on the exact content of your files and the exact moment of the commit. What doesn't vary is the structure: twelve files inherited from terraform-and-iac-guide plus .actrc, all entering Git's history for the first time in this same commit.

git log --oneline

What to expect (representative for the hash, literal for the structure) — a single commit, this repository's first:

ce6efa5 Bootstrap CI/CD layer: initialize git repository, .actrc, and .github/workflows/
git status

What to expect (literal):

On branch main
nothing to commit, working tree clean

Step 5 — LocalStack, started clean on the host

terraform-and-iac-guide (Module 1, lesson 5) already taught you, in full detail, why LocalStack's Hobby plan doesn't persist resources between container restarts, and why this guide family doesn't assume your previous session is still alive. This lesson doesn't re-explain that mechanism — it just repeats the startup, exactly like back then, because the rest of this guide (from Module 3 onward) needs LocalStack running on your host while act runs the jobs in its own separate containers.

Export your token and start the container:

export LOCALSTACK_AUTH_TOKEN=<YOUR_AUTH_TOKEN>
docker run \
  --rm -d \
  --name localstack_main \
  -p 127.0.0.1:4566:4566 \
  -p 127.0.0.1:4510-4559:4510-4559 \
  -e LOCALSTACK_AUTH_TOKEN=${LOCALSTACK_AUTH_TOKEN:?} \
  -v /var/run/docker.sock:/var/run/docker.sock \
  localstack/localstack

If you're missing the token (you left it empty, or didn't export it in this shell session), the container starts and shuts down within seconds with this message —reproduced literally, verified today against LocalStack's current image, the same error terraform-and-iac-guide documented—:

LocalStack version: 2026.7.3
LocalStack build date: 2026-08-12
LocalStack build git hash: 8f10c66d8

Localstack returning with exit code 55. Reason: 
===============================================
License activation failed! 🔑❌

Reason: No credentials were found in the environment. Please make sure to either set the LOCALSTACK_AUTH_TOKEN variable to a valid auth token. If you are using the CLI, you can also run `localstack auth set-token`.

Due to this error, Localstack has quit. LocalStack pro features can only be used with a valid license.

With the token correctly exported, check the logs until you see the full-startup signal:

docker logs -f localstack_main

What to expect (representative) — same format confirmed by the two previous guides, with no live run against a valid token at this moment; the Ready. line is the fixed signal that confirms startup:

LocalStack version: 2026.7.3
LocalStack build date: 2026-08-12

Starting LocalStack on host 0.0.0.0 ...
Waiting for all LocalStack services to be ready
Ready.

Exit the log follow with Ctrl+C (the container keeps running), and confirm your identity inside the lab, with exactly the command you already know:

awslocal sts get-caller-identity

What to expect (representative, same format already verified in the two previous guides):

{
    "UserId": "AKIAIOSFODNN7EXAMPLE",
    "Account": "000000000000",
    "Arn": "arn:aws:iam::000000000000:root"
}

"Account": "000000000000" — the same fixed account ID as always, the same one you're going to see, now too, in the output of every terraform plan you run inside an act job from Module 3 onward.


Closing Module 1

You completed the module that opens this guide. Review what you're taking with you:

  • The problem, precisely named: a manual apply leaves no record of who ran it, exposes long-lived credentials on a personal laptop, and depends on a specific person being available — revisited with the Claude Code destroy incident from a new angle: a pipeline doesn't prevent negligence, but it does make it auditable (lesson 2).
  • The vocabulary, with no ambiguity: CI (does it break anything?), CD-delivery (is it ready, and who says yes?), and CD-deployment (is it already live?) — three things, one acronym (lesson 3). GitOps as a principle —four properties, not a tool— with an exact origin at Weaveworks, 2017 (lesson 4).
  • The tool, with market honesty: GitHub Actions chosen for zero friction and for act, without pretending it "wins" over Jenkins, which dominates the surveyed Spanish stack 5 of 13 against ~3 of 13 (lesson 5).
  • The lab, installed and verified: act 0.2.89 running on your machine (lesson 6), connected to Docker, with a first complete workflow run end to end and two real errors already recognized (lesson 7).
  • The project prepared: andes-cargo-infra/, with Git initialized, .actrc pinned, .github/workflows/ created and empty, and LocalStack running — ready for its first real workflow.

What comes next

Module 2 takes this same andes-cargo-infra/ and dissects the complete syntax of a GitHub Actions workflow: on/jobs/steps/runs-on/uses/with/env, on a real workflow, not an isolated fragment. You're going to learn to simulate events with act -e —writing your own pr-event.json with a fixed PR number, with no need for a real GitHub account— and to pass secrets with act --secret-file .secrets. That module's project is the first workflow that actually touches Andes Cargo: hello-andes-cargo.yml, with a step that confirms, from inside act's container, that it can reach the LocalStack running on your host.


Common mistakes

Writing a real Andes Cargo workflow in this lesson, "to get ahead" (flow-based). What happens: someone, with lesson 7's cycle still fresh, directly adds a .yml file with a terraform plan step inside .github/workflows/, skipping the rest of this module and Module 2. Why it happens: the impulse of "I already know how to do this, why wait?" is understandable after lesson 7. How to spot it: if your .github/workflows/ already has a file before finishing Module 2. How to fix it: nothing serious happens technically, but you're going to skip a workflow's complete anatomy, how to simulate events with no GitHub account, and how to pass secrets safely — all content Module 2 builds, on purpose, before your first real Andes Cargo workflow exists.

Assuming andes-cargo-infra/ needs to become a new, separate repository (conceptual). What happens: someone creates a separate folder, andes-cargo-infra-pipeline/ or similar, for this guide, instead of using the exact folder terraform-and-iac-guide left finished. Why it happens: it feels "cleaner" to separate one guide's work from the other's. How to spot it: if you have two different folders with similar names, each with part of the project. How to fix it: this guide's entire design assumes it's the same repository, no exceptions — terraform-and-iac-guide's HCL and this guide's YAML have to coexist in the same folder, because the pipeline you're going to build automates exactly that HCL. If you separated the folders, move all the content back into one before continuing.

Worrying because .github/workflows/ "doesn't show up" in git status after creating it (expectation-based, clarified in Step 3). What happens: someone creates the empty folder, runs git status, and doesn't see it listed — and assumes something went wrong. Why it happens: on most file systems, a folder "exists" regardless of whether it has content; Git doesn't work that way. How to spot it: if you expected to see .github/ in the list of untracked files, and it isn't there. How to fix it: there's nothing to fix — Git only tracks files, never empty directories by themselves. The folder is going to show up in your next git status as soon as Module 2 adds the first .yml file inside it.


Exercises

Exercise 1 — Explain why terraform.tfvars doesn't show up in git status. Without looking at this lesson, explain to a colleague why, when running git status for the first time in a freshly initialized andes-cargo-infra/, the terraform.tfvars and dev.tfvars files don't show up in the list of untracked files, even though they exist in the folder.

See solution

The .gitignore inherited from terraform-and-iac-guide includes the line *.tfvars, which tells Git to completely ignore any file with that extension — Git doesn't even consider them "untracked," it excludes them directly from any listing or possibility of git add, unless explicitly forced with git add -f. This is intentional and correct: .tfvars files usually contain environment-specific values that shouldn't travel into a shared repository's history.

Exercise 2 — Predict act -l's result at two different moments. What do you expect act -l to show in andes-cargo-infra/ at the end of this lesson (Module 1), and what do you expect it to show at the end of Module 2, after you add hello-andes-cargo.yml? Justify the difference.

See solution

At the end of this lesson: only the table header (Stage Job ID Job name Workflow name Workflow file Events), with no rows — .github/workflows/ exists, but it's empty, so there's no job for act to list. At the end of Module 2: a new row, describing hello-andes-cargo.yml's job, with its name, the file it lives in, and the event that triggers it. The difference isn't a change in act's behavior — it's simply that real content now exists inside the folder that was previously empty.

Exercise 3 — Justify git init -b main instead of plain git init. A colleague asks you why this lesson specifically uses git init -b main, instead of the plain git init you used in other guides. Explain the reason to them in two or three sentences.

See solution

A complete answer sounds, roughly, like this: "Depending on the Git version and each machine's configuration, plain git init can create the initial branch with the name master instead of main. Every workflow in this guide —ci.yml, apply.yml, drift.yml— explicitly assumes main as the main branch's name in its on: push: branches: [main] block; if the real repository used master, none of those triggers would ever fire. Explicitly pinning the branch name with -b main at initialization time avoids that mismatch from the start, instead of discovering it when a Module 3 workflow simply doesn't run."


Summary and next step

In this project you prepared andes-cargo-infra/ to receive its first pipeline: you confirmed (or initialized) the Git repository with main as the default branch, created .actrc with the runner image pinned, and created .github/workflows/ —empty, ready for its first file. You confirmed with git log and git status that the bootstrap got committed, and started LocalStack on your host, reproducing the same honesty pattern as the two previous guides: literal where it doesn't depend on a token, representative where it does, never made up.

Before moving on you should be able to: explain from memory what this lesson adds on top of what andes-cargo-infra/ already brought from terraform-and-iac-guide; justify why .github/workflows/ doesn't show up in git status while empty; and say, without hesitation, why the main branch's name has to be main, not master, for the rest of this guide to work.

With this, Module 1 is closed. You have the problem understood, the vocabulary precise, the tool installed and tested, and the real project prepared for its first workflow.

Next module: the complete anatomy of a GitHub Actions workflow — you're going to dissect on/jobs/steps/runs-on/uses/with/env on a real workflow, you're going to simulate events with no GitHub account using act -e, and that module's project is the first workflow that actually touches Andes Cargo.

Resources

  1. Git Docs — git init — official documentation, including the -b option to pin the initial branch's name.
  2. nektosact.com — User Guide — reference for act -l, used in this lesson to confirm the empty project.
  3. LocalStack Docs — Auth Token — the source of the credentials error reproduced in this lesson.
  4. terraform-and-iac-guide, Module 1, lesson 5 (05-hands-on-installing-terraform-and-a-clean-start.md) — the complete explanation of LocalStack's clean startup, assumed and not repeated here.