Three separate decisions
A configuration may have valid syntax yet fail the business requirement. Separate candidate-file validation, byte installation and service acceptance. In this exercise the candidate has exactly worker and port; the worker must belong to the expected set and the port must lie between 1024 and 65535. These rules belong to the fictional scenario. They are not a universal banking application configuration. The Python validator reads the temporary path supplied through validate. If it rejects port 70000, the template must not replace the previous worker.conf.
Observe initial execution and repetition
Run converge.yml with service_port=8443. The template installs the file with mode 0600 and notifies the handler that appends one activation-log line. flush_handlers places that effect before final inspection. In the lab, a line means only that the handler executed; no application process is restarted. Repeat the same command and compare content, mode and line count. The recap should report zero changes and the log should retain one line per worker. If the template included a changing timestamp, the repeat would no longer represent the same input even if functional intent had not changed.
Inject an invalid proposal
The runner saves approved bytes before trying port 70000. Execution must fail and both destinations must retain identical bytes. It also confirms that the activation log did not grow. This combination is more useful than finding the word failed alone: it demonstrates candidate rejection and preservation of observed state. A validator that always examines the installed file while ignoring %s could approve a bad candidate because it read the old version. Deliberately test that case when reviewing a team validator.
Interpret check mode
The next rehearsal proposes 9443 with --check. The actual file remains at 8443. A changed prediction does not mean the service changed; the command handler is not activation evidence in this mode either. Inspect tasks with check_mode: false before treating a rehearsal as non-mutating. In production, add a functional probe after activation and define what happens if syntax passes but reconciliation fails. Record valid candidate, installed configuration, healthy process and accepted business outcome separately.
# File: converge.yml
- name: Validate and converge synthetic worker configuration
hosts: workers
gather_facts: false
vars:
node_dir: "{{ lab_root }}/{{ inventory_hostname }}"
tasks:
- name: Create isolated worker directory
ansible.builtin.file:
path: "{{ node_dir }}"
state: directory
mode: '0700'
- name: Validate before replacing the destination
ansible.builtin.template:
src: config.j2
dest: "{{ node_dir }}/worker.conf"
mode: '0600'
validate: "{{ ansible_playbook_python }} {{ playbook_dir }}/validate.py %s"
notify: Record activation
- name: Finish activation before acceptance
ansible.builtin.meta: flush_handlers
- name: Inspect persisted file content
ansible.builtin.stat:
path: "{{ node_dir }}/worker.conf"
checksum_algorithm: sha256
register: observed
- name: Require a private regular file after normal execution
ansible.builtin.assert:
that:
- observed.stat.isreg
- observed.stat.mode == '0600'
when: not ansible_check_mode
handlers:
- name: Record activation
ansible.builtin.command:
argv:
- "{{ ansible_playbook_python }}"
- -c
- "import pathlib,sys; p=pathlib.Path(sys.argv[1]); p.open('a').write('activated' + chr(10))"
- "{{ node_dir }}/activation.log"
# File: config.j2
worker={{ inventory_hostname }}
port={{ service_port }}
# File: validate.py
import pathlib,sys
lines=pathlib.Path(sys.argv[1]).read_text.splitlines
values=dict(line.split('=',1) for line in lines)
assert set(values)=={'worker','port'}
assert values['worker'] in {'worker_a','worker_b'}
assert 1024<=int(values['port'])<=65535
Port 70000 fails before replacement; previous hashes and log remain unchanged.
Common pitfalls
Validating the old destination; activating before validation; counting a prediction as an applied change.
Related topics: Executable rehearsal: scope, state and limits · Archives, residual files and markers · Recovery and change outcome
Validate the candidate and observe installation, activation and acceptance with separate criteria.
Reference: Template module · EX294 current objectives inspected 2026-09-30; RHCE in Ansible framework effective 2026-05-11; booking product version not publicly pinned