>DevOps Interview KB

How would you retrofit idempotency checks into an existing large Ansible playbook full of command/shell tasks, without rewriting every task at once?

AdvancedScenarioPracticalAnsible8 min read

Short Answer

Triage tasks by risk and frequency rather than rewriting top-to-bottom: first add changed_when/creates/removes to the highest-impact command/shell tasks as a cheap, low-risk fix that at least gets accurate change-reporting without touching the actual logic, then replace the worst offenders (the ones actually causing real problems — noisy handler triggers, incorrect check-mode behavior) with purpose-built modules one at a time, verified independently, while leaving lower-impact tasks alone until they're touched for another reason.

Detailed Explanation

The core risk of a big-bang rewrite is that a large, old playbook likely encodes tribal knowledge in ways that aren't obvious from reading the task alone — a shell command with a specific flag might be working around a real environment quirk that a "cleaner" purpose-built module replacement wouldn't replicate. Retrofitting incrementally, verified one change at a time, contains that risk to one task instead of the whole playbook.

Step one — cheap, low-risk fixes first: for every command/shell task, without changing its actual logic, add changed_when (based on the command's return code or output) or creates/removes where applicable. This alone fixes the most visible symptom (false "changed" on every run) without touching what the task actually does, making it a safe, mechanical first pass across the whole playbook that can be done task-by-task with minimal risk of behavior change.

Step two — triage by actual impact: not every non-idempotent task is equally worth fixing. Prioritize by identifying which tasks actually cause problems — tasks that notify handlers (so a false "changed" triggers unnecessary restarts every run), tasks that make check-mode output misleading for what people actually rely on it for, or tasks that are simply run often enough that the noise itself has a real cost. Tasks that run rarely and don't trigger anything downstream are lower priority even if technically non-idempotent.

Step three — replace high-priority tasks with purpose-built modules one at a time: for each task actually worth fixing properly, replace it with the equivalent module (copy/template/lineinfile/package/service, etc.), and verify the replacement produces the same end state as the original command/shell version before considering it done — ideally by running both in parallel in a non-production environment and diffing the result, not just trusting that the module "should" do the same thing.

Step four — set a going-forward standard: pair the retrofit with a rule that new tasks default to purpose-built modules, and that command/shell usage requires changed_when at minimum — so the debt doesn't silently regrow while the existing backlog is being worked down.

This staged approach means the playbook's actual behavior only changes deliberately, one verified task at a time, rather than in one large diff where a single subtle regression could be buried among hundreds of changed lines.

Interview Follow-Up Questions

Key Takeaways

  • Add changed_when/creates/removes as a cheap first pass across all command/shell tasks before attempting any real module replacement — it's low-risk and fixes the most visible symptom immediately.
  • Triage which tasks are actually worth replacing with purpose-built modules based on real impact (handler triggers, check-mode reliance, run frequency), not uniformly.
  • Verify each module replacement produces the same end state as the original before considering it done — old shell commands often encode undocumented environment-specific behavior.
  • Pair the retrofit with a going-forward standard so new tasks don't silently reintroduce the same debt.

References

Last updated August 21, 2026 · Last reviewed August 21, 2026