Skip to main content
RunBook Academy

Git, CI/CD & GitOpsLI · Ansible CIFoundations

The Ansible CI discipline — what "tested" means for Ansible

Intermediate⏱ ~24 mingitansible

What you'll learn

  • Distinguish static checks (yamllint, ansible-lint, --syntax-check) from runtime checks (--check, --diff, Molecule)
  • Identify what each layer can prove about a playbook and what it deliberately leaves unproven
  • Place lint, syntax-check, check, and Molecule as ordered gates in an Ansible CI pipeline
  • Recognise why "CI passed" is not equivalent to "safe to apply" for an Ansible change

Prerequisites

Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x

Not yet marked complete on this device.

The word “tested” on an Ansible pull request is doing more work than it looks. A change that has been “tested” can mean the YAML parses, the playbook parses, the static rules are clean, the dry run reports no changes, and a Molecule scenario converged and verified against an ephemeral container. These are five different claims about five different properties of the change. Conflating them is how untested playbooks reach production with the badge “CI passed”.

Static checks versus runtime checks

The gates available to an Ansible pipeline split cleanly into two families. Static checks read the files in the working tree and report what they find without ever connecting to a managed host. Runtime checks actually invoke the playbook against a target (ephemeral or otherwise) and observe what it would do or what it did.

flowchart LR
    A[Working tree YAML] --> B[Static checks]
    B --> B1["yamllint"]
    B --> B2["ansible-lint"]
    B --> B3["ansible-playbook --syntax-check"]
    B1 --> C[Runtime checks]
    B2 --> C
    B3 --> C
    C --> C1["ansible-playbook --check --diff"]
    C --> C2["molecule converge"]
    C --> C3["molecule verify"]

Static checks answer “is this code well-formed?”. Runtime checks answer “does this code do what the author intended, against a real system?”. Both questions must be answered; neither is a substitute for the other.

What each layer proves

Each gate in the pipeline is scoped to a specific claim. The CI run is the union of those claims:

GateQuestion it answersWhat it does not prove
yamllintIs the YAML well-formed and consistent?That the Ansible semantics are correct
ansible-lintDoes the playbook follow Ansible best-practice rules?That the playbook runs successfully
--syntax-checkDoes the playbook parse, with all variables and modules resolved?That the tasks succeed against a real host
--check --diffWhat would change on the target, without changing it?That the change would converge idempotently
molecule converge + molecule verifyDoes the role produce the expected state on an ephemeral host, verified by assertions?That it produces the expected state on production hosts

A “CI passed” badge that summarises all five is a much stronger claim than a badge that summarises only the first three. The discipline of this Part is to make all five explicit and to make the absence of any one of them visible.

The boundary between CI and apply

The apply step is where Ansible stops reading files and starts changing systems. Everything in this Part sits on one side of that boundary; the apply sits on the other side. The point of the CI pipeline is to make the boundary explicit: when CI passes, the change is well-formed, lints clean, parses, plans to do something defensible, and converges on an ephemeral host to a verified state. The apply on a production fleet then becomes a routine execution of an already-validated change, not a leap of faith.

A team that runs only static checks and skips the runtime gates is treating the boundary as “files look plausible”. A team that runs runtime checks on ephemeral hosts but skips the apply on protected environments is treating the boundary as “it works in Molecule” - which is true but is a claim about disposable containers, not production. The discipline is to be specific about which claim is being made at each layer.

Production discipline

  1. All five gates run on every pull request that touches Ansible files. Skipping a gate for “small changes” is how the gate that catches the small change gets removed.
  2. CI badges name the gates they cover. A green badge that aggregates lint + syntax-check is a different claim from a green badge that aggregates lint + syntax-check + molecule. Be specific in the pipeline status names.
  3. A Molecule scenario that has not been run in CI is a scenario that has not been tested. Local Molecule runs do not produce an audit trail and do not gate the merge.
  4. The apply job is the only job allowed to mutate production hosts. Every gate before it is read-only or runs against ephemeral infrastructure.
  5. Treat unverified Molecule scenarios as untested code. A scenario file in the repository is documentation, not evidence.

Cross-course references

  • Ansible for Production Sysadmins - Part X (YAML) covers the YAML rules that yamllint enforces.
  • Ansible for Production Sysadmins - Part XXV (CheckDiff) covers the --check and --diff semantics that this lesson layers into the pipeline.
  • Ansible for Production Sysadmins - Part XXVI (Testing) covers the Molecule scenario format that lesson LI-05 instantiates.
  • Ansible for Production Sysadmins - Part XXXVIII (GitCI) is the canonical Ansible CI pipeline; this Part is the GitOps-course instantiation of that pattern.

Quiz

Knowledge check · 4 questions

  1. Q1. A playbook passes yamllint, ansible-lint, and ansible-playbook --syntax-check, but the team has not run --check, --diff, or Molecule. What is the strongest claim that the CI badge can honestly make?

  2. Q2. ansible-playbook --syntax-check is a runtime check, because it parses the playbook the way Ansible would at apply time.

  3. Q3. Name the five gates an Ansible CI pipeline should run on every pull request, and classify each as static or runtime.

  4. Q4. Diagnose why a CI badge that says 'passed' did not prevent a production incident, and identify which gate would have caught the bug.

    A team runs yamllint, ansible-lint, and ansible-playbook --syntax-check on every Ansible pull request. A contributor adds a task that runs `ansible.builtin.file: path: /etc/nginx/nginx.conf state: absent`. The static gates all pass. The PR is merged and applied to a fleet of forty web servers. The apply removes the nginx configuration on every host. Outage.

Passing score: 75%. Answers are checked in this browser.