TT Lab
Get started
Learn Learning paths Courses

Infrastructure as Code

Splitting It Into desired / actual / plan

Continue in TT Lab

This lab runs on a real VM

This box is not a Pod but a virtual machine launched by KubeVirt. A Linux kernel runs separately, systemd actually manages services, and docker is not an imitation but a real Docker engine. A container started with docker run actually becomes a process, and docker exec and docker logs work as they should.

This lab used to run inside a Pod. Because it was a box with all kernel privileges dropped, the step of starting containers was blocked, and so you learned through a workaround of unpacking image archives by hand. The workaround is no longer needed.

There are two things to know.

Goal

Build for yourself the three-part structure of desired state, actual state (current), and their difference (plan), and confirm how drift is detected and converged.

Why it matters

The + create, - destroy, and ~ update that IaC tools print on screen are not magic but the result of comparing two states. If you build this structure by hand once, your eye for reading a plan changes. In automation in particular, the result has to be received as an exit code, so follow the convention as it is: 0 means no changes, 1 means an error, and 2 means there are changes. In terms of the three-axis model, the code is Desired, the state file is Last Known, and the actual infrastructure is Actual, and if even two of the three diverge, that is drift. GitOps self-healing is, in the end, just running this comparison repeatedly.

Steps

  1. Declare the desired state in /root/iac1/desired.yaml. Put a containers: line at the top level and 2 entries under it. Their names are iac-web and iac-cache respectively, and write one image: line for each entry (exactly 2 image: lines in the whole file). Choose images only from those already on the Pod: alpine:3.20, busybox:1.36, python:3.12-alpine, and nginx:1.27-alpine.
  2. Create /root/iac1/parse.sh to convert that YAML into /root/iac1/desired.json. The JSON shape is {"containers":[{"name":"...","image":"..."}, ...]} with 2 elements, and the image value must not be empty.
  3. Create /root/iac1/actual.sh. It outputs the managed containers that are actually running right now, as JSON of the same shape, to standard output. Each element must have both name and image. The managed containers are only those whose names start with iac-, and the image strings must be normalized to the same notation as desired by stripping the localhost/ or docker.io/library/ prefix.
  4. Create /root/iac1/plan.sh <desired.json> <actual.json>. It prints one line each: + create <이름> if it is only in desired, - destroy <이름> if it is only in actual, and ~ update <이름> if it is on both sides but the image differs (the placeholder is the name). If there is no difference at all, it prints no changes and exits with code 0. If there are differences, it exits with code 2 (0 = no changes, 1 = error, 2 = changes).
  5. Create /root/iac1/apply.sh to bring the actual state in line with the declaration. When it finishes, both containers listed in desired must be running.
  6. Without changing anything, run the plan again and save it to /root/iac1/plan2.txt. This file must contain no changes and must have no lines starting with +, -, or ~.
  7. Shake the state from outside the code. Delete one container with docker rm -f iac-cache, then run the plan and save it to /root/iac1/drift.txt. There must be exactly one change line, and that line must be + create iac-cache.
  8. Run apply.sh again to converge, and save the plan after that to /root/iac1/plan3.txt. It must show no changes, and both containers must be running.

Notes

Declare the desired state

Declare the desired state in /root/iac1/desired.yaml. Put a containers: line at the top level and 2 entries under it. Their names are iac-web and iac-cache respectively, and write one image: line for each entry (exactly 2 image: lines in the whole file). Choose images only from those already on the Pod: alpine:3.20, busybox:1.36, python:3.12-alpine, and nginx:1.27-alpine.

At the top level of /root/iac1/desired.yaml, put a containers: line and 2 entries under it. The names are exactly iac-web and iac-cache, with one image: line per entry (2 lines in total). Because it is offline, use only images that are already present.

Put the declaration in a machine-readable format

Create /root/iac1/parse.sh to convert that YAML into /root/iac1/desired.json. The JSON shape is {"containers":[{"name":"...","image":"..."}, ...]} with 2 elements, and the image value must not be empty.

You do not need a YAML parser. Extract the name and image values with grep/sed/awk and use jq -n to assemble {containers:[...]}. The /root/iac1/desired.json must have 2 elements, and image must not be empty.

Read the current state

Create /root/iac1/actual.sh. It outputs the managed containers that are actually running right now, as JSON of the same shape, to standard output. Each element must have both name and image. The managed containers are only those whose names start with iac-, and the image strings must be normalized to the same notation as desired by stripping the localhost/ or docker.io/library/ prefix.

actual.sh outputs JSON of the same shape as desired to standard output. The managed containers are only those whose names start with iac-. Strip the localhost/ and docker.io/library/ prefixes that podman adds, so the notation matches desired.

Calculate the difference

Create /root/iac1/plan.sh <desired.json> <actual.json>. It prints one line each: + create <이름> if it is only in desired, - destroy <이름> if it is only in actual, and ~ update <이름> if it is on both sides but the image differs (the placeholder is the name). If there is no difference at all, it prints no changes and exits with code 0. If there are differences, it exits with code 2 (0 = no changes, 1 = error, 2 = changes).

plan.sh <desired.json> <actual.json> prints + create <이름> if it is only in desired, - destroy <이름> if it is only in actual, and ~ update <이름> if the image differs (the placeholder is the name). There is a space between the symbol and the word. If there is no difference, it prints no changes with exit code 0.

Create the declared state

Create /root/iac1/apply.sh to bring the actual state in line with the declaration. When it finishes, both containers listed in desired must be running.

apply.sh only needs to execute what the plan says. For an image that exits immediately, such as alpine:3.20, you must give it a command like tail -f /dev/null so that it stays in the running state.

No change even when applied again

Without changing anything, run the plan again and save it to /root/iac1/plan2.txt. This file must contain no changes and must have no lines starting with +, -, or ~.

With nothing changed, run the plan again and save it to /root/iac1/plan2.txt. It must contain only no changes and have no lines starting with +, -, or ~. Grading compares the desired.json and actual.sh results again, so it fails even if the image notation differs by one character.

Catch a change made from outside

Shake the state from outside the code. Delete one container with docker rm -f iac-cache, then run the plan and save it to /root/iac1/drift.txt. There must be exactly one change line, and that line must be + create iac-cache.

After shaking the state from outside the code with docker rm -f iac-cache, save the plan to /root/iac1/drift.txt. There must be exactly one change line, + create iac-cache. If you deleted only one but several lines appear, it means actual.sh is choosing the managed containers wrongly.

Automatic convergence

Run apply.sh again to converge, and save the plan after that to /root/iac1/plan3.txt. It must show no changes, and both containers must be running.

Save the plan after applying again to /root/iac1/plan3.txt. If it shows no changes and both containers are running, you have reproduced by hand the self-healing that a GitOps controller does.