- Ansible is declarative: you state the end state, modules work out the how — Bicep thinking, not PowerShell.
- Push-based and agentless, with no state file — "changed" is computed fresh against the real machine each run.
- Bare shell tasks drag it back to scripting. Reach for a purpose-built module first.
cheat sheet
every tl;dr in order · ~10 min to skim the whole course
1 · Orientation
- The control node runs Ansible and connects OUT to managed nodes — no agent, daemon or service on targets.
- Each task copies a module to the node, executes it, removes it. Only registered vars and facts survive.
- The control node cannot be native Windows. Linux targets speak SSH; Windows targets WinRM, SSH or PSRP.
- ansible-core = the engine plus the ansible.builtin collection only. Minimal by design.
- The ansible community package = core plus a big curated bundle of collections — batteries included.
- Two version tracks: each community major pins one core series, and only the latest major is maintained.
- Since Ansible 2.10, modules live in collections — versioned packages under a namespace.
- FQCN is the full path: ansible.builtin.copy, not copy. Write it always.
- Old tutorials use bare names because they predate the split. That is how you date them.
2 · Setup
- pip install ansible (core + 85+ collections) or ansible-core (engine only); distro packages exist too.
- The control node needs a supported Python — policy is the three most recently released Python versions.
- No native Windows control node. WSL works — but the two official guides contradict each other about it.
- Every run reads ONE config file: ANSIBLE_CONFIG → ./ansible.cfg → ~/.ansible.cfg → /etc/ansible/ansible.cfg.
- First file found wins — the search stops there. Nothing merges.
- Best practice: a project-root ansible.cfg committed to Git, so everyone runs with identical settings.
- ansible <target> -m <module> runs one module against some hosts, right now. That is an ad-hoc command.
- Use it for quick checks and one-offs: ping a group, run uptime, gather a fact.
- Anything repeatable is playbook territory (unit 4) — ad-hoc is a probe, not configuration.
3 · Inventory
- The inventory is the file naming every host Ansible manages, sorted into named groups.
- Two formats — INI and YAML — describe the same thing; parent groups nest via children.
- ansible-inventory --graph shows what Ansible actually parsed. Trust the graph, not the file.
- group_vars/ and host_vars/ sit next to your inventory and auto-load by group and host name.
- Environment config lives here — per group in group_vars, pinned to one machine in host_vars.
- group_vars/all.yml hits every host in the inventory. Keep it minimal.
- Patterns pick hosts at run time: all, web*, web:&staging (in both), web:!db (web minus db).
- --limit narrows any run to the hosts you name — the re-run and debugging flag.
- One inventory directory per environment is the guardrail that stops a staging run touching prod.
- Dynamic inventory = a host list generated at run time by a plugin querying an API, not a static file.
- Inventory plugins pull from cloud sources and CMDBs, and run on the control node like every plugin.
- Static files suit estates you control by editing the file; dynamic suits estates whose truth lives elsewhere.
4 · Playbooks
- A playbook is a list of plays; a play targets hosts and holds tasks; each task calls one module.
- hosts: takes a lesson-10 pattern; -i picks the inventory that resolves it — same playbook, any environment.
- Read the output per task: ok means already in the desired state, changed means Ansible altered something.
- Indentation is structure, and it is spaces only — a tab is a parse error, not a style choice.
- A value that STARTS with {{ }} must be quoted, because a bare { opens YAML inline-dict syntax.
- Unquoted yes/no/true/false all become booleans — quote them when you mean the literal word.
- A module is a unit of work. Ansible ships it to the managed node, runs it, deletes it.
- A plugin extends the engine itself and always runs on the control node — it never leaves home.
- A collection is the shipping container: modules, roles and plugins packaged under one namespace.
- register: captures a task's full result dict into a named variable — the only state between tasks.
- Inspect .stdout, .rc and .changed; print them with ansible.builtin.debug (msg or var).
- register + debug is the everyday inspection loop: run, print, read, adjust.
5 · Logic
- The daily ladder, low to high: role defaults → inventory vars → role vars → set_fact → extra vars (-e).
- Tunables in defaults/, environment config in group_vars/, internal constants in vars/. -e only on purpose.
- Never pass secrets with -e — they leak into logs. When the wrong value wins, -vvv shows what each task used.
- Facts = per-host system data (OS, IPs, memory), auto-gathered by the setup module into ansible_facts.
- gather_facts: false skips the survey; fact caching (jsonfile/redis) reuses results across runs.
- set_fact writes your own runtime facts; /etc/ansible/facts.d adds custom facts on the node itself.
- when: takes a bare Jinja2 expression — no {{ }}. Wrapping the condition in braces is the classic mistake.
- Condition on facts or registered fields (.rc, .changed); a false condition reports skipping, not failure.
- A YAML list under when: means AND; combined with loop, the condition is evaluated per item.
- loop: runs one task once per list element; the current element lands in the item variable.
- with_* is legacy, not deprecated — it still works and ansible-lint only warns. Write loop: in new code.
- loop_control fixes noisy output: label picks what each status line prints.
- notify: queues a handler — a task that runs only when the notifying task reports changed.
- Queued handlers run once, at the end of the play, however many tasks notified them.
- listen: turns the notify string into a topic several handlers can subscribe to.
- A failed task drops that host from the play — every other host carries on. Containment is per host.
- failed_when redefines failure from the result; ignore_errors just barrels past a real one.
- block groups tasks; rescue runs on failure, always runs regardless — try/catch/finally.
6 · Making it real
- Modules check current state and act only on the difference — re-runs converge, ending changed=0.
- command and shell always execute and always report changed — fix with creates:, removes:, changed_when:.
- The working test: run the playbook twice — an idempotent second run ends all ok, changed=0.
- template renders a .j2 file with each host's variables on the control node; copy ships bytes verbatim.
- A filter transforms a value inside {{ }} via a pipe — default() and join are the everyday pair.
- {{ }} prints, {% %} controls, | transforms — that triad reads any .j2 file you will meet.
- A role packages tasks, handlers, templates, files and vars in a fixed layout; every main.yml auto-loads.
- defaults/ holds user-tunable knobs (lowest precedence); vars/ holds internal constants (high).
- Roles never name hosts — playbooks orchestrate roles against hosts. And a role is not a collection.
- import_tasks/import_role are static: resolved at parse time. include_tasks/include_role: at runtime.
- Default to import for parse-time checking; use include for variable filenames, loops, per-host paths.
- Bare include: was removed in ansible-core 2.16 — playbooks still using it fail to parse.
- ansible-galaxy installs collections and roles; requirements.yml is the manifest a project commits.
- One command restores every dependency: ansible-galaxy install -r requirements.yml.
- Pin versions — unpinned installs grab whatever is latest that day, and machines drift apart.
7 · Secrets & scale
- ansible-vault encrypts secrets at rest so they can live in Git; encrypt, decrypt, edit, view are the verbs.
- Encrypt whole var files or single values (!vault); unlock runs with --ask-vault-pass or a password file.
- Vault IDs label multiple keys, so dev and prod secrets can use different passwords in one run.
- linear (default) marches every host through a task before the next starts; free lets each host race ahead.
- serial: 1 turns a play into a rolling update — the whole play finishes on one batch before the next begins.
- delegate_to runs a task on a different host; run_once runs it a single time for the whole play.
- --check dry-runs the playbook: modules report what would change without changing anything.
- --diff adds line-by-line file diffs; tags: plus --tags runs a named slice of the tasks.
- The pre-prod ritual: --syntax-check, --list-tasks, --check --diff, then the real run.
8 · Windows track
- Three plugins reach Windows: winrm, psrp (both ride WinRM) and ssh — chosen per host in inventory.
- WinRM listens on 5985 (HTTP) / 5986 (HTTPS); unencrypted channels are refused, HTTPS is recommended.
- SSH is officially supported since core 2.18 but needs OpenSSH 7.9+ — in practice Server 2022 or newer.
- WinRM offers five auth options — Basic, Certificate, Kerberos, NTLM, CredSSP — differing on four properties.
- Kerberos is the official recommendation in a domain; Basic and NTLM only over HTTPS for local accounts.
- You pick with ansible_winrm_transport — or ansible_psrp_auth, which adds a negotiate option.
- Server 2016 / Windows 10 ship PowerShell 5.1 — no bootstrap needed on any current Windows target.
- ConfigureRemotingForAnsible.ps1 lives on in tutorials, but official docs now warn against using it.
- Modern setup: Enable-PSRemoting, explicit firewall rule, token filter policy, HTTPS listener, win_ping.
- Windows modules live in ansible.windows (core) and community.windows (extended) — not ansible.builtin.
- Your playbook habits transfer; module names don't — copy becomes ansible.windows.win_copy.
- win_chocolatey is NOT in ansible.windows — it ships in the vendor collection chocolatey.chocolatey.
- On Windows, become means become_method: runas — a fresh logon as become_user, not a sudo wrapper.
- become also bypasses WinRM restrictions (WUA, DPAPI, non-interactive limits) — key for lesson 35.
- Double-quoted "C:\temp" turns \t into a tab. Single-quote Windows paths and backslashes stay literal.
- WinRM hands the target a network logon: proof of who you are, but no credentials to pass onward.
- Any task reaching a second server — share, AD, SQL — dies with Access is denied: the double hop.
- Three fixes: Kerberos delegation, CredSSP, or become with explicit credentials.
9 · Workplace ecosystem
- The standard repo: ansible.cfg at root, inventories/<env>/, site.yml, roles/, two requirements.yml.
- One inventory directory per environment makes cross-env accidents physically impossible.
- Day one in a strange repo, read: ansible.cfg → inventories/ → site.yml → roles/.
- ansible-lint with profile: production — the strictest preset — is the bar real repos hold PRs to.
- yamllint catches indentation and line-length slips; ansible-lint catches Ansible-specific bad practice.
- Every skip in .ansible-lint needs a documented reason — undocumented skips get flagged in review.
- Molecule is the standard role test harness: boot a container, converge, check idempotence, verify, destroy.
- The idempotence phase is lesson 22's run-twice test, automated: the second run must report changed=0.
- Teams run it in CI on every PR. Linting is a separate step, not part of the default test sequence.
- AWX is the free open-source upstream; AAP is Red Hat’s supported enterprise product.
- “Tower” is a retired name — last version 3.8. When colleagues say it, they usually mean AAP.
- Both give job templates, workflows, surveys, credentials, RBAC, schedules and a /api/v2/ REST API.
- An EE is a container image: ansible-core + collections + their Python/system deps, built by ansible-builder.
- ansible-navigator runs playbooks inside an EE by default — the image tag pins your whole runtime.
- Inside an EE, localhost is the container: files "saved locally" vanish when the container is removed.
- Two ways in: shell out to ansible-playbook on the agent, or the Jenkins plugin's ansiblePlaybook step.
- The pipeline shape: checkout scm, ansible-galaxy install -r requirements.yml, then ansiblePlaybook.
- Secrets — SSH keys, vault passwords — live in the Jenkins credential store, referenced by ID.
- Debug a broken run in a fixed order: reachability, inventory, syntax, dry run, narrow, inspect.
- The day job is patching, packages/services, templated config, users, onboarding hosts, deploys.
- Mid-level interviews test concepts and scenarios, not custom module authoring. RHCE is Linux-only.