Splitting It Into desired / actual / plan
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.
- It takes a little over a minute to start. This is because the VM boots and installs Docker. It is slower than Pod labs (usually 40 seconds).
- There is no browser preview. Only one grading port is open for connections coming into the VM.
If you started a web server, check it with
curlinside the VM.
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
- Declare the desired state in
/root/iac1/desired.yaml. Put acontainers:line at the top level and 2 entries under it. Their names areiac-webandiac-cacherespectively, and write oneimage:line for each entry (exactly 2image:lines in the whole file). Choose images only from those already on the Pod:alpine:3.20,busybox:1.36,python:3.12-alpine, andnginx:1.27-alpine. - Create
/root/iac1/parse.shto convert that YAML into/root/iac1/desired.json. The JSON shape is{"containers":[{"name":"...","image":"..."}, ...]}with 2 elements, and theimagevalue must not be empty. - 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 bothnameandimage. The managed containers are only those whose names start withiac-, and the image strings must be normalized to the same notation as desired by stripping thelocalhost/ordocker.io/library/prefix. - 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 printsno changesand exits with code 0. If there are differences, it exits with code 2 (0 = no changes, 1 = error, 2 = changes). - Create
/root/iac1/apply.shto bring the actual state in line with the declaration. When it finishes, both containers listed in desired must berunning. - Without changing anything, run the plan again and save it to
/root/iac1/plan2.txt. This file must containno changesand must have no lines starting with+,-, or~. - 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. - Run
apply.shagain to converge, and save the plan after that to/root/iac1/plan3.txt. It must showno changes, and both containers must berunning.
Notes
- The point of this lab is not the quality of the parser but the three-part split of desired/actual/plan. Parsing the YAML at the level of
grep/sedis enough. nginx:1.27-alpinekeeps running if left alone, butalpine:3.20exits immediately. Hold it open likedocker run -d --name iac-cache alpine:3.20 tail -f /dev/null.- Grading item 6 keeps only
{name,image}from desired.json and the actual.sh output, sorts them, and compares them as they are. If the image notation differs by even one character, it fails. - Common mistakes: actual.sh picking up containers from other labs that do not start with
iac-, leaving out the space between the symbol and the word in the plan output, and deleting two in step 07 so that there are two change lines.
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.