1. Identify the logical target and execution location
A fictional team needs to remove servers from a pool during an update. The pool change occurs at a control point, but each decision concerns an application server. Draw two columns: the server being handled and the location where the operation executes. In the lab, four worker aliases and a sink alias use one machine through local connections. Reports are written through sink while inventory_hostname retains the originating identity. This lets you study the association without claiming five machines, SSH or a real load balancer. Before production automation, confirm resolved inventory, delegated destination and credentials used at that destination. A task’s display name does not replace this confirmation.
2. Define what once means
In the trial, serial: 2 divides four workers into two ordered batches. A run_once task writes two records, one per batch. That can be correct for batch preparation and wrong for a global migration. A second task explicitly checks the first host in the whole selection and writes one record. If selection is limited to worker_b and worker_d, that first host becomes worker_b. Do not hard-code worker_a as a universal leader without considering limits, failures and inventory changes. Neither run_once nor a play condition provides exclusion between two independent ansible-playbook processes. For a nonrepeatable global operation, define external coordination, idempotency or appropriate locking as well as local selection.
3. Avoid collisions at a shared destination
Delegating four tasks to one destination does not turn them into one task. If every worker generates a complete configuration for the same file, a later write can replace an earlier one. Reducing concurrency can prevent temporal overlap but does not fix that composition error: four sequential replacements can still retain only the final content. The lab writes one report per origin and checks all four files exist. Another design is to collect results and generate one complete document in a coordinated step. Define which hosts belong in that document, how failed hosts are handled and when collection is ready. The trial does not reproduce a random race; it demonstrates a structure that makes coverage observable without sharing the output file.
4. Make value ownership explicit
A delegated task can produce information about a different system from the one handled by the play. By default, the exercise’s delegated set_fact associates origin_fact with the worker. The next task uses delegate_facts: true and associates sink_fact with sink. Reports show both results through explicit hostvars, avoiding confusion among originating identity, destination values and global values. This distinction matters when an application play retrieves information from a database or control point. Before using the value in another template, confirm its host ownership, when it was produced and which observation it represents. A name such as db_status does not establish that the value was stored on the database host or describes current state.
5. Turn execution into change criteria
For handover, deliver more than an error-free recap. Record selected hosts, batches, global tasks, delegated destinations, expected effects and per-origin outcomes. In a fictional six-node service, two nodes per batch do not establish that the remaining four can carry traffic or occupy independent failure domains. Acceptance depends on measured service capacity, health, traffic and rollback criteria. Use the lab to predict how many operations will be requested, then exercise the actual component in an authorized environment. If a global operation fails, policy should specify whether to stop the change, retain a partial state or recover. Do not infer an availability outcome from playbook syntax.
# Local teaching example; four aliases use one machine. No SSH or services.
# Save the following inventory as inventory.ini:
# [workers]
# worker_a ansible_connection=local
# worker_b ansible_connection=local
# worker_c ansible_connection=local
# worker_d ansible_connection=local
# Run: ansible-playbook -i inventory.ini batches.yml
---
- hosts: workers
gather_facts: false
strategy: linear
order: sorted
serial: 2
tasks:
- name: Observe one result per batch
ansible.builtin.debug:
msg:
origin: "{{ inventory_hostname }}"
batch: "{{ ansible_play_batch }}"
run_once: true
- name: Observe one result for the selected play
ansible.builtin.debug:
msg: "Selected leader: {{ inventory_hostname }}"
when: inventory_hostname == ansible_play_hosts_all[0]
# Repeat with --limit worker_b,worker_d and compare the selected leader.
# debug runs on the controller; this snippet does not demonstrate remote delegation.
Four workers, serial 2 and run_once produce two records; a whole-selection condition produces one. With limit worker_b,worker_d, worker_b becomes the leader.
Common pitfalls
Shared destination treated as single execution; throttle treated as file composition; fixed first host treated as universal leader; recap treated as availability.
Related topics: Facts, caching and current evidence
Separate identity, execution, frequency and data ownership before accepting a coordinated change.
Reference: Delegation, execution context and fact ownership · EX294 current objectives inspected 2026-09-30; RHCE in Ansible framework effective 2026-05-11; booking product version not publicly pinned