Easier to read with cream paper, a tint, more contrast or larger text? Choose your colours and text, once, for every page.

Free lesson

A pipeline as code

After this lesson: I can write a pipeline as stages that stop at the first failure.

60 min lesson12 commandsOpenTofu lab: infrastructure as code with plan, apply and state on one VM

The broken change on Friday

A pipeline as code is a script or file that runs the same checks, in the same order, for every change to a project. It matters because it stops at the first check that fails, so a bad change never reaches the apply.

Somebody edits a file at four o'clock on Friday, pushes it and goes home. Nobody ran the checks, and the next apply fails halfway.

By the end of this lesson

Note

I can write a pipeline as stages that stop at the first failure.

Key words

WordWhat it meansExample
Pipelinesteps that run in a fixed order
Stageone step of a pipeline
Exit codethe number a command returns, where 0 means successecho $?
Gatea stage that stops the pipeline when a check fails
Plan filea saved plan that apply carries out exactlyplan.bin

The cells students fill in are left blank in this preview.

Every command ends with an exit code

When a command finishes it returns a number, its exit code. Zero means it worked. Any other number means it did not.

The shell keeps the last one in $?. A pipeline reads exit codes and nothing else, so a check that exits with a non-zero number is a check that stops the run.

Make a folder for the pipeline

Command
mkdir -p ~/iac-demo/pipe && cd ~/iac-demo/pipe

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write a project with one resource

Command
printf '%s\n' 'variable "shop" {' '  type    = string' '  default = "Corner Shop"' '}' '' 'resource "terraform_data" "door" {' '  input = format("%s: open", var.shop)' '}' > main.tf

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Prepare the folder

Command
tofu init -no-color | grep successfully

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write a second file with untidy spacing

Command
printf '%s\n' 'resource "terraform_data" "sign" {' '    input = "Open"' '}' > sign.tf

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Check the layout and read the exit code

Command
tofu fmt -check; echo "exit code $?"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

A pipeline stops at the first failure

A script can run each stage and stop when one fails. Here one small function, stage, runs a command and exits if it returns anything but zero.

The stages go in the order you would run them by hand: format, validate, plan, gate, apply. A failed stage means the later ones never start.

Write the gate

Command
printf '%s\n' '[.resource_changes[].change.actions[]] | index("delete") | not' > gate.jq

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Write the pipeline script

Command
printf '%s\n' '#!/bin/bash' 'stage() { echo "== stage: $1"; name=$1; shift; "$@" > /dev/null || { echo "stage $name failed, stopping"; exit 1; }; }' 'gate() { tofu show -json plan.bin | jq -e -f gate.jq; }' 'stage format tofu fmt -check -no-color' 'stage validate tofu validate -no-color' 'stage plan tofu plan -no-color -out=plan.bin' 'stage gate gate' 'stage apply tofu apply -no-color plan.bin' 'echo "pipeline passed"' > ci.sh

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Run it while sign.tf is untidy

Command
bash ci.sh; echo "pipeline exit $?"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Fix the layout and run it again

Command
tofu fmt -no-color; bash ci.sh; echo "pipeline exit $?"

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

A common mistake

Warning

Many people think a green pipeline means the change is safe. It means every check you wrote passed. A change can be valid, planned and applied, and still be the wrong change. The gate and a person reading the plan are the safety.

A gate reads the plan before apply

The plan stage saved the plan in plan.bin, and the apply stage carries out that file and nothing else. So what the gate approved is what runs.

tofu show -json turns the plan into data. The gate filter says no when any resource is going to be deleted.

Delete a resource and run it

Command
rm sign.tf; bash ci.sh; echo "pipeline exit $?"; tofu state list

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

A hosted pipeline is a file

On a service that hosts pipelines, the stages are written in a file in the repository, and the service runs them on its own machine. A job can say needs to wait for another.

You cannot run one here, because there is no hosted runner. You can write one and read it.

Write a workflow file

Command
mkdir -p .github/workflows && printf '%s\n' 'name: infra' 'on:' '  pull_request:' '  push:' '    branches: [main]' 'jobs:' '  check:' '    runs-on: ubuntu-latest' '    steps:' '      - uses: actions/checkout@v4' '      - run: tofu fmt -check' '      - run: tofu validate' '      - run: tofu plan -out=plan.bin' '  apply:' '    needs: check' "    if: github.ref == 'refs/heads/main'" '    runs-on: ubuntu-latest' '    steps:' '      - run: tofu apply plan.bin' > .github/workflows/infra.yml

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

Find the order and the commands in it

Command
grep -nE "^  (check|apply):|needs:|run:" .github/workflows/infra.yml

Shown here, not run. Create a free account to press Run and type it into your own lab’s terminal.

A pipeline run worked out in Python

Code: python
# Run a pipeline over stages that each need earlier ones.
jobs = {"check": [], "plan": ["check"], "apply": ["plan"]}
failing = {"plan"}
state = {}
for name in ["check", "plan", "apply"]:
    blocked = False
    for need in jobs[name]:
        if state[need] != "passed":
            blocked = True
    if blocked:
        state[name] = "skipped"
    elif name in failing:
        state[name] = "failed"
    else:
        state[name] = "passed"
    print(name, state[name])
# Empty the failing set and look again.

About this lab and its source: OpenTofu lab: infrastructure as code with plan, apply and state on one VM

Teaching a class? Set this module for your students with a teacher guide, marked assignments and a lab for every student.