Home/Ansible
Lab · Manual of Ansible

Ansible

The orchestra conductor of the infrastructure.

28 exercises8 Parts6 appendices
The colour code shows the level:FoundationalIntermediateAdvancedCloud Architect
get the exercises
git clone --filter=blob:none --sparse https://github.com/calmict/book_labs.git
cd book_labs
git sparse-checkout set ansible/ed1
calm@calmict:~$ cat ansible/topics
Part 1

The Foundations of Control

  • 01The three cracksFoundational
  • 02The messenger, not the tenantFoundational
  • 03The key that stays homeFoundational
  • 04The score that liesFoundational
  • 05The switch and the doorbellFoundational
Part 2

First Steps into the Chaos

  • 06The batonFoundational
  • 07The nearest music standFoundational
  • 08The address bookFoundational
  • 09The cue, not the scoreFoundational
Part 3

The Art of Automation

  • 10The written scoreFoundational
  • 11The caretaker's keysIntermediate
  • 12Annotations on the scoreIntermediate
  • 13The chain of commandIntermediate
  • 14The recall at the end of rehearsalIntermediate
  • 15If, and for eachIntermediate
Part 4

Structured Modularity

  • 16The sectionIntermediate
  • 17The shared repertoireIntermediate
Part 5

Handling Sensitive Data

  • 18The strongboxAdvanced
  • 19The strongroomAdvanced
Part 6

Advanced Tools

  • 20The arrangerAdvanced
  • 21The roll-callAdvanced
  • 22When a string snapsAdvanced
Part 7

Quality and Testing

  • 23The dress rehearsalAdvanced
  • 24The throwaway stageAdvanced
  • 25The right tempoCloud Architect
Part 8

Automation in Production

  • 26The stage machineryCloud Architect
  • 27Without stopping the musicCloud Architect
  • 28The standing theatreCloud Architect
calm@calmict:~$ ls ansible/exercises/
01The three cracksFoundational

What you build

The manual opens with a promise: to become the conductor of your infrastructure orchestra. But before you raise the baton you have to understand *why* the hand-written score — the Bash script — crumbles when the players are not three but three thousand. In this first lab we will not use Ansible yet: we install it in chapter 6. Here you feel with your own hands the *problem* Ansible exists to solve, so the rest of the manual has a reason to exist.

Three "servers" (three containers), one state you want to keep identical across all of them, and a script that tries to enforce it. The script will work. Then you re-run it, and the first crack opens.

Objectives

  • See the three cracks of imperative scripting in action: it is not repeatable, it describes *steps* not a *state*, and divergence is inevitable.
  • Tell apart two ideas that look the same and are not: repeatability (I can re-run it without it blowing up) and convergence (it brings me back to the desired state, whatever the starting point).
  • Understand why going from 3 to 3000 servers is not a quantitative but a qualitative jump, and what push and pull mean.

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap01/solution
./run.sh

Expected result

  • The starting script fails on the second run with the useradd error (crack 1 seen).
  • With TODO 1's guards the script re-runs without errors, but after the sabotage the drift on server2 survives with the blind guard (crack 3 seen).
  • With TODO 2 (content guard) server2 goes back to version=1.0 after the sabotage (convergence).
  • You can explain in your own words why *repeatable* is not *convergent*, and why at 3000 servers the imperative script is not enough.
02The messenger, not the tenantFoundational

What you build

The conductor of the orchestra does not implant a chip in every musician's brain: he speaks, and they — who already know how to read music — play. Ansible works the same way. It does not install an *agent* that lives on the machine: it visits over SSH, has it do one thing using the Python that is already there, and leaves. This is called agentless, and it is the heart of the architecture.

In this lab you become the messenger: you will reproduce by hand, over SSH, the journey Ansible automates for every task. We will not use Ansible yet (you install it in chapter 6): the point is exactly to show that nothing *of its own* is needed on the target — only SSH and Python.

Objectives

  • Understand agentless and its three gifts: nothing to install/maintain on the target, no daemon listening (unchanged attack surface), works on anything that speaks SSH + Python.
  • Tell apart the control node (your machine, where you start from) and the managed node (the machine you configure, which hosts nothing of yours).
  • Rebuild the journey of a task, frame by frame: copy the module, run it with the remote Python, JSON on stdout, cleanup.
  • See the role of Python — and when it is *not* needed (the raw module, pure shell over SSH).
  • Touch the facts: how Ansible "interviews" the machine.

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap02/solution
./run.sh

Expected result

  • The managed node is up with only sshd + python3, and you get in over SSH with a key.
  • The completed module.py returns valid JSON with at least three facts in ansible_facts, run with the node's Python.
  • You reproduced the four frames of the journey (copy, remote execution, JSON, cleanup), and at the end the temporary file does not remain on the node.
  • You can say why the raw module does not require Python on the target, and what it is for.
03The key that stays homeFoundational

What you build

In chapter 2 the script handed you a key and you got onto the managed node Ansible travels on. At its heart is asymmetric cryptography and one golden rule: the private key never leaves the control node; only its public half travels. We will build a small world — an exposed bastion and a target locked in a segregated network — and cross it by hand.

Objectives

  • Understand the asymmetric pair: the private key (stays home) and the public key (goes onto the servers, into authorized_keys); and the handshake that puts it to work.
  • The file anatomy and the permissions that matter (the "UNPROTECTED PRIVATE KEY" trap).
  • ~/.ssh/config: readable aliases and ControlMaster, the multiplexing that makes Ansible fast.
  • Bastion host / ProxyJump: crossing a segregated network.
  • Passphrase (protection at rest), ssh-agent (automation without prompts), and the host key checking trap.

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap03/solution
./run.sh

Expected result

  • You get into the bastion with the key via the alias; the 0644 key is refused.
  • The target entry with ProxyJump takes you onto the target through the bastion (the direct attempt fails).
  • With ControlMaster the second connection is near-instant (master socket created).
  • You can explain why the private key must never leave the control node.
04The score that liesFoundational

What you build

YAML is the score you will write every playbook and every inventory on. It looks trivial — and that is exactly the trap: a value written the way you mean it can be 1.2, 22:30 becomes 1350. In this lab you learn the anatomy of YAML and, above all, how not to let the score lie to you. No containers, no Ansible yet: just YAML files and the very parser Ansible uses under the hood (PyYAML).

Objectives

  • The three structures — scalar, list, dictionary — and nesting by indentation.
  • The silent traps (Norway Problem, implicit typing, leading zeros, base 60) and the loud ones (colons, indentation).
  • Quoting: when and why to add the quotes.
  • Block scalars: the pipe | (literal) and the greater-than > (folded).
  • Anchors and merge << for reuse (DRY).
  • yamllint as the safety net.

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap04/solution
./run.sh

Expected result

  • inspect.py on start/config.yml shows the mis-typed values (country bool, version float, file_mode int…).
  • After TODO 1, the same values are strings.
  • After TODO 2, web and db share &defaults via << and db overrides only the timeout.
  • You can explain the difference between a loud trap and a silent one, and why the second is worse.
05The switch and the doorbellFoundational

What you build

In chapter 1 you saw the difference between "re-runnable" and "convergent". Now we take it to the core: idempotence. A light switch is idempotent — you flip it to ON; if it is already ON, nothing happens, and the room is lit either way. A doorbell is not: every press rings again. Ansible is made of switches: you tell it the desired *state*, it acts only if needed, and it tells you the colour of the change. Before installing it (chapter 6), you build it in miniature with your own hands: a tiny idempotent engine in bash that reports the colours and can do a "dry run".

Objectives

  • Idempotence without fear: applying twice = applying once.
  • Switch vs doorbell; declarative vs imperative.
  • The colours of change: ok (green, nothing to do), changed (yellow, I acted), failed (red).
  • The black swans: operations that are not idempotent by nature, and how to judge them (changed_when).
  • The dress rehearsal: check mode (dry-run) and diff.

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap05/solution
./run.sh

Expected result

  • ensure.sh completed: first run all [changed], second run [ok] (idempotence).
  • The doorbell append_line stays [changed] on every run (never converges).
  • In check mode ensure_line says [changed] WOULD but writes nothing.
  • render with changed_when reports [ok] on the second run with the same inputs.
06The batonFoundational

What you build

For five chapters you studied the score without ever raising the baton. Now you pick it up: you install Ansible. But like a good conductor you do not dirty the stage — you install it in an isolated environment (a virtualenv), so you never touch the system Python. Then you tune the players: you prepare the nodes you will configure in the coming chapters.

Objectives

  • ansible-core versus the ansible package: the engine + the ansible.builtin modules, against the bundle with hundreds of community collections.
  • The installation methods (system / pip / pipx) and why the venv is your salvation (isolation).
  • Installing ansible-core in a venv and making it reproducible with a requirements.txt.
  • Verifying: ansible --version, the command family, the smoke test ansible localhost -m ping.
  • Preparing the target nodes: the lab you will use from here on.

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap06/solution
./run.sh

Expected result

  • The venv exists and ansible --version shows ansible-core.
  • The five commands answer.
  • ansible localhost -m ping → pong.
  • The two target nodes are reachable over SSH.
07The nearest music standFoundational

What you build

The conductor has the baton (chapter 6); now comes the orchestra rulebook: ansible.cfg, the file where the rules of how Ansible works live — how many players in parallel, which key to enter with, whether to ask for permission. The most important thing to understand is not *what* is written in it, but which copy of the rulebook gets read: Ansible looks at four music stands in a fixed order and uses the first one it finds, in its entirety — no merging. And there is a security trap: if the stand is in a room anyone can write in, Ansible refuses to read it.

Objectives

  • The search hierarchy: ANSIBLE_CONFIG → ./ansible.cfg → ~/.ansible.cfg → /etc/ansible/ansible.cfg; the first one found wins outright (no merging).
  • The file structure and the [defaults] section (inventory, forks, host_key_checking).
  • [privilege_escalation] (become) and [ssh_connection] (pipelining — chapter 3's ControlMaster becomes a setting).
  • The ansible-config tools: list, view, and the precious dump --only-changed.
  • The trap of the world-writable directory: the cwd cfg is convenient and dangerous.

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap07/solution
./run.sh

Expected result

  • You can say which cfg is active and why (the four stands in order).
  • start/ansible.cfg completed: dump --only-changed shows forks, host_key_checking, become, pipelining with your file's path.
  • You saw the trap: in the world-writable directory the cfg is ignored with the WARNING.
  • You can explain why the hierarchy does not merge files, and what that implies.
08The address bookFoundational

What you build

The conductor has the baton (ch. 6) and the rulebook (ch. 7), but does not yet know their addresses, the groups they play in. It is the file that turns "a container on port 2281" into web1, and "web1 and web2" into web — so from here on you say *ansible web* and not a list of IPs. In this lab you write it in INI, verify it with the right tools, and at the end the conductor calls the roll: ping the whole fleet, by name.

Objectives

  • What an inventory is; the INI format (hosts, groups, variables) and the YAML equivalent.
  • Groups of groups with :children.
  • The host patterns: groups, exclusions (web:!web2), combinations.
  • The ranges: edge[01:03] — three hosts in one line.
  • Host and group variables in the inventory, and the tidy form: the group_vars/ and host_vars/ directories.
  • The magic groups all and ungrouped; verification with ansible-inventory.

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap08/solution
./run.sh

Expected result

  • ansible-inventory --graph shows prod → web(web1,web2) + db(db1) and edge with the 3 range hosts.
  • The patterns answer: web → 2 hosts, 'web:!web2' → 1, ungrouped → 0.
  • group_vars/web.yml exists and debug prints greeting on web1.
  • ansible prod -m ping → 3 SUCCESS.
09The cue, not the scoreFoundational

What you build

The address book answers the roll (ch. 8). Now the conductor gives the first orders — but without writing the full score (that is the playbook, ch. 10). An ad-hoc command is a *cue*: one module, one target, right now, across the whole fleet. Perfect for a one-off (is it up? how much disk? restart that service); wrong for anything to repeat or version — that needs the score. Here you learn the anatomy of the cue, the arsenal of modules, and the crucial difference — which you already sensed in ch. 5 — between a switch module and a doorbell command.

Objectives

  • When ad-hoc is right and when it is wrong.
  • The anatomy: ansible <pattern> -m <module> -a "<args>" [-b].
  • command vs shell (pipes and redirections), and why both are "doorbells".
  • The arsenal: copy and file (idempotent switches), setup (the facts = the chapter 2 interview).
  • The forks: measurable parallelism (ch. 7).
  • -b / become: administrator on the fly (ch. 7 and 11).
  • The real cases (9.8): the morning round.

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap09/solution
./run.sh

Expected result

  • The completed runbook.sh runs: motd deployed (copy), /etc/cap09.d created (file+become), one fact read (setup).
  • copy and file: green on the second run (idempotent). command: always CHANGED.
  • command vs shell: the pipe is literal with command, executed with shell.
  • -b: whoami goes from deploy to root.
10The written scoreFoundational

What you build

In chapter 9 the conductor gave cues on the fly: one module, one target, right now. Useful, but fleeting — no record, nothing to review, nothing to re-run with confidence. Now you write the score: the playbook, a YAML file that puts the same modules in order, with a name, under version control. This is the heart of Ansible — from here on almost everything is a playbook. You learn the layered structure (play → task → module), write your first playbook line by line, learn to read its output, and rediscover the property that matters most of all: re-running it does no harm (the acid test of ch. 5, now at scale).

Objectives

  • Why the playbook, not the cue: repeatable, versioned, reviewable.
  • The layered structure: play (who + a list of tasks) → task (name + module + args) → module.
  • The first playbook line by line: ---, name, hosts, become, vars, tasks.
  • Running and reading the output: PLAY, TASK, Gathering Facts, PLAY RECAP and its counters.
  • The acid test: re-run → changed=0 (idempotence).
  • More plays in one file.
  • Directives useful from the start: vars, become_user, tags (--tags / --skip-tags).
  • Good habits from the first line.

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap10/solution
./run.sh

Expected result

  • ansible-playbook site.yml shows two plays and a recap with web1/web2 (ok=4 changed=3) and db1 (ok=2 changed=1).
  • Re-running → changed=0 on all three (idempotence).
  • --tags structure runs only the directory tasks; --skip-tags content skips the copies.
  • --syntax-check passes.
11The caretaker's keysIntermediate

What you build

In chapter 10 privilege was a switch flipped in bulk: become: true, and everything ran as root. Now you open that box. become is not "just be root": it is asking the building's caretaker for the keys — on Linux, almost always sudo. The caretaker has a rulebook (the sudoers file): it decides *who* may take *which* key, and whether they must first show ID (the password). This chapter — the first of the Intermediate tier — shows you the anatomy of become, the rulebook under the hood, the three answers to the password, how to become a user *other* than root, and the golden rules for not leaving the master key lying around.

Objectives

  • Why not to log in directly as root (11.1).
  • The anatomy of become: become, become_method, become_user, become_flags (11.2).
  • sudoers under the hood: the gate that decides who becomes whom (11.3).
  • The sudo password: -K, the variable (to be encrypted), NOPASSWD (11.4).
  • Not only sudo: the other methods (11.5).
  • Becoming a user other than root (11.6).
  • The golden rules of security (11.7).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap11/solution
./run.sh

Expected result

  • With become: true, the debug shows deploy -> root and secops -> root.
  • web2 without ansible_become_password → "Missing sudo password"; with the variable → it passes.
  • /srv/app/owner.txt is owned by appsvc, not root; the /srv/app directory is appsvc's.
  • Re-running → changed=0 (idempotence).
12Annotations on the scoreIntermediate

What you build

The score of chapter 10 was rigid: /etc/motd, port 80, those values written inside the playbook. But web1 and web2 are not identical — different ports, different limits — and rewriting the playbook for each would be chapter 1's crack returning. Variables are the pencil annotations on the score: a value with a name, written once and reused everywhere, that can arrive from many sources — the group, the single host, the command line, the facts Ansible discovers on its own. In this chapter you see what shape they take (the types), how to use them (Jinja2's double braces), where they live, how to capture a result on the fly, and how to keep them tidy.

Objectives

  • Why variables: one playbook, many nodes (12.1).
  • The types of value: string, integer, boolean, list, dictionary (12.2).
  • Jinja2's double braces: using them, reaching into lists and dictionaries (12.3).
  • Where they live: play, inventory (group_vars/host_vars), command line -e (12.4).
  • The facts: the variables Ansible discovers on its own (12.5).
  • Capturing results: register and set_fact (12.6).
  • The safety nets: default values (12.7).
  • Keeping order: where it pays to define what (12.8).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap12/solution
./run.sh

Expected result

  • The rendered config.ini holds all the types: string (app_name), integer (port), boolean (debug), list (features), dictionary (max_connections/timeout).
  • web1 port=8080 (group_vars), web2 port=8081 (host_vars wins).
  • -e app_name=canary → canary on both (extra var wins).
  • workers comes from set_fact (nproc x 2); log_level comes from the default (info).
  • Re-running → changed=0 (idempotence).
13The chain of commandIntermediate

What you build

In chapter 12 you saw the command line beat group_vars, almost without thinking. It was not magic: it was precedence. Ansible lets you define a variable in *many* places — a huge convenience — but the price is that, when two places declare the same name with different values, one must win. Ansible has a rigid chain of command: 22 levels, from the weakest (role defaults) to the strongest (-e). This chapter does not have you build infrastructure: it has you *investigate*. You provoke real clashes between variables, watch who wins, learn the three principles that explain almost everything, the two pitfalls that surprise everyone, and how to design so as never to fight.

Objectives

  • Why so many levels exist (13.1).
  • The three principles that explain almost everything (13.2).
  • The complete list, from weakest to strongest (13.3).
  • Real clashes: seeing it in action (13.4).
  • The tools to not get lost (13.5).
  • The two pitfalls: dictionaries that do not merge, and facts (13.6).
  • Designing so as not to fight (13.7).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap13/solution
./run.sh

Expected result

  • winner: web1 = group_vars(web), web2 = host_vars(web2); with -e → EXTRA on both.
  • bad_limits: web1 has 2 keys, web2 has 1 (the dictionary was replaced, not merged).
  • combine: the merge gives web2 {max_connections: 500, timeout_seconds: 30} (timeout kept).
  • set_fact: mode stays set_fact_value even with a task var (19 beats 17).
14The recall at the end of rehearsalIntermediate

What you build

You change a service's configuration: now it must be reloaded to read it. But reloading it on *every* run of the playbook — even when you touched nothing — is waste and risk: needless interruptions, dropped connections, for nothing. You want to reload it only if the config really changed. Ansible solves this with the notify / handler pair: a task leaves a *recall*, and at the end of the rehearsal — only if that task reported changed — the handler fires. It is the changed colour of chapter 5 that stops being a mere signal and becomes a trigger.

Objectives

  • The problem: reload only when needed (14.1).
  • The engine: the changed state (14.2).
  • notify and handlers: the pair that solves it (14.3).
  • The three golden rules of handlers (14.4).
  • Several handlers together, and the listen trick (14.5).
  • Controlling the changed state by hand with changed_when (14.6).
  • A real example, from start to finish (14.7).
  • Good habits with handlers (14.8).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap14/solution
./run.sh

Expected result

  • First run: reloads.log and metrics.log have 1 line each (two tasks notify, two handlers via listen, each fires once).
  • Re-running with no changes: the logs stay at 1 (rule 2).
  • With -e greeting=ciao: the config changes → the logs go to 2.
  • With -e force_reload=true: even with no file changes, changed_when triggers → the logs grow again.
15If, and for eachIntermediate

What you build

So far every task did one thing, once, always. But reality adapts: dev and prod are not the same, some features are optional, some actions must be repeated over twenty items. You need two new powers. when gives a task the ability to *decide*: act only *if* a condition is true. loop gives it the ability to *repeat*: one task, many items. With these two — and the braces trap that comes with them — a playbook stops being a fixed list and becomes an intelligent procedure.

Objectives

  • The problem: a playbook that adapts (15.1).
  • when: the task that decides whether to act (15.2), and the braces trap (15.3).
  • register + when: conditions on a task's outcome (15.4).
  • Composite conditions: and, or, the AND list (15.5); and is defined (15.6).
  • loop: one task, many items (15.7), including lists of dicts (15.8).
  • loop + register and loop_control (15.9, 15.10); the old with_* (15.11).
  • The braces, once and for all (15.13).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap15/solution
./run.sh

Expected result

  • loop: the users websvc (/bin/bash) and batchsvc (/usr/sbin/nologin) are created; the directories logs, cache, run exist.
  • dev (default): PRODUCTION, metrics.enabled and tuning.conf do not exist (three skips); firstrun.txt does.
  • Re-run: the first-time task skips (sentinel).
  • -e app_env=prod -e enable_metrics=true -e tuning_profile=fast: the three files appear.
  • -e app_env=prod alone: metrics.enabled does not appear (the AND wants both).
16The sectionIntermediate

What you build

The playbook of chapter 15 can decide and repeat — but it has grown: tasks, variables, templates, handlers, all piled into a single file. Tomorrow a second project will want the same app: do you copy and paste? The role is the answer. It is a section of the orchestra: a self-contained block, holding its own tasks, its own files, its own default tuning, that the conductor calls by name — and that you can reuse in any concert. In this chapter you turn that bloated playbook into a clean role, and the playbook shrinks back to three lines.

Objectives

  • The problem: the playbook that never stops growing (16.1).
  • What a role is: a folder with a precise structure (16.2, 16.3).
  • The playbook that becomes tiny (16.4).
  • defaults versus vars: the heart of reusability (16.5).
  • files and templates: no more paths (16.6).
  • meta and dependencies (16.7); include_role and import_role (16.8).
  • The skeleton with ansible-galaxy init (16.9); anatomy of a good role (16.10).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap16/solution
./run.sh

Expected result

  • ansible-galaxy init creates the skeleton; the main.yml files are filled in.
  • Rendered config: app_name = webfromgroup (group_vars beats defaults), config_dir = /etc/webapp (the role's vars beats group_vars; /etc/WRONG does not exist).
  • template (app.conf.j2) and file (motd) resolved with no path, from the role's folders.
  • The reload webapp handler fires (from the role's handlers/main.yml).
  • The playbook is three lines (roles: - webapp); re-running → changed=0.
17The shared repertoireIntermediate

What you build

The role of chapter 16 is yours, written at home. But thousands of people have already written and shared roles and modules for every conceivable task: managing firewalls, databases, cloud providers, system services. You do not have to re-compose what already exists — you can draw on the shared repertoire. The place is Ansible Galaxy; the unit of distribution is the collection; and the way to cite each piece precisely is the fully qualified name, the FQCN. This chapter — the last of the Intermediate tier — teaches you to stand on the shoulders of giants without losing reproducibility.

Objectives

  • On the shoulders of giants, and from roles to collections (17.1, 17.2).
  • The mystery of the dotted names: the FQCN (17.3).
  • Installing a collection (17.4) and declaring it in requirements.yml (17.5).
  • Where collections end up and how to keep them with the project (17.6).
  • Using it in a playbook (17.7).
  • Automation Hub and private repositories (17.8); publishing (17.9).
  • Good habits with Galaxy and collections (17.10).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap17/solution
./run.sh

Expected result

  • requirements.yml installs community.general (pinned version) into ./collections (inside the project).
  • The playbook uses community.general.ini_file by FQCN and writes [server] port = 8080 to the INI.
  • Re-running → changed=0 (the collection module is idempotent like the built-ins).
  • ansible-galaxy collection list shows the pinned version, from the project's path.
18The strongboxAdvanced

What you build

In chapter 11 you asked the caretaker for the keys: become, and for the node whose sudo needs a password you made it work — but you wrote that password *in clear text* in the inventory, with the promise "one day we will encrypt it". Today is that day. A playbook ends up in a Git repository, and Git *does not forget*: a password committed in clear stays in the history forever, even if you delete it tomorrow. This chapter — the first of the Advanced tier — opens Ansible's strongbox: Ansible Vault, which encrypts secrets *inside* your files, so the repository stays shareable and the secret stays secret.

Objectives

  • The original sin: the plaintext secret, and why Git makes it eternal (18.1).
  • Encryption with a passphrase (18.2) and the ansible-vault commands (18.3).
  • What an encrypted file looks like (18.4).
  • Encrypting a single secret with encrypt_string (18.5).
  • Running a playbook with encrypted data: interactive, file, config (18.6).
  • More secrets, more passwords: vault-ids (18.7).
  • Vault's limits and what comes next (18.8); the good habits (18.9).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap18/solution
./run.sh

Expected result

  • group_vars/web/vars.yml holds no plaintext password: only the {{ vault_become_password }} indirection and the inline !vault block.
  • group_vars/web/vault.yml is encrypted (first line $ANSIBLE_VAULT;1.1;AES256); ansible-vault view shows the value.
  • The playbook becomes root with the password taken from the vault and writes the root:root marker; on a rerun → changed=0.
  • Without the passphrase the playbook fails with "Attempting to decrypt but no vault secrets found".
  • The prod-labelled secret has the header ;1.2;AES256;prod and decrypts with its vault-id.
19The strongroomAdvanced

What you build

In chapter 18 you shut the password in the strongbox. But a paradox remained: the strongbox is encrypted, and its key — the vault passphrase — where does it live? If you write it in a file next to the playbook, you are back at chapter 11's sin: a secret in clear on disk. Chapter 18 moved the problem, it did not remove it. The real solution changes paradigm: the secret is not kept at all — neither in clear nor encrypted — but fetched at runtime from an external service that guards it, hands it to whoever has the right, and never lets it rest with you. That service is a strongroom: in this lab, HashiCorp Vault.

Objectives

  • The three limits Vault (ch. 18) does not solve (19.1).
  • The paradigm shift: the runtime lookup (19.2).
  • HashiCorp Vault and the community.hashi_vault collection (19.3).
  • Authentication: token, AppRole and machine identity (19.4).
  • The cloud secret managers: AWS, Azure, GCP (19.5).
  • SSH keys in production: distribution, rotation, bastion (19.6).
  • no_log: the secret that must not end up in logs (19.7).
  • When Vault is enough (ch. 18), when you need a manager (19.8).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap19/solution
./run.sh

Expected result

  • requirements.yml installs community.hashi_vault; hvac is in the venv.
  • In group_vars/web/vars.yml the become password is a community.hashi_vault lookup, not a value.
  • The playbook becomes root with the password fetched from the strongroom; the marker is root:root; on a rerun → changed=0.
  • The secret secops-pw does not appear in the configuration (group_vars, site.yml) nor in the -vvv output.
  • The AppRole (machine identity) reads the same secret with a read-only policy.
  • The no_log-marked task shows "the output has been hidden" at -vvv.
20The arrangerAdvanced

What you build

So far you have *passed* data around: a variable here, a list there, a dictionary into a template. But raw data rarely already has the shape you need: you have a list of services and want only the enabled ones; you have a base configuration and a bundle of per-environment changes, and you want them merged; you have a map and want to walk it line by line. Chapter 20 gives you the arranger: Jinja2 in its full form — the filters that transform, the tests that ask, the lookups that fetch — and the .j2 templates that, from the data, write the configuration *by themselves*. You stop writing configs by hand: the config becomes a *function* of the data.

Objectives

  • The three families: filters, tests, lookups (20.1).
  • default and mandatory: the safety net (20.2).
  • Transforming data: map, select, selectattr (20.3).
  • Working with dictionaries: dict2items and combine (20.4).
  • The tests: is defined, is version and the others (20.5).
  • The .j2 templates: configurations that write themselves (20.6).
  • The lookups, finally in full (20.7).
  • The good habits with Jinja2 (20.8).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap20/solution
./run.sh

Expected result

  • enabled_services (TODO 1) keeps only the services with enabled true.
  • effective_config (TODO 2) merges base and overrides (timeout 60, loglevel debug).
  • The rendered app.conf contains the [settings] section with the overrides, a block for api-01/db-01/web-01 (in order, not web-02 which is disabled), and "allowed prod ports: 8080,8081,5432".
  • On a rerun → changed=0 (the template is idempotent).
  • The playbook runs in connection: local, with no nodes.
21The roll-callAdvanced

What you build

In chapter 8 you wrote the address book by hand: a file with the nodes, one per line. It works as long as the fleet stands still. But in the real world the fleet *moves on its own*: machines that are born when load rises and die when it drops, in the cloud, while you sleep. A hand-written address book is stale the moment you save it. The answer is to flip the mechanism: instead of *listing* the nodes, you have them answer a roll-call — Ansible asks whoever really knows the fleet (the provider) "who is here now?", and builds the inventory *on the spot*. Here the provider is the Docker daemon, and the fleet is containers that come and go; but AWS, Azure, GCP work identically.

Objectives

  • Static versus dynamic: the mindset shift (21.1).
  • The mechanism: the inventory plugins (21.2).
  • The first dynamic inventory on AWS (21.3, gallery).
  • The real magic: grouping with keyed_groups (21.4).
  • groups and compose: the Jinja2 filters at work (21.5).
  • Readable names and performance: hostnames and cache (21.6).
  • The good habits with dynamic inventories (21.7).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap21/solution
./run.sh

Expected result

  • The dynamic inventory discovers the three fleet containers (cap21-web1/web2/db1) without you writing their names.
  • keyed_groups (TODO 1) creates role_web (web1, web2), role_db (db1), env_prod (web1, db1), env_staging (web2).
  • groups (TODO 2) creates production with web1 and db1 (the only env=prod).
  • compose makes every node reachable over the docker connection: ansible role_web -m ping → pong.
  • site.yml (TODO 3) on role_web touches only web1 and web2; on a rerun → changed=0.
22When a string snapsAdvanced

What you build

You can discover the fleet (ch. 21) and act on it. But a real orchestra plays in an imperfect world: a string snaps mid-concert, a stand falls, a player misses the cue. The question is not *whether* something will go wrong on one of the thousand nodes at the roll-call, but *what the conductor does when it happens*. By default Ansible, faced with an error, stops on that host — prudent, but not enough. This chapter gives you the tools of resilience: recover with block/rescue/always, retry what is slow, redefine what counts as an error, and — when needed — stop everything fast before the disaster spreads.

Objectives

  • The default behaviour: stop on that host (22.1).
  • block, rescue, always: Ansible's try/catch/finally (22.2).
  • ignore_errors: carry on anyway, with judgement (22.3).
  • failed_when and changed_when: redefine success and change (22.4).
  • Retry what is slow: until, retries, delay (22.5).
  • Fail fast: any_errors_fatal and max_fail_percentage (22.6).
  • Validate before acting: assert and fail (22.7).
  • Handlers and failures: force_handlers (22.8).
  • The good habits with error handling (22.9).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap22/solution
./run.sh

Expected result

  • assert (TODO 3) blocks deploy_env=banana *before* writing any file; with a valid value it proceeds.
  • block/rescue/always (TODO 1): on db1 the rollback and cleanup markers exist; on web1 the deployed and cleanup markers exist but *not* rollback; the play is not failed (rescued=1 on db1).
  • until (TODO 2): the health check passes after a few attempts instead of failing at once.
  • ignore_errors leaves the play at failed=0 (ignored >= 1); failed_when: false treats rc=1 as success.
  • handlers.yml: fh.done exists despite the failure (force_handlers).
  • failfast.yml: with any_errors_fatal the rollout reaches no host.
23The dress rehearsalAdvanced

What you build

Chapter 22 taught you to *react* to errors. But the best way to handle an error is not to make it — or at least to catch it *before* you touch production. Before a concert no orchestra walks on stage blind: it re-reads the parts (is there a misprint?), holds a dress rehearsal in an empty theatre (plays everything, no audience), and only then opens the doors. Ansible gives you the same safety net, at three increasingly rich levels: --syntax-check (the quick read), ansible-lint (the expert proofreader), and check mode with --diff (the dress rehearsal that shows you what would change without changing it). Better a red error on your terminal than a silent breakage on a thousand nodes.

Objectives

  • Three levels of net, cheapest to richest (23.1).
  • The first step: --syntax-check (23.2).
  • ansible-lint: the community's wisdom in one command — profiles, false positives (23.3).
  • Check mode: the dress rehearsal in an empty theatre, with --diff (23.4).
  • The limits of check mode, and how to work around them (23.5).
  • Putting it all in order: the validation flow (23.6).
  • The good habits with validation (23.7).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap23/solution
./run.sh

Expected result

  • --syntax-check passes on site.yml.
  • ansible-lint passes on site.yml (TODO 1) at the production profile declared in .ansible-lint (TODO 3); and *fails* on the sloppy starting playbook.
  • The read task has check_mode: false (TODO 2): it runs even under --check.
  • --check --diff shows the diff of conf.txt but does *not* write the file; the real run writes it; on a rerun → changed=0.
24The throwaway stageAdvanced

What you build

Chapter 23 gave you three nets — syntax-check, lint, check mode — but none of them actually *runs* the role against a real system. A clean lint and an empty-theatre rehearsal tell you the playbook is *well written* and what it *would change*, not that the role works: that it starts from scratch, converges, is idempotent, and leaves the system in the right state. That is the wall chapter 23 cannot cross. Molecule tears it down: it stands up a real but throwaway environment (a container), applies the role, checks idempotence and result, and tears it all down — one command up, one command down. It is the rehearsal on a real stage, with the certainty that you can always rebuild it from nothing.

Objectives

  • The wall of chapter 23 and why you need a test that truly executes (24.1).
  • What Molecule is: the throwaway environment as a test bench (24.2).
  • Installation and first scenario (24.3).
  • Anatomy of a scenario: driver, platforms, provisioner, verifier (24.4).
  • The lifecycle: create, converge, idempotence, verify, destroy (24.5).
  • Writing the verifications with Testinfra: a second pair of eyes (24.6).
  • Working in phases during development (24.7).
  • More scenarios, more distributions (24.8).
  • The good habits with Molecule (24.9).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap24/solution
./run.sh

Expected result

  • molecule.yml declares the platform and the testinfra verifier (TODO 1).
  • The marker task has the creates guard (TODO 2): the idempotence phase passes (changed=0).
  • The testinfra verifications are written (TODO 3) and the verify phase passes.
  • "molecule test" is green from start to finish: create, converge, idempotence, verify, destroy — and it leaves no container behind.
25The right tempoCloud Architect

What you build

Until now you have orchestrated a handful of nodes, and with a handful every playbook feels fast. But the Cloud Architect tier opens with a different question: what happens when the nodes become a multiplied by a thousand, repeated on every deploy. The bottleneck is no longer *what* the playbook does, but *how* Ansible spreads it across the fleet. This chapter gives you the levers to tighten that time: how many nodes to drive at once (forks), whether to march them in lock-step or let them race (strategies), and how not to pay for work nobody needs (taming facts). You measure them on a real fleet — twelve nodes — and watch them bite: the same rollout drops from ~24 seconds to ~8.

Objectives

  • Why at scale the problem changes nature: not the task, but the distribution (25.1).
  • forks: how many nodes Ansible drives in parallel, and why "in waves" costs (25.2).
  • Strategies — linear vs free: the per-task barrier and how to remove it (25.3).
  • Pipelining and ControlPersist: fewer SSH round trips per task (25.4).
  • Taming facts: gather_facts off, gather_subset, fact caching (25.5).
  • Mitogen: the strategy plugin that rewrites execution — powerful and a commitment (25.6).
  • Measure, don't guess: the profile_tasks callback (25.7).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap25/solution
./run.sh

Expected result

  • ansible.cfg carries forks = 12 (TODO 1): the fleet runs in a single wave.
  • deploy.yml uses strategy: free (TODO 2): no per-task barrier.
  • deploy.yml uses gather_facts: false (TODO 3): no useless setup.
  • The tuned rollout is clearly faster than the starting one (here ~8s vs ~24s) and profile_tasks confirms it: fact gathering is gone, and the steps no longer march in lock-step.
26The stage machineryCloud Architect

What you build

Chapter 25 gave you the speed to serve a thousand nodes. But at that scale one thing has already changed under your feet: it is no longer a person who launches the playbook. With a thousand nodes and many hands on the same code, an ansible-playbook typed by hand from someone's laptop is too fragile — nobody checked the lint, nobody knows which version it starts from, nobody stops an untested change from reaching production on a Friday evening. The answer is a stage machinery: every change passes through version control, crosses a pipeline that validates it on its own (CI), and only an authorised release crosses the gate into production (CD). This chapter builds it: the quality gates on GitHub Actions, the production gate that opens only on a tag, and the pre-commit hooks that run the check before the commit even exists.

Objectives

  • What CI and CD mean, and why at scale they replace the person who launches (26.1).
  • The foundation: without version control there is no pipeline (26.2).
  • The anatomy of an Ansible pipeline: the gates in a row (26.3).
  • A concrete pipeline: GitHub Actions and the quality gates (26.4).
  • Deploy and the production gate: who takes the stage, and when (26.5).
  • GitLab CI: the same pattern, different syntax (26.6).
  • Shift left further: pre-commit hooks, the same gate before the commit (26.7).
  • Good habits with CI/CD (26.8).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap26/solution
./run.sh

Expected result

  • The test job of ci.yml runs the two quality gates on every push (TODO 1): lint.sh and validate.sh.
  • The deploy job has needs: test and the tag if (TODO 2): it does not start if CI fails, and runs only on a release tag.
  • .pre-commit-config.yaml has the local ansible-lint hook (TODO 3): the same lint fires before the commit.
  • The gates actually bite: green on the good project, red on a broken playbook — both in the pipeline and in the pre-commit hook.
27Without stopping the musicCloud Architect

What you build

Chapter 26 gave you the stage machinery that carries a change to the door of production. But the deploy job was one line of echo, and now the real question arrives: *how* do you update a fleet of a thousand nodes without taking the service down? Applying to everyone at once is an outage — for a few seconds or minutes, every backend is down at the same moment. A conductor does not stop the whole orchestra to let one violinist change a string: they bring sections in and out one at a time, and the music never stops. This is orchestration: the rolling update. This chapter gives you the levers — before you touch it), the choreography pre_tasks/tasks/post_tasks (drain → update → re-enable), and them at work on a web farm where, at every instant, no more than one wave is out of rotation.

Objectives

  • From automation to orchestration: why "apply to everyone" is no longer enough (27.1).
  • The first lever: serial and wave releases (27.2).
  • The second lever: delegate_to with the load balancer (27.3).
  • The full choreography: pre_tasks, tasks, post_tasks (27.4).
  • The emergency brake: max_fail_percentage (27.5).
  • When something goes wrong: rollback and recovery (27.6).
  • Coordinating multiple tiers and groups (27.7).
  • Good habits with orchestration (27.8).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap27/solution
./run.sh

Expected result

  • The play has serial: 2 (TODO 1): the release proceeds in waves, never the whole farm at once.
  • The play has the post_tasks that re-enable the node (TODO 2): every drained node returns to rotation.
  • The play has max_fail_percentage: 25 (TODO 3): a wave that fails stops the rollout.
  • The pool ledger confirms it: every node leaves and returns, and at every instant no more than one wave (2) is out of rotation.
28The standing theatreCloud Architect

What you build

Chapter 27 gave you the wave release; chapter 26 the pipeline that launches it. But in both it is still you — or a script — typing a command at a terminal, with inventories, credentials and "who-can-do-what" kept in your head or scattered across files. That works for one person. It does not work for an point the terminal is no longer enough — the way a troupe touring from town to town is no longer enough once the city wants a resident season, with a home, a box office, a company roster and an archive. automation stops being a gesture at a terminal and becomes a service with a console, its own permissions, and its own history. This chapter assembles its core objects — the job template, versioned and validated before they go on stage. Because the Cloud Architect way to run the platform is not "click in the UI": it is GitOps here too.

Objectives

  • Why the terminal is no longer enough at organisation scale (28.1).
  • AWX and Ansible Automation Platform: who is who (28.2).
  • The central concept: the job template (28.3).
  • Credentials, RBAC and audit: governing access (28.4).
  • Workflows: chaining jobs with success and failure branches (28.5).
  • The other building blocks: EE, scheduling, EDA (28.6).
  • Good habits with the platform (28.7).

How to test it

Fill in the TODOs in the starter files, then run the solution's test:

cd ansible/ed1/cap28/solution
./run.sh

Expected result

  • The deploy job template is complete and every reference resolves (TODO 1): project, inventory and credential exist, and the playbook is in the project.
  • The RBAC grant is least privilege (TODO 2): a narrow role (execute) on a specific resource (the job template), not admin over an organisation.
  • The workflow is a well-formed DAG with a failure branch to the rollback (TODO 3).
  • The pre-import validator accepts the graph: references resolve, secrets are referenced not written, access is scoped, the workflow is valid.