Real World Patterns
This chapter doesn’t introduce much new syntax — one new test, is version, and that’s about it. Everything else here is composition: taking facts, variables, filters, register, and the tests from this section, and combining them the way real playbooks actually do. If a pattern below feels like it’s reaching back into three or four earlier chapters at once, that’s deliberate — this is what all of it was building toward.
Pattern: OS-Conditional Tasks
The most common real reason anyone reaches for when: at all — different distributions need different commands:
- name: OS-Conditional Demo
hosts: servers
tasks:
- name: Debian-family specific step
ansible.builtin.debug:
msg: "Using apt on this Debian-family host"
when: ansible_facts['os_family'] == 'Debian'
- name: RedHat-family specific step
ansible.builtin.debug:
msg: "Using dnf on this RedHat-family host"
when: ansible_facts['os_family'] == 'RedHat'ok: [ubuntu] => {
"msg": "Using apt on this Debian-family host"
}
skipping: [ubuntu]
skipping: [fedora]
ok: [fedora] => {
"msg": "Using dnf on this RedHat-family host"
}In a real playbook, those debug tasks would be actual command invocations of apt-get and dnf — dedicated package-management modules exist and are the properly idiomatic choice once you meet them, but the conditional logic controlling which branch runs is exactly what’s shown here, unchanged.
Pattern: Comparing Versions Correctly With is version
Here’s a genuinely surprising trap. Distribution and software versions look like numbers, but they aren’t reliably comparable as either plain numbers or plain strings:
vars:
distro_version: "9.10"
tasks:
- name: Naive string comparison — this is WRONG
ansible.builtin.debug:
msg: "Is 9.10 greater than 9.9? {{ distro_version > '9.9' }}"Is 9.10 greater than 9.9? FalseThat’s backwards. 9.10 is genuinely a later version than 9.9 — but compared as plain strings, character by character, '1' (the second character of "9.10") comes before '9' (the second character of "9.9"), so the string comparison says "9.10" < "9.9". It’s not a small edge case either — this exact trap catches multi-digit minor versions constantly.
The fix is a dedicated test built specifically for this:
- name: Correct version comparison
ansible.builtin.debug:
msg: "Is 9.10 greater than 9.9? {{ distro_version is version('9.9', '>') }}"Is 9.10 greater than 9.9? Trueis version(comparison_value, operator) understands version semantics — comparing version segments numerically, not the whole string character by character. Any time you’re comparing a version number for real, this is the test to reach for, never a plain > on the raw strings.
Pattern: Detecting And Reporting Failure
Combining register, ignore_errors, and the result tests from earlier in this section into a genuine monitoring-style check:
- name: Attempt a risky operation
ansible.builtin.command: /bin/false
register: risky_result
ignore_errors: true
- name: Alert on failure
ansible.builtin.debug:
msg: "ALERT: the operation failed on {{ inventory_hostname }}"
when: risky_result is failed
- name: Confirm success
ansible.builtin.debug:
msg: "Operation succeeded on {{ inventory_hostname }}"
when: risky_result is succeededinventory_hostname — the magic variable from the variables section — makes the alert message genuinely useful across a multi-host run, naming exactly which host had the problem, rather than a generic message that could apply to any of them.
Pattern: Acting Only When Something Actually Changed
The copy module’s real idempotency, put to work exactly the way a real deployment would:
- name: Deploy configuration
ansible.builtin.copy:
src: app.conf
dest: /etc/app.conf
register: config_result
- name: Restart the service only if config actually changed
ansible.builtin.debug:
msg: "Would restart the service now"
when: config_result is changedNote
This is exactly the problem a feature called handlers solves more elegantly, once you meet them later in this course — but the underlying logic, “only act if this specific thing actually changed,” is identical either way. Understanding it built from raw pieces here makes the dedicated feature much easier to appreciate later.
Pattern: Combining A Feature Flag With An Environment Check
The list form of when: from the previous chapter, applied to a genuinely common real scenario — a behavior that should only activate in a specific environment, and only when explicitly enabled:
vars:
environment_type: staging
enable_debug_logging: true
tasks:
- name: Enable verbose logging only in staging with the flag on
ansible.builtin.debug:
msg: "Verbose logging enabled"
when:
- environment_type == 'staging'
- enable_debug_loggingBoth conditions have to hold — a production host with enable_debug_logging: true still wouldn’t trigger this, and neither would a staging host with the flag turned off.
Pattern: Defensively Validating Configuration Before Using It
This last one deliberately reaches back across two entire sections at once — is defined and is mapping from this section, and the general discipline of not trusting a data structure’s shape from the manipulating-data section:
vars:
app_config:
port: 8080
tasks:
- name: Only proceed if the config looks genuinely valid
ansible.builtin.debug:
msg: "Config is valid — using port {{ app_config['port'] }}"
when:
- app_config is defined
- app_config is mapping
- app_config['port'] is definedThree checks, each guarding against a different way this could have gone wrong: the variable not existing at all, existing but being the wrong shape entirely (a string or a list instead of a dictionary), or being a dictionary that’s still missing the one key this task actually needs.
What You Just Did
Look back at what just got combined, chapter by chapter: facts (variables section) drove the OS branching; is version solved a real string-comparison trap; register plus ignore_errors plus result tests (this section) built a genuine failure-alerting check; copy’s idempotency (the playbook chapter, all the way back in the first section) powered the change-detection pattern; the list form of when: (previous chapter) combined a feature flag with an environment check; and is defined/is mapping closed the loop on defensively handling data whose shape you can’t fully guarantee. None of this required new modules or exotic syntax — it’s the same handful of tools from across this entire course, composed.
Best Practices
- Reach for
is version, never a plain comparison operator, any time you’re comparing version numbers — this is one of the easiest real bugs to introduce silently, and one of the easiest to prevent entirely once you know the trap exists. - Build alerting and change-detection patterns from
registerplus the appropriate result test, rather than assuming you need a specialized feature for it — you often don’t, especially while you’re still learning what’s actually needed. - Layer defensive checks (
is defined, then type, then specific keys) in that order — checking a key’s existence on a variable that might not even be a dictionary yet is a mistake it’s easy to make backward.