Skip to main content
RunBook Academy

Git, CI/CD & GitOpsCX · Ansible Delivery PipelineSyntaxAndTest

Syntax check and Molecule — the static and dynamic check

Advanced⏱ ~28 mingitansible

What you'll learn

  • Explain what ansible-playbook --syntax-check proves and what it deliberately does not
  • Configure Molecule scenarios that converge and verify against ephemeral targets
  • Distinguish a Molecule scenario that proves convergence from one that proves post-state
  • Recognise the boundary between static checks and dynamic checks in the pipeline

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 third and fourth gates of an Ansible delivery pipeline mark the boundary between static and dynamic testing. ansible-playbook --syntax-check is the last gate that runs against the working tree with no managed host; Molecule is the first gate that actually invokes a playbook against a target. Treating the two as interchangeable is a common mistake; they answer different questions, and the pipeline needs both answers.

What —syntax-check proves

ansible-playbook --syntax-check parses the playbook the way Ansible would at apply time. It walks the plays, resolves variables against the local inventory and role defaults, validates that every module is available in the collection cache, and exits without ever connecting to a managed host. The canonical invocation in CI is:

ansible-playbook --syntax-check playbook.yml

The flag is the only thing distinguishing this from a real apply. With --syntax-check, the parser stops at the first step it would ordinarily execute and reports any failure as a syntax error. The exit code is the gate: zero means the playbook is parseable and resolvable against the local environment; non-zero means it is not.

What the flag proves:

  • The visible YAML parses.
  • Every variable referenced by a task resolves against the inventory and role defaults available at parse time.
  • Every module referenced by a task is present in the local collection cache.
  • Every role the playbook depends on has a sane meta/main.yml.

What the flag does not prove:

  • That any task succeeds against a managed host.
  • That a variable resolved at parse time still has the expected value at apply time (host facts override defaults; vault variables override everything).
  • That the playbook is idempotent.
  • That the playbook does not remove a file the host depends on.

What Molecule proves

Molecule is the dynamic half of the test pair. A Molecule scenario provisions an ephemeral target - a Docker container by default, a Podman container, a Vagrant VM, or a cloud VM - and runs the role against it. The canonical CI invocation runs the full scenario:

molecule test

The test subcommand is a meta-step that runs destroy, dependency, syntax, create, prepare, converge, idempotency, verify, and destroy in order. Two of those steps matter most for this lesson:

flowchart LR
    A["molecule converge"] --> B["Target converges to role"]
    B --> C["molecule verify"]
    C --> D["Assertions on post-state pass"]
    D --> E["Second converge (idempotency)"]
    E --> F["No further changes reported"]
  • molecule converge applies the role to the ephemeral target and produces the expected system state. A converge that fails is a role that does not produce the state the author intended, and the pipeline should block.
  • molecule verify runs a separate verification playbook - typically named verify.yml - that asserts facts about the post-state using ansible.builtin.assert, ansible.builtin.stat, or third-party test modules. A verify that fails is a role that produced some state but not the expected state, and the pipeline should block.

A common mistake is to treat converge as the test. It is not. Converge without verify is a role that ran without anyone asserting what it produced. The verify step is the part that proves the role did what it claimed.

The static-dynamic boundary

The boundary between --syntax-check and molecule test is the most important architectural line in the pipeline. On one side: gates that read files and exit. On the other side: gates that invoke playbooks against targets. The boundary determines three operational properties:

  • Credentials. The static side holds no managed-host credentials. The dynamic side holds credentials for ephemeral targets, which are scoped per run and destroyed after the scenario completes.
  • Cost. The static side runs in seconds. The dynamic side runs in minutes - container pull, role converge, verify, idempotency, destroy. Putting the dynamic side too early in the pipeline wastes minutes on every PR that would have failed the static side.
  • Failure surface. The static side fails on parse errors. The dynamic side fails on missing packages, missing services, misconfigured handlers, idempotency drift, and assertion failures. The failure modes are different, and the diagnostics differ.

A pipeline that runs only one side of this boundary is a pipeline that either wastes time or wastes confidence.

Idempotency as part of the dynamic gate

molecule test includes an idempotency check as part of the meta-step sequence. After the initial converge and verify, Molecule runs converge a second time and asserts that the second run reports zero changes. A role that mutates state on every run is a role that cannot be safely re-applied after a partial failure, and the idempotency check is the gate that catches this class of bug.

The idempotency check is what separates Ansible from a one-shot configuration script. A role that converges once and changes state once is a script. A role that converges any number of times and reports no further changes after the first is an Ansible role. The CI gate must enforce this distinction.

What the two gates together cannot prove

Even with both --syntax-check and molecule test, the pipeline has not proven that:

  • The role converges on a production host, which may differ in OS, package versions, and existing configuration from the ephemeral container.
  • The role survives being applied alongside other roles in a real playbook, where variable scope and ordering can change behaviour.
  • The role’s idempotency holds across long time intervals, when external systems (certificates, tokens) expire and re-trigger handlers.
  • The role produces the desired state on hosts that were configured by hand before the role was introduced.

These gaps are exactly why the apply stage exists: the production fleet is the ultimate verification, and the pipeline’s job is to make the apply a routine execution of a validated change, not a leap of faith.

Production discipline

  1. ansible-playbook --syntax-check runs on the same inventory Molecule will use. A --syntax-check against a different inventory is a --syntax-check that proves parseability against a different world than the converge that follows.
  2. The Molecule scenario uses an ephemeral driver, not the production driver. A scenario that provisions real cloud VMs is a scenario that has escalated the credential boundary.
  3. molecule test is the CI invocation. Converge-only is fine for local iteration; CI always runs the full sequence including verify and idempotency.
  4. The verify playbook is committed. A verify playbook that lives only on a developer’s laptop is a verify playbook that does not gate the merge.
  5. The converge log is uploaded as an artefact. A green check with no log is hard to debug when the next PR fails.

Cross-course references

  • Ansible for Production Sysadmins - Part XXVI (Testing) covers the Molecule scenario format this Part layers into the pipeline.
  • Ansible for Production Sysadmins - Part XXV (CheckDiff) covers the --check and --diff semantics that the idempotency check builds on.
  • This course, Part LI (AnsibleCI) - lesson git-cicd-gitops-li-04 covers the syntax-check stage in detail; lesson git-cicd-gitops-li-05 covers Molecule in detail.
  • This course, Part CIX (TerraformDelivery) - lesson git-cicd-gitops-cix-03 covers the analogous plan-and-validate boundary for Terraform, which is structurally similar.

Quiz

Knowledge check · 4 questions

  1. Q1. Which statement best describes what `ansible-playbook --syntax-check playbook.yml` proves?

  2. Q2. Running `molecule converge` alone is equivalent to running `molecule test` for CI purposes, because converge applies the role and the exit code reflects success.

  3. Q3. Name the three stages inside `molecule test` that this lesson focuses on, and the property each one proves.

  4. Q4. Diagnose a CI pipeline that runs Molecule but skips the verify step, and prescribe the structural correction.

    A team runs `molecule converge` as the dynamic gate. A contributor adds a task that silently fails to install a package because the package name differs between the role's metadata and the underlying OS. The converge succeeds (the task reports `ok` because the failure handler swallows the error). The role is merged and applied to production, where the package is missing and the service fails to start.

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