16. Cheat sheet
Ansible · 16. Cheat sheetOne page. Everything from the fifteen chapters before it.
The model
Section titled “The model” INVENTORY which machines exist, and their groups ↓ PLAYBOOK a list of PLAYS ↓ PLAY hosts + tasks + handlers ↓ TASK a MODULE plus arguments ↓ MODULE copied to the target, run, deleted. Prints ONE LINE OF JSON. Idempotence is ITS job, not Ansible's.Commands
Section titled “Commands”ansible-playbook site.ymlansible-playbook site.yml -i inventory/productionansible-playbook site.yml --limit web1 # SAFETY FLAGansible-playbook site.yml --check --diff # dry runansible-playbook site.yml --tags nginxansible-playbook site.yml --skip-tags packagesansible-playbook site.yml --start-at-task "Deploy config"ansible-playbook site.yml --stepansible-playbook site.yml --list-hosts --list-tasks --list-tagsansible-playbook site.yml --syntax-checkansible-playbook site.yml -e "version=2.4.0" # always winsansible-playbook site.yml -f 50 -vvansible-playbook site.yml --force-handlersansible-playbook site.yml --flush-cache
ansible all -m ansible.builtin.pingansible all -m ansible.builtin.shell -a "df -h /" -oansible web1 -m ansible.builtin.setup
ansible-inventory --graphansible-inventory --host web1ansible-config dump --only-changedansible-doc ansible.builtin.copyansible-galaxy collection install -r requirements.ymlansible-lintansible-vault edit group_vars/production/vault.ymlExit codes
Section titled “Exit codes” 0 success 1 an error 2 one or more hosts FAILED 4 one or more hosts UNREACHABLE 99 interrupted2 and 4 mean different things. Treat them differently in CI.
Inventory patterns
Section titled “Inventory patterns” all / * everything webservers a group webservers:databases union (OR) webservers:&london intersection (AND) webservers:!web3 exclusion web*.example.com glob ~web\d+ regex webservers[0] first host — the canary webservers[0:2] a sliceVariable precedence — the five rules
Section titled “Variable precedence — the five rules” 1. -e ALWAYS wins (level 22) 2. role defaults are WEAKEST (level 2) role vars are near-STRONGEST (level 15) 3. more specific beats more general (host_vars > child group > parent > all) 4. among siblings: later alphabetically wins 5. set_fact beats almost everything (level 19)
→ defaults/ is the role's API. vars/ is its private state.Play keywords
Section titled “Play keywords”- hosts: webservers become: true gather_facts: true serial: [1, 5, '25%'] # canary → batches max_fail_percentage: 0 # per BATCH, not per play any_errors_fatal: false strategy: linear # free | host_pinned order: inventory # sorted | shuffle | ... force_handlers: true environment: {http_proxy: "..."} vars_files: [vars/main.yml]Run order: facts → pre_tasks → handlers → roles → tasks → post_tasks → handlers.
Roles run before tasks regardless of file order.
Task keywords
Section titled “Task keywords”- name: Ensure X is present # ALWAYS write a name ansible.builtin.module: # ALWAYS use the FQCN arg: value become: true when: condition # bare — no {{ }} loop: "{{ items }}" loop_control: {label: "{{ item.name }}"} register: result changed_when: false failed_when: result.rc not in [0, 2] check_mode: false no_log: true notify: Handler Name tags: [x] until: result.status == 200 retries: 30 delay: 5 delegate_to: localhost run_once: true # per BATCH, not per play throttle: 2 async: 3600 poll: 0Handlers — the five rules
Section titled “Handlers — the five rules” 1. notification requires `changed` 2. they run at the END of the play 3. in DEFINITION order, not notify order 4. NOT AT ALL if the host failed → --force-handlers / force_handlers: true 5. per play — notifications do not cross plays
meta: flush_handlers runs them now listen: "name" many handlers, one notificationIdempotence
Section titled “Idempotence” command / shell ALWAYS report changed. Fix, in order:
1. use the real module ← nearly always possible 2. creates: / removes: ← skip if path exists 3. changed_when: false ← for read-only commands
ignore_errors = "failure does not matter" (rarely true) failed_when = "here is what failure means"Test it: run twice, expect changed=0.
import vs include
Section titled “import vs include” import_* include_* ────────────────────────────────────────────────── when processed parse time run time variable filename NO YES in a loop NO YES `when:` applies to every task in the include only the file tags apply to every task in the include only the file → need `apply:` --list-tasks shows yes no
→ import by default. include for runtime behaviour.Jinja essentials
Section titled “Jinja essentials”{{ x | default('y') }} undefined → y{{ x | default('y', true) }} undefined OR empty → y{{ x | mandatory }} fail loudly if undefined{{ x | default(omit) }} drop the argument entirely{{ x | bool }} "false" string → False{{ a | combine(b, recursive=true) }}{{ l | selectattr('k','equalto','v') | map(attribute='n') | list }}{{ d | to_nice_yaml(indent=2) }}{{ s | quote }} shell-safe
when: x is defined / is not defined / is skipped / is failedwhen: v is version('2.4', '>=') ← the ONLY correct version comparison{# at the top of a template #}#jinja2: trim_blocks: True, lstrip_blocks: TrueAlways finish map/select with | list — they return generators.
Lookups run on the CONTROL node, never the target.
[defaults]forks = 50 # from 5 — biggest single wingathering = smartfact_caching = jsonfilefact_caching_connection = /tmp/ansible_fact_cachefact_caching_timeout = 7200stdout_callback = ansible.builtin.yamlcallbacks_enabled = ansible.posix.profile_tasksinterpreter_python = auto_silentroles_path = ./roles:./galaxy_roles
[ssh_connection]pipelining = true # 30-50%, one settingssh_args = -C -o ControlMaster=auto -o ControlPersist=600s \ -o PreferredAuthentications=publickeyAlso: pass lists to package/apt/pip instead of looping.
ansible.cfg order — first found wins entirely, no merging:
$ANSIBLE_CONFIG → ./ansible.cfg → ~/.ansible.cfg → /etc/ansible/ansible.cfg
A world-writable directory is skipped silently.
Repository layout
Section titled “Repository layout” ansible.cfg requirements.yml ← pin versions, commit it inventory/ production/{hosts.yml,group_vars/,host_vars/} staging/{hosts.yml,group_vars/} roles/ ← yours galaxy_roles/ ← installed, GITIGNORED playbooks/site.yml
group_vars/production/ main.yml db_password: "{{ vault_db_password }}" vault.yml vault_db_password: ... ← encryptedOne inventory directory per environment makes the wrong-environment mistake impossible rather than merely discouraged.
Secrets
Section titled “Secrets”ansible-vault create / edit / view / rekey file.ymlansible-vault encrypt_string 'x' --name 'vault_x'ansible-playbook site.yml --vault-id prod@~/.vault_prod ✓ prefix vault variables with vault_ ✓ reference indirectly from a plain file ✓ vault IDs per environment ✓ no_log: true on anything touching a secret ✓ loop_control: label on loops over credentials ✓ mode: '0600' on rendered files with secrets
✗ NEVER ansible-vault decrypt — it writes plaintext in place, under the same name ✗ never a secret on a command line (process list) ✗ never commit .vault_passSymptom → cause
Section titled “Symptom → cause” succeeded, did nothing → host pattern matched nothing. Exit code 0. ansible-inventory --graph
variable will not change → it is in role vars/ not defaults/ (or set_fact, or -e, or a group_vars file named for a group that does not exist)
handler never runs → the task reported `ok`, not `changed` (80%) or the host failed → --force-handlers
second run reports changes → command without changed_when, self-matching lineinfile regex, or mode: 0644 UNQUOTED
"dict object has no attribute stdout" → the task was SKIPPED. `is not skipped` or check_mode: false
`when` is always true → it is the STRING "false". Use | bool
tags run too much / too little → tags do not reach inside include_*. Use apply: --list-tasks --tags to check
Failed to connect via ssh → -vvvv, find "SSH: EXEC", run that command by hand
/usr/bin/python not found → bootstrap with `raw`, then pin ansible_python_interpreter
sudo: a password is required → -K, or passwordless sudo. And ALWAYS validate: 'visudo -cf %s' on sudoers files
twenty minutes for nothing → forks=5, pipelining off, uncached facts, or a loop that should be a list. profile_tasks.Debugging order
Section titled “Debugging order”1. --list-hosts --list-tasks what will run?2. --check --diff --limit web1 what would change?3. ansible-inventory --host web1 what does it believe?4. -vvv --limit web1 what is it doing?5. --step one task at a time6. --start-at-task "..." resume from the break7. --limit @site.retry re-run only failuresThe twenty rules
Section titled “The twenty rules”1. Always name your tasks. Phrase as desired state.2. Always write the FQCN.3. changed_when: false on every read-only command.4. Use the real module before command; creates: before changed_when.5. Quote file modes: '0644', never 0644.6. defaults/ for anything a caller might change. Never vars/.7. Prefix role variables with the role name.8. Never begin a variable with ansible_.9. | bool on every boolean from outside a YAML file.10. | list after every map or select.11. Pass lists to modules; do not loop them.12. loop_control: label on any loop over dictionaries.13. import_* by default; include_* only for runtime needs.14. validate: on template/copy. Mandatory for sudoers.15. no_log: true on anything touching a secret.16. --limit on every ad-hoc command. There is no --check.17. force_handlers where a half-applied config is worse than a failure.18. Assert preconditions at the top of a role, tags: always.19. tags: [never] on anything destructive.20. Run twice. The second run must report changed=0.The three sentences
Section titled “The three sentences”A script describes an action; a playbook describes a destination. Every failure of the for-loop-over-SSH approach comes from that one distinction, and every feature in Ansible follows from choosing the second.
Idempotence belongs to the module, not to Ansible. package checks before it acts; command cannot. Almost all bad Ansible is command doing something a real module already does, and the cost is not slowness — it is that changed stops meaning anything.
Convergence is not reproducibility. Ansible reduces drift; only rebuilding eliminates it. If you can throw the machine away, do that — and use Ansible to build the image. What is left over is machines that must persist, the network they sit on, and every place a container platform is not on offer. That is still a great deal of the world.
That is Ansible, and the twenty-fourth guide on this site.