cswork / libraryOpen practice workspace

Articles

Ansible Interview Scenarios: Handlers, Serial Batches, and Check Mode

Practice Ansible interview scenarios with executed handler timing, serial batches, run_once behavior, failure handling, and honest check-mode boundaries.

Ansible scenario questions are tests of execution order. A good answer predicts what runs on each host, in each serial batch, and at each handler boundary before discussing syntax. Three phrases cause many mistakes: run_once means once for the current batch under serial, notified handlers normally wait for a flush point, and check mode is only as safe and informative as the modules and tasks involved.

This guide combines current Ansible documentation with an original four-host localhost exercise executed using ansible-core 2.21.4. The playbook is practice material, not an employer prompt or production deployment. Start with Design Safe Configuration and Deployment for Hundreds of Services to frame the rollout risk before tracing mechanics.

Predict serial batches before task counts

Official behavior: serial divides the active hosts into batches. With four hosts and serial: 2, the play processes two hosts, then the next two. Every normal task runs for eligible hosts in the current batch before the play advances.

Write the batches first: [node1,node2], then [node3,node4]. Now annotate every task with its host loop. A configuration copy runs four times overall. A run_once coordinator task executes on the first available host in each batch, so it runs twice overall—not once for the entire play. Its result and facts can be applied to other active hosts in that batch.

If a task truly must run once for the whole rollout, run_once plus serial is an unsafe shorthand. Put global work in a separate play, target an explicit coordinator, or gate it with a stable condition independent of the batch. Explain how retries behave if the coordinator fails.

Trace handler notifications and flush points

Handlers run when notified by changed tasks. Multiple notifications of the same handler for a host are coalesced until the next flush. Normal flush points occur after major play sections; meta: flush_handlers forces pending handlers to run at that location.

In the practice play, writing each host’s configuration notifies record restart. The next task writes one coordinator file per batch with run_once. Then flush_handlers runs the pending restart handler for both hosts in that batch before the next batch begins.

The first execution produced four configuration files, four restart markers, and two batch coordinator files. The second execution reported changed=0, so the copy tasks did not notify the handler again. This is observed local evidence for this playbook, not a guarantee that arbitrary commands are idempotent.

If another changed task notifies the same handler after an early flush, the handler can run again at a later flush. “A handler runs only once per play” is therefore too broad. The useful rule is that duplicate pending notifications are coalesced between flushes for each affected host.

Explain failure and force_handlers carefully

By default, a later failure on a host can prevent its notified handlers from running. This can leave a changed configuration without the intended restart. Ansible’s force_handlers option can make notified handlers run even on hosts with failed tasks, subject to conditions such as unreachable hosts.

Do not recommend force_handlers automatically. Ask whether running the handler after partial configuration is safe. A restart could activate an invalid file. A stronger rollout validates the rendered configuration before notification, writes atomically, and uses rescue or rollback behavior when validation fails.

With serial batches, failure policy also affects whether later batches begin. Explain the blast radius explicitly: which hosts changed, which handlers ran, which hosts remain untouched, and what evidence is required before continuing.

Treat check mode as a prediction, not a sandbox

Official behavior: check mode asks supporting modules to predict changes without modifying targets. Support is module-specific. A task marked check_mode: false runs normally even during a check-mode invocation, so dry-run syntax is not a universal safety barrier.

The local exercise completed with --check --diff, but that only describes the modules and inputs used. Some modules may return incomplete data, skip operations, contact external systems, or lack meaningful check support. Registered variables may differ because the real action did not occur.

Before using check mode against important hosts, inspect every module’s attributes, identify tasks forced out of check mode, and use disposable inventory first. Separate “the playbook parsed,” “the module predicted a change,” and “the real system would reach the desired state.” Those are different claims.

Diff mode can reveal proposed file changes, which may include secrets. Protect CI logs and interview recordings. Use obvious dummy values in practice and explain why sensitive templates need redaction.

Work through the original four-host trace

The play creates an isolated directory under /tmp, copies mode=ready per host, notifies a marker handler, writes one coordinator record per batch, and flushes handlers. All inventory entries use local connection; no remote credentials or production systems are involved.

StageBatch oneBatch twoExpected total
Configuration copynode1, node2node3, node4Four files
run_once coordinatorfirst available hostfirst available hostTwo files
Explicit handler flushtwo pending hoststwo pending hostsFour markers on first run
Second identical runno content changeno content changeNo handler notification

The executed checks verified those outcomes and a completed check-mode run. The play does not prove network concurrency, rolling health checks, service restart correctness, or behavior after unreachable hosts. Those require additional controlled scenarios.

Diagnose common scenario mistakes

When asked “How many times does this run?”, do not count YAML lines. Expand host batches, evaluate conditionals per host, mark delegation, then place flush points. Include failures that remove hosts from the active set.

When asked whether a playbook is idempotent, distinguish module idempotence from workflow idempotence. A copy task can be idempotent while a notified command always changes external state. changed_when only changes reporting; it does not make a side effect safe to repeat.

When asked about delegate_to, remember that delegation changes where a task executes, not automatically how many times Ansible schedules it. Combine it deliberately with a once condition if only one control-plane action is intended.

For check mode, name the unsupported boundary and the validation alternative. You might render templates locally, lint syntax, test in containers, use a canary batch, validate service health, and then expand. A dry-run is one layer, not the release strategy.

Structure a strong interview answer

Use four passes. First, state inventory and serial batches. Second, produce the per-host task trace. Third, explain handler notifications, flushes, and failure behavior. Fourth, separate locally observed results from production assumptions.

If requirements change—such as “restart after each batch only if every host validates”—move the flush after validation and define batch failure policy. Say whether successful hosts should roll back when one peer fails. The answer should connect syntax to operational consequences.

End with a verification plan: lint and syntax check, disposable check mode, first real canary batch, handler evidence, application health, and controlled continuation. State exactly what would stop the rollout.

Answer four variations without memorizing slogans

Variation one: two tasks notify the same restart handler in one batch. Assuming no intervening flush, each affected host has one pending instance of that handler. The handler runs once for that host at the flush point. If the first task changes only node1 and the second changes only node2, both hosts still have their own pending notification. Coalescing is not a global restart shared by every host.

Variation two: the play flushes after the first change, then a later task changes and notifies again. The first pending handler runs at the explicit flush. The later notification creates new pending work, so the same handler may run at the next flush. This is why an answer based only on the number of handler names is insufficient; timing changes the count.

Variation three: a run_once database migration appears inside a play with serial: 2. It can execute once in each batch. That may attempt the migration twice. Move the migration to a separate single-host play, use an explicit coordinator with a stable guard, or make the operation safely repeatable. Then explain what other hosts may do if the migration fails. Simply adding delegate_to does not remove the per-host scheduling loop.

Variation four: check mode reports no change, but the real rollout fails. Investigate whether the relevant module has full check support, whether conditional data differed, whether a command was skipped, and whether runtime validation depends on a service or external API. A clean preview is evidence about predicted module behavior, not proof of a successful deployment.

These variations share one method: enumerate the scheduling unit, state the pending state, identify the boundary that changes it, and predict an observable artifact. This is more reliable than recalling isolated statements such as “handlers run at the end” or “run_once means once.”

Extend the local exercise safely

Add a validation task after copying configuration but before the handler flush. Make one host’s dummy configuration invalid and record which hosts reach the flush. Repeat with force_handlers only after defining whether restarting with an invalid file is acceptable. The point is to observe the trade-off, not to prove that forced handlers are always safer.

Next, add a percentage serial schedule such as 10%, then inspect how Ansible calculates batch sizes for the chosen inventory. Do not extrapolate a four-host result to hundreds without checking rounding and current documentation. Record the actual ansible_play_batch at each stage.

Finally, add one deliberately unsupported or externally observable check-mode task in a disposable environment. Compare normal and --check --diff output, including registered values and subsequent conditionals. Keep the target local and use dummy data. The result should teach you what the selected module reports; it should never be used to claim that check mode prevents every side effect.

For every extension, reset the trace directory or preserve a clearly named baseline before running. Otherwise artifacts from an earlier execution can make a later assertion pass for the wrong reason. Record the command, ansible-core version, inventory, exit status, and changed counts. That small discipline turns a demonstration into reproducible evidence and makes disagreements about execution order easier to resolve.

Keep those artifacts beside the playbook so another reviewer can repeat the same trace and challenge your assumptions.

Sources and Further Reading