Skip to main content
RunBook Academy

← All checklists in Ansible

Before deploymentDeployment

Checklist: Playbook production readiness

19 items ·13 critical ·4 warn ·2 info

Run this when a playbook is first proposed for production and again whenever it changes materially - a new play, a new role, a changed host pattern, a changed serial. It is a review gate, not a pre-flight: the pre-execution checklist is what you run in the minute before the change itself.

The commands assume the repository layout this course uses throughout: inventories/<environment>/, playbooks/, roles/. Substitute your own paths; the shape of each check does not change.

What a failure means

Every critical item here is about blast radius or recoverability. A playbook that fails one of them may work perfectly on the first host and still be unsafe, because what it lacks is the ability to stop, to be inspected before it runs, or to be undone.

The warn items are about how expensive the failure will be to diagnose, and the info items are review prompts rather than gates.

Access this needs

Everything except the four ansible-playbook runs is a static read of the repository and needs nothing but a checkout. The check-mode run and the idempotence run need working credentials for the canary host and will connect to it. They do not change it - check mode reports what would change - but they are not offline checks, and they should be run against a canary rather than against the whole group.

Where the evidence goes

Attach the --list-tasks output and the --check --diff output to the change record. Those two artefacts are what a reviewer reads; the playbook itself is what the author reads. If the diff is empty, say so explicitly rather than attaching nothing, because an empty diff and a missing diff look identical in a ticket six weeks later.

Sign-off

  • Author: _________________ Date: ___________
  • Reviewer: ________________ Date: ___________

Critical13 items

  1. ansible-playbook -i inventories/production playbooks/deploy.yml --syntax-check
  2. ansible-lint --nocolor -q --profile production playbooks/deploy.yml; echo "exit=$?"
  3. python3 - <<'PY'
    import glob, yaml
    for f in sorted(glob.glob('playbooks/**/*.yml', recursive=True)):
        try: plays = yaml.safe_load(open(f))
        except Exception: continue
        if not isinstance(plays, list): continue
        for p in plays:
            if isinstance(p, dict) and p.get('hosts') in ('all', '*'):
                print(f"FINDING: {f}: play {p.get('name')!r} targets every host in the inventory")
    PY
  4. python3 - <<'PY'
    import glob, yaml
    for f in sorted(glob.glob('playbooks/**/*.yml', recursive=True)):
        try: plays = yaml.safe_load(open(f))
        except Exception: continue
        if not isinstance(plays, list): continue
        for p in plays:
            if isinstance(p, dict) and 'hosts' in p and 'serial' not in p:
                print(f"FINDING: {f}: play {p.get('name')!r} has no serial - it will sweep the whole group")
    PY
  5. python3 - <<'PY'
    import glob, yaml
    for f in sorted(glob.glob('playbooks/**/*.yml', recursive=True)):
        try: plays = yaml.safe_load(open(f))
        except Exception: continue
        if not isinstance(plays, list): continue
        for p in plays:
            if not isinstance(p, dict) or 'hosts' not in p: continue
            if p.get('serial') and 'max_fail_percentage' not in p and not p.get('any_errors_fatal'):
                print(f"FINDING: {f}: play {p.get('name')!r} batches but has no failure threshold")
    PY
  6. grep -rnE '^[[:space:]]*ignore_errors:[[:space:]]*(true|yes|True)' playbooks/ roles/ \
      | while IFS=: read -r f n _; do
          case "$(sed -n "$((n-1))p" "$f")" in
            *'#'*) ;;
            *) echo "FINDING: $f:$n ignore_errors with no justification comment above it" ;;
          esac
        done
  7. ansible-playbook -i inventories/production playbooks/deploy.yml --list-tasks
  8. ansible-playbook -i inventories/production playbooks/deploy.yml --check --diff --limit canary
  9. ansible-playbook -i inventories/production playbooks/deploy.yml --limit canary | awk '/changed=/ && !/changed=0/ {print "NOT IDEMPOTENT: " $0}'
  10. grep -rInE '(pass|password|passwd|secret|token|api_key|private_key)[a-z_]*:[[:space:]]*[^[:space:]{]' playbooks/
  11. ansible-inventory -i inventories/production --host web01.example.com
  12. ansible-playbook -i inventories/production playbooks/deploy.yml --list-tasks | tail -20

Warning4 items

  1. python3 - <<'PY'
    import glob, yaml
    for f in sorted(glob.glob('playbooks/**/*.yml', recursive=True)):
        try: plays = yaml.safe_load(open(f))
        except Exception: continue
        if not isinstance(plays, list): continue
        for p in plays:
            if isinstance(p, dict) and p.get('become') is True:
                print(f"FINDING: {f}: play {p.get('name')!r} escalates for every task in the play")
    PY
  2. ansible-playbook -i inventories/production playbooks/deploy.yml --check --diff --limit canary | sed -n "/^--- /,/^+++ /p"
  3. grep -rn 'flush_handlers' playbooks/ roles/
  4. grep -rnE 'wait_for|async:|poll:' playbooks/ roles/

Info2 items

  1. ansible-playbook -i inventories/production playbooks/deploy.yml --list-tasks --tags config
  2. grep -rn 'with_[a-z_]*:' playbooks/ roles/