Git, CI/CD & GitOpsLI · Ansible CIFormatAndValidate
YAML and playbook linting — yamllint and ansible-lint as the first gate
What you'll learn
- Run yamllint against an Ansible repository and interpret its findings
- Run ansible-lint and choose a ruleset (default, moderate, safety, shared) appropriate to the team
- Identify what yamllint and ansible-lint catch and what they deliberately leave to syntax-check and Molecule
- Configure a .yamllint and a .ansible-lint file so the CI gate matches the team conventions
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
The first gate in an Ansible CI pipeline is two tools, not one. yamllint reads YAML files and reports formatting and structural issues - the mistakes that come from the fact that Ansible playbooks are YAML before they are Ansible. ansible-lint reads the same files with Ansible semantics in mind and reports issues that only make sense in the context of tasks, modules, and plays. The two overlap in coverage but not in scope, and the discipline is to run both with rules that match the team, not the defaults that match the tool authors.
What yamllint is for
YAML has a handful of foot-guns that survive into Ansible: tabs instead of spaces, inconsistent indentation, unquoted strings that the parser coerces into booleans (yes, no, on, off, true, false), strings that look like sexagesimal numbers, and documents that begin with content the parser expected to be the start of the next document. Ansible inherits all of these because its playbooks are YAML.
yamllint reads each file and applies a configurable ruleset. The canonical CI invocation is:
yamllint .
The dot walks the current directory; yamllint auto-discovers a .yamllint configuration file in the working tree. Without a config file, the tool applies the default profile, which catches the highest-value cases (indentation, line length, trailing spaces, truthy coercion) without being so strict that a normal playbook fails out of the box.
flowchart LR
A[Working tree YAML] --> B["yamllint ."]
B --> C{Config present?}
C -->|no| D[Apply default profile]
C -->|yes| E[Apply .yamllint ruleset]
D --> F{Issues found?}
E --> F
F -->|no| G[Pass]
F -->|yes| H[Print line:column finding]
A .yamllint file is the team’s contract with the tool. It states which rules are enforced, at which severity, with which exceptions. A team that has not written a .yamllint has not decided what its YAML looks like; it has left that decision to the tool maintainers. That is acceptable for a first pass but is not a production posture.
What yamllint catches and what it does not
The things yamllint is good at catching:
- Indentation (the
indentationrule) - tabs versus spaces, inconsistent step, mixed indent. - Truthy coercion (the
truthyrule) - bareyes/no/on/offstrings that the YAML parser turns into booleans. - Line length (the
line-lengthrule) - lines above the configured threshold, default 80. - Document start and end markers (the
document-startrule) - missing or required---and...markers. - Trailing spaces and final newlines (the
trailing-spacesandnew-line-at-end-of-filerules). - Brace and bracket alignment (the
braces,brackets,braces-spacesrules).
What yamllint does not catch:
- That a task is missing a required module argument.
- That a module is the wrong one for the operating system.
- That a variable is undefined and will fail at apply time.
- That a play has no hosts or no tasks.
- That a playbook uses bare module names instead of fully qualified collection names.
Those are the gaps ansible-lint is designed to fill.
What ansible-lint is for
ansible-lint reads the working tree, applies Ansible semantics, and reports findings tagged with rule IDs. The canonical CI invocation is:
ansible-lint
The tool auto-discovers an .ansible-lint file in the working tree. Without a config, it applies the default ruleset; with a config, it applies whatever profile and rule overrides the team has chosen. To see what the active ruleset contains:
ansible-lint --rules
This command prints the list of enabled rules and a short description of each. Reviewing this output during pipeline setup is how a team decides whether the default profile is the one they want, or whether they need moderate (stricter), safety (a curated subset focused on operational risk), or a hand-rolled shared config.
The kinds of rules ansible-lint applies:
- Naming and structure -
name[]on every task and play, lowercase role names, consistent playbook headers. - FQCN usage -
ansible.builtin.copyrather thancopy, so the module is resolved against a specific collection. - Module choice - preferring
ansible.builtin.commandoveransible.builtin.shellunless shell features are actually used; preferringansible.builtin.urioveransible.builtin.command: curl .... - Idempotency hints - flagging
command/shelltasks that lackchanged_when. - Formatting - alignment of
key=valuearguments, consistent indentation inside tasks.
The full set is documented at the ansible-lint rules reference; the team should read it once during setup and again whenever a rule is added.
Choosing a ruleset
The ansible-lint config can set a profile, rules, and per-rule ignore lists. The four profiles the tool ships are:
min- the smallest set of rules; useful for repositories that have not adopted linting before.basic- a stricter starter set; the recommended default for most teams.moderate- adds rules that catch operational risk (idempotency, change detection).safety- a curated subset focused on production-safety issues.shared- the strictest profile; the one used by the community collections.
A team adopting ansible-lint for the first time should start with basic, run the gate on the existing repository, and either fix the findings or explicitly ignore them with a rationale. Promoting the profile to moderate after the existing repository is clean is the production path; promoting without that cleanup produces a CI badge that is permanently red and teaches engineers to ignore the gate.
What neither tool catches
The two linters are deliberately scoped to static, deterministic checks. They do not run the playbook, they do not connect to a managed host, and they do not verify that any task would succeed. A playbook that lints clean can still:
- Reference a variable that is never defined.
- Install a package the target does not have a provider for.
- Restart a service the host does not have.
- Use
shellto invoke something the host does not have installed.
Those are gaps that ansible-playbook --syntax-check, ansible-playbook --check --diff, and molecule converge + molecule verify exist to close. The two linters are the first gate; they are not the only gate.
Production discipline
- Both yamllint and ansible-lint run on every PR. A pipeline that runs only one of them is leaving the other class of bug to the reviewer.
- The
.yamllintand.ansible-lintfiles are committed. Rules that are not in version control are rules that can change without notice. - The active profile is named in the pipeline job name.
lint (basic)andlint (moderate)are different gates and produce different findings. # noqacomments require a rationale. A bare# noqais a suppression without a justification; in code review it should be rejected unless the comment explains why.- Rule changes go through pull requests, not silent edits. A
.ansible-lintchange is a policy change and is reviewed like one.
Cross-course references
- Ansible for Production Sysadmins - Part X (YAML) covers the YAML rules that
yamllintcodifies; it is the language reference for the lint findings. - Ansible for Production Sysadmins - Part XXV (CheckDiff) covers the runtime gates that sit after the lint gates.
- Ansible for Production Sysadmins - Part XXXVIII (GitCI) is the full pipeline; this lesson is the first stage.
- This course, Part XLIX (InfrastructureCI) - lesson
git-cicd-gitops-xlix-02-format-stageis the general framing of the format gate; this lesson is the Ansible-specific instantiation.
Quiz
Knowledge check · 4 questions
Q1. A team wants their Ansible CI pipeline to catch YAML-shape mistakes and Ansible-shape mistakes. Which combination of tools and configs covers both?
Q2. A playbook that passes yamllint and ansible-lint is not guaranteed to apply successfully against a managed host.
Q3. Name the four ansible-lint profiles that ship with the tool, and state the recommended starting profile for a team adopting it for the first time.
Q4. Diagnose why a team adopting ansible-lint on an existing repository is overwhelmed by findings, and propose a staged adoption that does not break trust in the gate.
A team adopts ansible-lint with the moderate profile and runs it on a 400-task existing playbook repository. The first CI run reports 2,300 findings across naming, FQCN, missing changed_when, and indentation. The team silences all of them with a project-wide skip_ansible_lint directive in .ansible-lint and ships the gate in CI. Six months later, a contributor adds a task with a bare module name and a missing changed_when; the gate does not flag it. A production apply produces a non-idempotent change.
Passing score: 75%. Answers are checked in this browser.