freedzhost.cloud

DevOps module 3: infrastructure as code, Kubernetes and secrets

Level IntermediateEstimated time 14 hRead first DevOps module 2: CI/CD pipelines and containersA word you don't know? Glossary

Fourteen sessions to stop configuring by hand: machines described in Ansible playbooks, infrastructure in OpenTofu files reviewed like code, an application run by Kubernetes, and secrets that never touch the repository. The module ends with hello-ops deployed three ways from code alone.

Everything runs on your own machine: Ansible against your VM (or a second one), OpenTofu driving your local Docker, Kubernetes inside Docker with kind. No cloud account is needed. Addresses are documentation ranges (192.0.2.0/24, 198.51.100.0/24).

Day 29 · 01/12 — Infrastructure as code: why, and its history

Goal: explain declarative, idempotent and drift, and place the main tools in time.

Theory 1/2: pets and cattle

A server configured by hand, nursed when ill and irreplaceable, is a pet; servers built from a description, replaced rather than repaired, are cattle. The image is credited to Bill Baker (Microsoft, mid-2000s) and was spread by Randy Bias around 2012. Infrastructure as code (IaC) is that description: text files, versioned in Git, reviewed in pull requests, applied by a tool. The machine then matches a file, not a memory.

A server breaks. In the "cattle" approach you…

  1. log in and repair it by hand
  2. replace it with a new one built from the code
Answer

Rebuild from the description; the code is the reference.

Theory 2/2: declarative, idempotent, drift

Imperative: the steps ("install nginx, then copy the file"). Declarative: the desired state ("nginx is installed, this file has this content"); the tool works out the steps. Idempotent: applying it twice gives the same result as once; the second run changes nothing. Drift: the real system no longer matches the code, because someone changed it by hand.

A little history: CFEngine (Mark Burgess, 1993), Puppet (2005), Chef (2009), Salt (2011), Ansible (Michael DeHaan, 2012, bought by Red Hat in 2015) manage what is on machines; Terraform (HashiCorp, 2014) and its open fork OpenTofu manage the machines, networks and services themselves.

You run the same playbook twice; the second run reports 0 changes. The playbook is…

  1. idempotent
  2. broken
Answer

Idempotent: the state was already the desired one.

What is the name for the gap between the code and the real system after manual changes?

Answer

Drift.

echo "line" >> /etc/app.conf run twice is…

  1. idempotent
  2. not idempotent: the line is added twice
Answer

Appending is not idempotent; "this line is present" is.

Practice (30 min)

  1. List in your notebook everything you did by hand on your VM since day 1 (packages, users, files, services). That list is what IaC would describe.
  2. Rewrite three of those items as desired states ("package X is present"), not steps.
  3. Install Ansible as the installation guide recommends (for example pipx install --include-deps ansible), and check ansible --version.

With your AI: "Give me eight shell commands one at a time and ask me, for each, whether it is idempotent and how to make it idempotent if it is not. Correct me with one sentence each. Start with mkdir, echo >>, useradd and sed -i."

Recap (10 min)

Three lines: pets and cattle, declarative and idempotent, drift. Then place CFEngine, Puppet, Ansible and Terraform on a timeline from memory.

Day 30 · 02/12 — Ansible 1: inventory and ad-hoc commands

Goal: reach machines with Ansible and run modules on them.

Theory 1/2: agentless, over SSH

Ansible installs nothing on the targets: it connects over SSH (day 9), copies small Python programs called modules, runs them and reads their result. The inventory lists the machines, in groups:

# inventory.ini
[web]
web1 ansible_host=192.0.2.21

[local]
localhost ansible_connection=local

What does Ansible need on a target machine?

  1. An Ansible agent running
  2. SSH and Python
Answer

Agentless: SSH access and Python on the target.

Theory 2/2: modules and ad-hoc commands

An ad-hoc command runs one module on a group, without a file. Modules are named by collection: ansible.builtin.ping, ansible.builtin.apt, ansible.builtin.copy… Each reports ok (nothing to do) or changed.

ansible all -i inventory.ini -m ansible.builtin.ping
ansible local -i inventory.ini -m ansible.builtin.setup -a "filter=ansible_distribution*"
ansible local -i inventory.ini -b -m ansible.builtin.apt -a "name=tree state=present"

-b ("become") runs with sudo; -K asks for its password.

Which option makes an Ansible command run with privilege escalation?

Answer

-b (--become).

You run the apt command above twice. The second time it says…

  1. ok, because tree is already present
  2. changed again
Answer

state=present is a desired state: nothing to do the second time.

Practice (30 min)

  1. Create ~/lab/ansible/inventory.ini with a local group (and a web group if you have a second VM reachable by SSH key).
  2. Run ping, then setup with the filter: note the distribution Ansible sees.
  3. Install tree with the apt module and -b -K; run it again and compare changed and ok.
  4. Remove it with state=absent.
  5. Read ansible-doc ansible.builtin.apt and note two options you did not know.

With your AI: "I am learning Ansible ad-hoc commands. Give me six tasks (check uptime, create a folder, install a package, copy a file, restart a service, gather facts), one at a time; I answer with the command, you check the module name and arguments against ansible.builtin and tell me if I got them wrong."

Let an agent do it: an agent can run ad-hoc checks across an inventory and summarise (disk use, kernel version). Ask it to show each command. You still check one host by hand, and you never let it run -b with a state change without reading the command first.

Recap (10 min)

Three lines: agentless over SSH, the inventory, ok versus changed. Then redo the ping and apt commands without notes.

Day 31 · 03/12 — Ansible 2: playbooks, idempotence, handlers

Goal: write a playbook that prepares a host for hello-ops, and preview its effect before applying.

Theory 1/2: a playbook

# site.yml
- name: Prepare an app host
  hosts: local
  become: true
  tasks:
    - name: Install packages
      ansible.builtin.apt:
        name: [curl, ca-certificates]
        state: present
        update_cache: true

    - name: Create the app user
      ansible.builtin.user:
        name: app
        system: true
        create_home: false

    - name: Create the app folder
      ansible.builtin.file:
        path: /srv/app
        state: directory
        owner: app
        mode: "0750"

A play targets hosts; its tasks call modules in order. Run it with ansible-playbook -i inventory.ini site.yml -K; the recap at the end counts ok, changed, failed.

Why quote "0750"?

  1. Style only
  2. Without quotes YAML may read it as a number and the mode comes out wrong
Answer

Give modes as quoted strings; the Ansible docs warn about it.

Theory 2/2: check, diff, handlers

--check simulates (nothing is changed) and --diff shows what would change in files: run both before every real apply. A handler runs only when a task notify-ed it and that task changed something: restart a service only if its configuration changed.

    - name: Install the unit file
      ansible.builtin.copy:
        src: hello-ops.service
        dest: /etc/systemd/system/hello-ops.service
        mode: "0644"
      notify: Restart hello-ops
  handlers:
    - name: Restart hello-ops
      ansible.builtin.systemd_service:
        name: hello-ops
        state: restarted
        daemon_reload: true

Which two options preview a playbook's changes without applying them?

Answer

--check --diff.

The unit file is unchanged on this run. Does the handler restart the service?

  1. No, the task reported ok, not changed
  2. Yes, every run
Answer

Handlers fire on change only.

Practice (30 min)

  1. Write site.yml with the three tasks; run it with --check --diff, then for real, then a second time: the second recap shows changed=0.
  2. Add a unit hello-ops.service that runs python3 /srv/app/app.py as user app, a task copying app.py to /srv/app, the unit task with its handler, and a task that enables and starts the service.
  3. Apply; curl http://127.0.0.1:8080/health. Change the unit's description, apply with --diff: the handler fires once.
  4. Commit the playbook in a new repository hello-ops-infra.

With your AI: "Review this Ansible playbook for idempotence and safety: [paste site.yml]. For each task, say whether a second run would report changed and why. Flag any shell or command module that could be replaced by a proper module. Answer as a table."

Let an agent do it: agents write playbooks fast, often with shell: tasks that are not idempotent. Instruction: "Add a task that installs and enables fail2ban using only ansible.builtin modules; no shell or command module; run with --check --diff and show me the output." You still check: run it twice for real and read the second recap (changed=0).

Recap (10 min)

Three lines: play and tasks, check and diff, handlers. Then rewrite the handler block from memory.

Day 32 · 04/12 — Ansible 3: variables, templates, roles

Goal: turn the playbook into a reusable role with a template and variables, and lint it.

Theory 1/2: variables and Jinja2 templates

Variables are set in the inventory, in group_vars/, in a role's defaults/, or on the command line; their order of precedence is documented, and role defaults are the weakest, so easy to override. A template is a file with Jinja2 placeholders, filled in by ansible.builtin.template:

# templates/hello-ops.service.j2
[Service]
User={{ app_user }}
Environment=PORT={{ app_port }}
ExecStart=/usr/bin/python3 {{ app_dir }}/app.py

Which module renders a Jinja2 file onto the target?

Answer

ansible.builtin.template.

Theory 2/2: roles, Galaxy, lint

A role packages tasks, handlers, templates, files and defaults in a fixed tree (ansible-galaxy role init hello_ops creates it), so a playbook only says roles: [hello_ops]. Ansible Galaxy shares roles and collections; read one before using it, like any third-party code. ansible-lint checks playbooks against good practices (full module names, named tasks, no needless shell).

Where do you put a role variable meant to be overridden easily?

  1. vars/main.yml
  2. defaults/main.yml
Answer

Defaults have the lowest precedence: anything else overrides them.

ansible-lint warns fqcn on apt:. What does it want?

  1. The full name ansible.builtin.apt
  2. A different package
Answer

Fully qualified collection names remove ambiguity.

Practice (30 min)

  1. ansible-galaxy role init roles/hello_ops; move your tasks, handler and unit into it; turn the unit into the template above, with app_user, app_port, app_dir in defaults/main.yml.
  2. Reduce site.yml to the play and roles: [hello_ops]; apply; then override app_port=8090 with -e and check with curl.
  3. Install and run ansible-lint; fix every finding.

With your AI: "Explain Ansible variable precedence with my case: app_port is in roles/hello_ops/defaults/main.yml (8080), in group_vars/local.yml (8085) and passed with -e (8090). Which value wins and why, and where would you put each kind of variable? Point me to the documentation page to verify."

Let an agent do it: converting a flat playbook into a role is mechanical work agents do well. Instruction: "Convert site.yml into a role hello_ops with defaults for every literal value; run ansible-lint and a --check --diff; show both outputs." You still check that the second real run shows changed=0 and that no value moved silently.

Recap (10 min)

Three lines: templates, a role's tree, ansible-lint. Then rewrite the template from memory. (The site's article Rebuild your machines in one command shows a real workstation role.)

Day 33 · 05/12 — OpenTofu 1: providers, plan, apply, state

Goal: describe a container in OpenTofu, preview it, create it and destroy it.

Theory 1/2: Terraform, the licence change and OpenTofu

HashiCorp released Terraform in 2014 under an open source licence (MPL 2.0). In August 2023 it moved its products to the Business Source License, which is not open source. Part of the community forked the last open version as OpenTofu, under the Linux Foundation; its first stable release, 1.6.0, came out in January 2024, and it joined the CNCF in 2025. The language (HCL) and the workflow are the same; the command is tofu instead of terraform. This path uses OpenTofu.

Why does OpenTofu exist?

  1. Terraform stopped working
  2. Terraform's licence changed to a non-open-source one in 2023, and the community forked the last open version
Answer

The BSL change of August 2023.

Theory 2/2: providers, resources, plan, state

A provider is a plugin that speaks to an API (a cloud, DNS, Docker…); a resource is one object it manages. The cycle: tofu init (download providers), tofu plan (compare the code with reality and show what will be created +, changed ~, destroyed -), tofu apply, tofu destroy. OpenTofu remembers what it created in a state file, terraform.tfstate: it may hold secrets, so it never goes in Git.

terraform {
  required_providers {
    docker = {
      source  = "kreuzwerker/docker"
      version = "~> 4.0"
    }
  }
}

provider "docker" {}

resource "docker_image" "app" {
  name         = "hello-ops:dev"
  keep_locally = true
}

resource "docker_container" "app" {
  name  = "hello-ops"
  image = docker_image.app.image_id
  ports {
    internal = 8080
    external = 8080
  }
}

Which command shows what would change, without changing anything?

Answer

tofu plan.

Should terraform.tfstate be committed?

  1. No: it can contain secrets, and it belongs in a protected backend
  2. Yes, like the code
Answer

Never in Git; add *.tfstate* and .terraform/ to .gitignore.

Practice (30 min)

  1. Install OpenTofu following the official guide; tofu version.
  2. In hello-ops-infra/tofu/, write main.tf above (check the provider's current version on the OpenTofu registry).
  3. tofu init, tofu plan (read every +), tofu apply, curl http://127.0.0.1:8080/health.
  4. Change the external port to 8081, tofu plan: does it update in place or replace? Apply, then tofu destroy.
  5. Create a .gitignore with .terraform/ and *.tfstate*, and commit (the .terraform.lock.hcl file is committed).

With your AI: "Here is the output of tofu plan: [paste it]. Explain each symbol and each resource line, then tell me which change forces a replacement and why. Under 15 lines."

Let an agent do it: an agent can write HCL for a resource you describe. You still read every line of tofu plan before any apply: an agent must never run apply or destroy unattended on infrastructure you care about.

Recap (10 min)

Three lines: why OpenTofu, init / plan / apply / destroy, what the state is. Then rewrite the container resource from memory.

Day 34 · 06/12 — OpenTofu 2: variables, outputs, modules, remote state

Goal: parameterise the configuration, package it as a module, and know where state lives in a team.

Theory 1/2: variables, outputs, modules

variable "image_tag" {
  type        = string
  description = "Image tag to deploy"
  default     = "dev"
}

output "url" {
  value = "http://127.0.0.1:${docker_container.app.ports[0].external}"
}

Values come from -var, a *.tfvars file or TF_VAR_image_tag. A module is a folder of .tf files called with module "name" { source = "./modules/app" … }: the same code deploys staging and production with different variables.

How do you pass image_tag on the command line?

  1. tofu apply image_tag=1.0
  2. tofu apply -var image_tag=1.0
Answer

-var name=value.

Theory 2/2: state in a team

Two people applying with two local state files corrupt each other's picture of reality. Teams keep the state in a remote backend (an object store, a database, a forge's backend) with locking, so only one apply runs at a time. OpenTofu can also encrypt the state and plan files itself (since version 1.7), a feature Terraform does not have.

What does state locking prevent?

  1. Two applies changing the same infrastructure at the same time
  2. Reading the code
Answer

Concurrent writes to the state.

Which command prints the outputs after an apply?

Answer

tofu output.

Practice (30 min)

  1. Add image_tag and external_port variables and the url output; apply with -var external_port=8082; tofu output url.
  2. Move the image and container into modules/app/, and call it twice from the root (staging on 8083, prod on 8084, different container names). Plan, apply, curl both, destroy.
  3. Read the state encryption page and note in your notebook how a key would be supplied without writing it in a file.

With your AI: "I have one OpenTofu module deployed twice (staging and prod) from the same root. Compare three ways to separate environments (two module calls, workspaces, two root folders) in a table: isolation of state, risk of applying to the wrong one, duplication. Mark anything you are unsure of."

Let an agent do it: an agent can extract a module from a flat configuration. Instruction: "Move these resources into modules/app without changing behaviour; run tofu fmt, validate and plan; the plan must show only moves, no destroy." You still read the plan: a refactor that destroys and recreates is not a refactor (use moved blocks).

Recap (10 min)

Three lines: variables and outputs, modules, remote state with locking. Then rewrite the variable block from memory.

Day 35 · 07/12 — Infrastructure as code in the pipeline, and prompting for it

Goal: check infrastructure code in CI like application code, and write a prompt for IaC you can verify.

Theory 1/3: the IaC pipeline

On a pull request: tofu fmt -check (format), tofu validate (syntax and references), tofu plan (the plan is posted for the reviewer), ansible-lint and a --check run. On merge to main: apply, with a human approval for production (an environment with required reviewers on GitHub). A scheduled tofu plan that should show "No changes" detects drift.

A nightly tofu plan shows one change nobody committed. What is it?

  1. A bug in OpenTofu
  2. A new feature
  3. Drift: someone changed the infrastructure by hand
Answer

Drift. Bring the change into the code, or undo it.

Which command fails if the files are not formatted?

Answer

tofu fmt -check.

Theory 2/3: prompting for infrastructure code

A weak prompt: "write terraform for a web server". What is wrong: no tool (Terraform or OpenTofu, which version?), no provider, no environment, no rule about what may be destroyed, no way to check. The model will pick a cloud you do not use, a provider version from its memory, and maybe a security group open to the world.

The edited prompt:

Role: you are an infrastructure engineer reviewing for safety.
Context: OpenTofu (tofu version prints the version), provider
kreuzwerker/docker ~> 4.0, local Docker, module in modules/app.
Task: add a variable "replicas" (number, default 1) and create that
many containers named hello-ops-0, hello-ops-1..., ports 8080 + index.
Constraints: no new provider; no change to existing names or the
plan must show no destroy; tofu fmt clean; descriptions on variables.
Inputs: modules/app/main.tf and variables.tf: [paste both].
Output: the changed files in full, then the expected tofu plan summary.
Check: I will run tofu fmt -check, tofu validate and tofu plan, and
compare the plan with your expected summary.

Why ask the model for "the expected plan summary"?

  1. To make the answer longer
  2. So you can compare it with the real tofu plan and catch a misunderstanding
Answer

A prediction you can check against the tool is a test of the answer.

Theory 3/3: fix this prompt

"Write an Ansible playbook to secure my server." Which addition matters most?

  1. The distribution and version, the exact measures wanted (SSH keys only, firewall rules), "ansible.builtin modules only, idempotent", and "I will run it with --check --diff first"
  2. "You are the best security expert in the world"
Answer

Context, constraints and the check; flattering roles add nothing.

Practice (30 min)

  1. In hello-ops-infra, add a workflow that runs on pull requests: tofu fmt -check, tofu init -backend=false, tofu validate, and ansible-lint. Install OpenTofu in the job as the install guide says for CI, or use its official container image.
  2. Open a pull request with a badly formatted .tf file: the check fails. Fix it with tofu fmt.
  3. Send the edited prompt above with your real files; run fmt, validate and plan on the answer; compare the plan with its prediction.

With your AI: "Here is my weak prompt: 'write a playbook that deploys my app'. Rewrite it with role, context, task, constraints, inputs, output format and check, for my real setup: Debian 13, role hello_ops, app.py in /srv/app, systemd. Then explain each line you added."

Let an agent do it: an agent can open the IaC pull requests itself, and even comment the plan. You still review the plan and approve the apply: the approval step exists precisely so a human reads what will change.

Recap (10 min)

Three lines: what runs on a pull request, what runs on merge, how drift is detected; and the parts of an IaC prompt. Then rewrite the weak web-server prompt properly.

Day 36 · 08/12 — Kubernetes 1: why orchestrate, the architecture

Goal: explain what an orchestrator does and name the parts of a Kubernetes cluster.

Theory 1/2: from Compose to an orchestrator

Compose runs containers on one machine. With many machines you need something that places containers, restarts them when they die, replaces a failed node, spreads traffic and rolls updates: an orchestrator. Google ran one internally, Borg; it announced Kubernetes (Greek for helmsman) in June 2014, released 1.0 in July 2015 and gave it to the new Cloud Native Computing Foundation (CNCF), where it was the first project to graduate (2018). Kubernetes is often written k8s.

Which internal Google system inspired Kubernetes?

  1. MapReduce
  2. Borg
Answer

Borg.

Theory 2/2: desired state and the architecture

You declare a desired state (three copies of this image); controllers compare it with reality, endlessly, and act to close the gap. The control plane: the API server (everything goes through it, kubectl included), etcd (the key-value store holding the state), the scheduler (picks a node for each Pod), the controller manager. Each node runs the kubelet (starts the Pods the API asks for, through a container runtime such as containerd) and kube-proxy (service networking).

Which component stores the cluster's state?

Answer

etcd.

A node dies with two Pods of a three-replica Deployment. What happens?

  1. The controller sees one replica instead of three and creates two on other nodes
  2. Nothing until an admin intervenes
Answer

The reconciliation loop restores the desired state.

Which component picks the node a new Pod runs on?

  1. kubelet
  2. etcd
  3. the scheduler
Answer

The scheduler; the kubelet then starts it.

Practice (30 min)

  1. Draw the architecture in your notebook: control plane (API server, etcd, scheduler, controller manager), two nodes (kubelet, kube-proxy, runtime, Pods), and kubectl talking to the API server.
  2. Do the first module of the official Kubernetes Basics tutorial.
  3. Install kubectl (guide) and kind; check kubectl version --client and kind version.

With your AI: "Quiz me on Kubernetes architecture: describe eight events (kubectl apply, a node failure, a Pod crash, a new Pod to place…), one at a time, and ask me which component acts first and what it does. Correct me with one sentence."

Recap (10 min)

Three lines: what an orchestrator does, desired state and reconciliation, the parts of the control plane. Then redraw the architecture without notes.

Day 37 · 09/12 — Kubernetes 2: a cluster with kind, Pods and Deployments

Goal: run hello-ops in a local cluster with three replicas.

Theory 1/2: Pod, ReplicaSet, Deployment

A Pod is the smallest unit: one or more containers sharing a network address. You rarely create Pods directly: a Deployment declares the image and the number of replicas, and manages a ReplicaSet that keeps that many Pods alive. Objects are YAML manifests applied with kubectl apply -f.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: hello-ops
spec:
  replicas: 3
  selector:
    matchLabels:
      app: hello-ops
  template:
    metadata:
      labels:
        app: hello-ops
    spec:
      containers:
        - name: app
          image: hello-ops:dev
          ports:
            - containerPort: 8080

You delete one Pod of this Deployment. What happens?

  1. Two Pods remain
  2. The ReplicaSet creates a new one to get back to three
Answer

Desired state: three.

Theory 2/2: kubectl, the daily commands

kubectl get pods -o wide
kubectl describe pod NAME      # events: why it does not start
kubectl logs NAME              # container output
kubectl exec -it NAME -- sh
kubectl scale deployment hello-ops --replicas=5
kubectl delete -f deploy.yaml

A Pod stuck in ImagePullBackOff cannot get its image; CrashLoopBackOff starts and keeps crashing: read describe, then logs.

Which command shows a Pod's events, such as an image that cannot be pulled?

Answer

kubectl describe pod NAME.

A Pod shows CrashLoopBackOff. Next command?

  1. kubectl logs (with --previous for the crashed run)
  2. Reinstall the cluster
Answer

The app is crashing: its logs say why.

Practice (30 min)

  1. kind create cluster --name lab; kubectl get nodes.
  2. Load your local image into the cluster: kind load docker-image hello-ops:dev --name lab.
  3. Save the manifest as k8s/deploy.yaml, add imagePullPolicy: IfNotPresent to the container, apply it, kubectl get pods until three are Running.
  4. Delete a Pod and watch it come back: kubectl get pods -w. Scale to 5, then back to 3.
  5. Break it: set the image to hello-ops:nope, apply, read describe. Fix it.

With your AI: "Here is kubectl describe pod for a Pod that will not start: [paste]. Point to the event line that explains it, say what it means in one sentence, and give the single command that would confirm it. Do not give me a fix yet."

Let an agent do it: an agent with kubectl access can triage a failing Pod (describe, logs, events) and propose the fix. Restrict it to a test cluster and to read commands first. You still check the cause in the events yourself and apply the fix through the manifest in Git, not with a live edit.

Recap (10 min)

Three lines: Pod, ReplicaSet, Deployment; the five kubectl commands; the two common failure states. Then rewrite the Deployment from memory.

Day 38 · 10/12 — Kubernetes 3: Services, ConfigMaps, namespaces, traffic in

Goal: give the Pods one stable address, configure them from outside the image, and know how traffic enters a cluster.

Theory 1/2: Services and namespaces

Pods come and go with new addresses. A Service gives a stable name and address and spreads traffic over the Pods whose labels match its selector. Types: ClusterIP (inside the cluster, the default), NodePort (a port on every node), LoadBalancer (an external load balancer, in clouds). Inside the cluster, the Service is reachable by DNS: hello-ops.default.svc.cluster.local, or just hello-ops from the same namespace. A namespace groups objects (per team, per environment).

apiVersion: v1
kind: Service
metadata:
  name: hello-ops
spec:
  selector:
    app: hello-ops
  ports:
    - port: 80
      targetPort: 8080

How does a Service know which Pods to send traffic to?

  1. By their names
  2. By its label selector
Answer

Labels: app: hello-ops.

Which Service type is the default, reachable only inside the cluster?

Answer

ClusterIP.

Theory 2/2: ConfigMaps, and traffic from outside

A ConfigMap holds configuration as key-value pairs, injected as environment variables or files: the same image runs everywhere with different settings (the twelve-factor app, factor III). To route HTTP from outside, Kubernetes has the Ingress API and its successor, the Gateway API; both need a controller installed. The most used controller, ingress-nginx, was retired by the Kubernetes project in March 2026 (no more fixes); the project points new installations to Gateway API implementations.

Where should APP_VERSION come from in Kubernetes?

  1. A ConfigMap (or the Deployment's env), not baked into the image
  2. A file edited inside the running Pod
Answer

Configuration lives outside the image.

Practice (30 min)

  1. Apply the Service; kubectl get svc; reach it from your machine with kubectl port-forward svc/hello-ops 8080:80 and curl http://127.0.0.1:8080/.
  2. From inside: kubectl run tmp --rm -it --image=busybox -- wget -qO- http://hello-ops/health.
  3. Create a ConfigMap: kubectl create configmap hello-ops --from-literal=APP_VERSION=0.2.0 --dry-run=client -o yaml > k8s/config.yaml, apply it, reference it in the Deployment with envFrom: [{configMapRef: {name: hello-ops}}], apply, curl: the version shows.
  4. Create a namespace staging and deploy the same manifests there with -n staging.

With your AI: "Explain the path of an HTTP request from my laptop to a container through kubectl port-forward, a Service and a Pod, step by step. Then explain how it would differ with a Gateway API implementation in front. Keep each step to one line."

Let an agent do it: generating Service and ConfigMap manifests is routine for an agent. Ask it to use kubectl create ... --dry-run=client -o yaml so the output comes from the tool. You still check the selector matches the Pod labels (kubectl get endpoints hello-ops must list addresses).

Recap (10 min)

Three lines: Service and selector, ConfigMap, Ingress and Gateway API. Then rewrite the Service from memory.

Day 39 · 11/12 — Kubernetes 4: probes, resources, rollouts and rollbacks

Goal: make Kubernetes know when the app is healthy, cap its resources, and update and roll back without downtime.

Theory 1/2: probes and resources

A readiness probe says when a Pod may receive traffic; a liveness probe says when to restart it; a startup probe protects slow starts. Requests are what the scheduler reserves for a container; limits are the ceiling (a container over its memory limit is killed: OOMKilled).

          readinessProbe:
            httpGet:
              path: /health
              port: 8080
            periodSeconds: 5
          livenessProbe:
            httpGet:
              path: /health
              port: 8080
            initialDelaySeconds: 5
          resources:
            requests:
              cpu: 50m
              memory: 32Mi
            limits:
              memory: 64Mi

Which probe removes a Pod from the Service without restarting it?

  1. Liveness
  2. Readiness
Answer

Readiness: not ready, no traffic. Liveness restarts.

What status does a container get when it exceeds its memory limit?

Answer

OOMKilled.

Theory 2/2: rolling updates and rollbacks

Changing the Deployment's image starts a rolling update: new Pods are created and must be ready before old ones are removed (maxSurge, maxUnavailable). Each version is kept as a revision.

kubectl set image deployment/hello-ops app=hello-ops:v2
kubectl rollout status deployment/hello-ops
kubectl rollout history deployment/hello-ops
kubectl rollout undo deployment/hello-ops

Which command goes back to the previous revision of the hello-ops Deployment?

Answer

kubectl rollout undo deployment/hello-ops.

The new version's readiness probe never passes. During the rolling update…

  1. the old Pods keep serving; the rollout stalls
  2. all Pods go down
Answer

Readiness protects the rollout: unready Pods get no traffic and old ones are not removed.

Practice (30 min)

  1. Add the probes and resources to k8s/deploy.yaml; apply; kubectl describe pod shows them.
  2. Build hello-ops:v2 (change the message), load it with kind, kubectl set image, and watch rollout status while a loop curls through the port-forward: no failed request.
  3. Build a broken hello-ops:v3 whose /health returns 500; deploy it: the rollout stalls, the old Pods serve. kubectl rollout undo.
  4. Put the image change in the manifest file, not only on the command line, and commit.

With your AI: "Here is my Deployment with probes and resources: [paste]. For each value, say what happens if it is too low and too high, in a table. Then tell me which value you would check first if Pods restart every few minutes."

Let an agent do it: an agent can watch a rollout and run rollout undo if errors rise. Give it the exact rule ("undo if more than 2 of 20 curls fail"). You still check afterwards which revision runs and why the bad one failed.

Recap (10 min)

Three lines: readiness versus liveness, requests versus limits, rollout and undo. Then rewrite the readiness probe from memory.

Day 40 · 12/12 — Kubernetes 5: Helm, Kustomize, GitOps

Goal: manage several environments without copying manifests, and understand GitOps.

Theory 1/2: Helm and Kustomize

Helm (a CNCF graduated project) packages manifests as a chart: templates plus a values.yaml; helm install renders and applies them, and tracks releases. Kustomize, built into kubectl (kubectl apply -k), keeps plain manifests as a base and applies overlays per environment (replicas, tags, namespace), with no template language.

k8s/
  base/            deploy.yaml service.yaml kustomization.yaml
  overlays/
    staging/       kustomization.yaml   # namespace: staging, replicas: 1
    prod/          kustomization.yaml   # namespace: prod, replicas: 3

Which tool is built into kubectl?

  1. Helm
  2. Kustomize
Answer

Kustomize, with kubectl apply -k.

Theory 2/2: GitOps

GitOps, a term coined by Weaveworks in 2017: the Git repository is the single source of truth for what runs; an agent inside the cluster (Argo CD or Flux, both CNCF graduated) pulls the repository and reconciles the cluster to it. Deploying is merging a pull request; a manual change in the cluster is reverted. The CI no longer needs credentials to the cluster: it only pushes images and updates a tag in Git.

In GitOps, how do you deploy version 1.3?

  1. Merge a change setting the tag to 1.3 in the Git repository
  2. Run kubectl set image on production
Answer

The repository is the truth; the agent applies it.

Someone edits a Deployment directly in a GitOps-managed cluster. Then…

  1. the change stays forever
  2. the agent detects the drift and restores what Git says
Answer

Self-healing reconciliation (if enabled).

Practice (30 min)

  1. Reorganise k8s/ into a Kustomize base and two overlays (staging: 1 replica, prod: 3, different namespaces and ConfigMap values).
  2. kubectl kustomize k8s/overlays/prod to see the rendered YAML; then kubectl apply -k both overlays; kubectl get pods -A -l app=hello-ops.
  3. Read Argo CD's getting started page and draw in your notebook where it would sit in your pipeline. Optional: install it in the kind cluster and point it at your repository.

With your AI: "Compare pushing deployments from CI with kubectl versus GitOps with Argo CD for my setup (one small app, one cluster, two environments), as a table: credentials needed, audit trail, drift handling, rollback. Then ask me two questions to check I understood."

Let an agent do it: an agent can convert flat manifests into a Kustomize base and overlays. You still diff the rendered output (kubectl kustomize) against what was deployed before: the result must be identical except for the intended per-environment values.

Recap (10 min)

Three lines: Helm, Kustomize, GitOps. Then redraw the base / overlays tree from memory.

Day 41 · 13/12 — Secrets management

Goal: find a secret in a repository, keep secrets encrypted or outside Git, and know what Kubernetes Secrets do and do not protect.

Theory 1/2: where secrets leak

A secret is anything that grants access: password, API token, private key, database URL with a password. They leak through Git history (day 14), CI logs, container images (docker history), shell history, screenshots. Scanners such as Gitleaks search a repository and its history for them, locally or in CI. A leaked secret is rotated first: deleting the commit does not undo the leak.

A token was pushed to a public repository an hour ago. First action?

  1. Rewrite the Git history
  2. Revoke and replace the token
Answer

Rotate first: bots scan public repositories within minutes.

Are Kubernetes Secrets encrypted by default?

  1. No: base64 encoded, which anyone can decode; encryption at rest must be configured
  2. Yes, with AES
Answer

Base64 is an encoding, not encryption. Restrict access with RBAC and enable encryption at rest.

Theory 2/2: three ways to keep secrets

  • The platform's secret store: CI secrets (${{ secrets.NAME }}, masked in logs), Kubernetes Secrets with restricted access.
  • Encrypted in Git: SOPS encrypts the values of a YAML or JSON file (keys stay readable for diffs) with age keys or a cloud KMS; only holders of the key decrypt.
  • A secrets manager: a service that stores, audits and rotates secrets, and hands them out at runtime: HashiCorp Vault (under the Business Source License since 2023), its open fork OpenBao (Linux Foundation), Infisical…

With SOPS, what stays readable in the encrypted file?

  1. Nothing
  2. The values
  3. The keys (field names), so diffs remain meaningful
Answer

Only values are encrypted.

A secret is used in a CI step with run: echo $TOKEN. Is it safe because the forge masks secrets?

  1. Yes, masking is a guarantee
  2. No: masking is best effort (an encoded or split value is not masked); never print secrets
Answer

Masking helps; it does not make printing safe. The tj-actions attack printed secrets double-encoded to avoid it.

Practice (30 min)

  1. Plant a fake secret in a test repository: the example access key that AWS publishes in its own documentation (search "AWS example access key"), written as aws_secret_access_key = ... in a config file. Commit, then delete it in a second commit.
  2. Run Gitleaks on it (its Docker image or a release binary; read its README for the current command, for example gitleaks git -v .): it finds the secret in the history.
  3. Install SOPS and age; age-keygen -o ~/.config/sops/age/keys.txt; encrypt a secrets.yaml with sops --encrypt --age <your public key> secrets.yaml > secrets.enc.yaml; open it with sops --decrypt. Commit only the encrypted file.
  4. Create a Kubernetes Secret, then read it back with kubectl get secret NAME -o yaml and decode the value with base64 -d: see why access control matters.

With your AI: "Here is how my project handles secrets today: [describe: .env file on the server, CI secret for the registry, Kubernetes Secret created by hand]. List the risks in order, and for each one the smallest change that reduces it. Do not ask me to paste any secret value."

Let an agent do it: an agent can add a Gitleaks job to CI and a pre-commit hook. It must never be given a real secret to "test" with. You still check: the job fails on the planted fake secret, and passes once it is removed from the files and allow-listed history is justified.

Recap (10 min)

Three lines: where secrets leak, rotate first, the three ways to keep them. Then redo the SOPS encrypt and decrypt without notes.

Day 42 · 14/12 — Module 3 deliverable: hello-ops from code alone

Goal: deliver an infrastructure repository that deploys hello-ops three ways, with no manual step, and check what you learnt.

Theory 1/2: the acceptance test of IaC

The test of infrastructure as code is brutal and simple: delete everything, run the code, and get the same result. If a step needs you to remember something, it is not in the code yet. Write that test down as a README section: "from a fresh machine to a running app".

Your playbook works, but only after you created a folder by hand once. Is the infrastructure fully in code?

  1. No: the folder must be a task
  2. Yes
Answer

Any step from memory is a gap.

Theory 2/2: module review

Which command must you read before every tofu apply?

  1. tofu init
  2. tofu plan
Answer

The plan says what will be created, changed or destroyed.

A Pod is Running but receives no traffic from its Service. First check?

  1. Restart the cluster
  2. Raise the memory limit
  3. The Service selector against the Pod labels, and the readiness probe
Answer

kubectl get endpoints shows whether the Service found ready Pods.

The two ansible-playbook options to preview a run?

Answer

--check --diff.

Base64 in a Kubernetes Secret is…

  1. an encoding, not a protection
  2. strong encryption
Answer

Anyone with read access decodes it.

Practice (30 min)

  1. Finish hello-ops-infra: ansible/ (role hello_ops, systemd deployment), tofu/ (module, two environments, Docker provider), k8s/ (Kustomize base and overlays with probes and resources), a CI workflow (fmt, validate, ansible-lint, kubectl kustomize renders), secrets encrypted with SOPS or absent.
  2. Run the acceptance test: kind delete cluster, tofu destroy, then recreate everything from the README alone.
  3. Run Gitleaks on the whole history: clean.
  4. Add it to your portfolio list: deliverable 3.

With your AI: "Here is the README of my infrastructure repository, section 'from a fresh machine to a running app': [paste]. Find every step that assumes something not written (a tool installed, a variable set, a file present). List them; do not rewrite the README."

Let an agent do it: an agent is a good acceptance tester: give it a fresh VM and only your README, and ask it to follow it literally and report where it got stuck. You still read its log: each place it improvised is a missing line in your README.

Recap (10 min)

Count your score. The missed topics come back on day 56, when the final project is deployed.

Sources

Found this useful? Tell us what to measure next.