ObservabilityLXXXV · CI ValidationCIValidation
YAML Lint
What you'll learn
- Run yamllint against the observability configuration tree and interpret the finding code
- Write a .yamllint configuration that matches the team style without being noisy
- Distinguish a syntax error (blocker) from a style finding (warning) in CI
- Wire yamllint into a pre-commit hook and a GitHub Actions job
Prerequisites
Verified against Prometheus 2.55.x · Alertmanager 0.28.x · node_exporter 1.8.x · blackbox_exporter 0.26.x · Grafana 11.x · Loki 3.x · Tempo current · OpenTelemetry Collector 0.110.x · Grafana Alloy current · Docker Engine 28.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-13
A team merges a Grafana dashboard provisioning change. The
YAML is valid. Prometheus is happy. The change deploys.
Grafana fails to load the dashboard at runtime because the
key datasources (plural) was spelled datasource (singular)
in two places and Grafana silently ignored the panel. A
three-character typo shipped to production. yamllint, wired
in correctly with a project-aware rule set, catches the
duplicate-key mistake in 100 milliseconds. The lesson is that
the lint is not just a style tool. With the right
configuration, it is the cheapest gate that catches a class of
mistakes that the schema-aware checks miss.
What it is
yamllint is a Python linter for YAML files. It validates
syntax against the YAML 1.2 specification and applies a
configurable set of style rules. The default rule set is
opinionated: it flags line length, indentation style, key
ordering, truthy values, comment placement, and more. Most
teams override the defaults to match their style guide; the
override is the .yamllint configuration file at the repo
root.
The findings yamllint emits have a three-character code, a severity, and a line and column reference:
observability/grafana/dashboards/checkout.yml
12:5 warning truthy value is not truthy (use 'true' instead of 'yes')
23:1 error syntax found character '\t' that cannot start any token
Two properties:
- It is purely structural. yamllint parses the YAML and
applies its rules. It does not understand the schema of
any specific tool (Prometheus, Grafana, Loki). The
schema-aware checks are
promtool check config,amtool check-config, etc. yamllint is the layer below. - It is configurable per rule. Each rule has a severity
(
error,warning,info) and can be disabled. A team that findsline-lengthtoo noisy can set its max to a project-appropriate number or disable it.
Why a sysadmin cares
The lint is the cheapest gate that catches a real class of mistake. The four shapes it catches most often:
- Syntax error. Tabs instead of spaces, unclosed quotes, duplicate keys. The schema-aware checks also catch these, but yamllint catches them earlier in the pipeline and with better error messages.
- Duplicate key.
datasources:followed bydatasources:further down. The second silently overwrites the first in most YAML parsers (depending on the implementation); the lint flags the duplicate immediately. - Truthy value.
enabled: yesparses as a booleantrue, but the YAML 1.2 specification definesyes/noas strings, not booleans. The lint flags it; the tool consuming the YAML may or may not coerce. - Style drift. Inconsistent indentation (2 spaces here, 4 spaces there), trailing whitespace, missing document marker. The schema-aware checks pass these files, but the diff is hard to read and merge conflicts are common.
Each of these is invisible to the schema-aware checks. Each is caught in 100 milliseconds per file.
How it works
The mental model:
yamllint -c .yamllint observability/
|
v
Walk the named paths (or recurse if a directory)
|
v
For each *.yml, *.yaml, *.yaml.j2 file:
Parse the YAML
Apply each enabled rule
Collect findings with code, severity, line, column
|
v
Print findings to stdout
Exit 1 if any finding has severity "error"
Exit 0 otherwise (warnings and infos do not fail)
The exit code is the gate. yamllint exits 1 only on
error-level findings. Warnings and infos print but do not
fail the run; the team’s .yamllint decides which rules are
errors.
How to configure it
The .yamllint configuration is a YAML file at the repo
root. A reasonable starting point for an observability
configuration repo:
# .yamllint
extends: default
rules:
# Syntax-level rules are blockers.
braces:
level: error
brackets:
level: error
colons:
level: error
commas:
level: error
comments:
level: error
require-starting-space: true
min-spaces-from-content: 1
comments-indentation:
level: error
document-end:
present: false # many tools require no end marker
duplicate-keys:
level: error
empty-lines:
max: 2
max-start: 0
max-end: 1
empty-values:
level: error
hyphens:
level: error
max-spaces-after: 1
indentation:
level: error
spaces: 2
indent-sequences: consistent
check-multi-line-strings: false
key-duplicates:
level: error
key-ordering:
level: warning # advisory; teams often disable
new-line-at-end-of-file:
level: error
new-lines:
level: error
type: unix
octal-values:
level: error
forbid-implicit-octal: true
forbid-explicit-octal: true
quoted-strings:
level: warning # advisory; many YAML styles don't quote
trailing-spaces:
level: error
truthy:
level: error
check-keys: true
level-key: warning
# Style rules that are noisy for observability config; disable.
line-length:
disable: true
ignore: |
node_modules/
.venv/
vendor/
observability/test/fixtures/
Two design choices:
duplicate-keys: level: error. This is the rule that catches the two-spelled-the-same-key mistake. A team that ships observability configurations has to enable it.line-length: disable: true. Long lines are common in observability configurations (Prometheus selectors, Loki query strings). Enable the rule only if the team is willing to enforce the limit in review.
Locally, as a pre-commit hook:
# .pre-commit-config.yaml
repos:
- repo: https://github.com/adrienverge/yamllint
rev: v1.35.1
hooks:
- id: yamllint
args: ['-c', '.yamllint']
pre-commit runs yamllint on every staged .yml and .yaml
file before the commit lands. A failed lint blocks the
commit.
In a GitHub Actions workflow:
# .github/workflows/yamllint.yml
name: yamllint
on:
pull_request:
paths:
- 'observability/**'
- 'rules/**'
- '.yamllint'
- '.github/workflows/yamllint.yml'
permissions:
contents: read
jobs:
lint:
name: yamllint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Install yamllint
run: pip install yamllint==1.35.1
- name: Lint
run: yamllint -c .yamllint observability/ rules/
The job runs only on pull requests that touch the
configuration. The pip pin (==1.35.1) locks the version; CI
must use the same yamllint version as the pre-commit hook.
How to validate it
Three checks confirm the gate is wired correctly.
1. The lint accepts a known-good file.
yamllint -c .yamllint observability/prometheus/prometheus.yml
Expected output, exit 0:
(no output)
No output means no findings. yamllint is quiet on success.
2. The lint rejects a known-bad file.
# observability/prometheus/bad.yml
global:
scrape_interval: 1m
evaluation_interval: 30s
scrape_configs:
- job_name: 'node'
static_configs:
- targets: ['localhost:9100']
scrape_interval_hours: 1 # tab-indented
yamllint -c .yamllint observability/prometheus/bad.yml
Expected output, exit 1:
observability/prometheus/bad.yml
9:5 error syntax found character '\t' that cannot start any token
The error names the file, line, column and finding code. The fix is to replace the tab with spaces.
3. The CI job is wired correctly.
Open a draft pull request that intentionally introduces a
duplicate key. Confirm the yamllint job exits non-zero.
Revert the change; confirm the job exits 0 and the merge is
allowed.
How it can fail
Six failure modes specific to yamllint:
-
The configuration is too noisy. Symptom: every PR triggers dozens of findings, the team stops reading them, real errors get lost in the noise. Cause: the
.yamllintextendsdefaultwithout overrides, and the default rule set is opinionated about line length, quoting and indentation. Disable or relax the rules that are not blockers for the team. -
The
ignore:path includes too much. Symptom: real mistakes in the ignored paths ship to production. Cause: the team added a directory toignore:to silence noise without reading every file in the directory. Audit the ignored paths quarterly; remove the ones that are no longer needed. -
The pre-commit hook and the CI use different yamllint versions. Symptom: a finding that passes locally fails in CI (or vice versa). Cause: the pre-commit hook pins
v1.35.1but the GitHub Actions workflow installs1.35.0. Pin both to the same version. -
yamllint cannot parse Jinja-templated YAML. Symptom: yamllint fails to lint a
*.yaml.j2file because the Jinja template syntax is not valid YAML. Cause: the file is a template, not a rendered YAML. Add the file toignore:or render it before linting. -
The
truthyrule is too strict. Symptom: the lint flagsenabled: yeseven in configurations where the consuming tool acceptsyesas a boolean. Cause: YAML 1.2 definesyes/noas strings, not booleans, but many tools coerce. Settruthy.check-keys: falseor relax the rule towarning. -
The lint runs against the wrong directory. Symptom: a change in
observability/is not linted. Cause: the CI job’spaths:trigger is too narrow (for example,observability/prometheus/**only). Broaden the trigger to cover the whole configuration tree.
How to troubleshoot it
In order:
- Read the finding code. yamllint’s three-character code
(for example,
syntax,duplicate-keys,truthy) is the key to the rule. Theyamllint -dflag prints the rule documentation for the named code. - Run on one file at a time. When the lint rejects a directory, isolate the offending file with a single path argument and rerun.
- Confirm the
.yamllintconfiguration is in scope. yamllint walks up the directory tree looking for the configuration file. If no.yamllintis found, the default rule set is used. - Check the ignore paths. A file in
ignore:is silently skipped. Confirm the file is not inignore:when it should be linted. - Confirm the yamllint version.
yamllint --versionmust match the version pinned in CI.
Security implications
- yamllint prints file content on error. A configuration that contains a basic-auth password or a bearer token will leak the secret into the CI log on parse error. Use a secret manager or a credential file mounted at reload time, not committed credentials.
- yamllint does not authenticate against any service. It is read-only and does not expose any new endpoint. Safe to run from any workstation.
- The
truthyrule may force a style change that exposes a hidden coercion. Aenabled: yesthat was silently coerced totruebecomesenabled: trueafter the lint flag, and the consuming tool’s behaviour is now explicit. This is good practice but should be a deliberate change.
Performance implications
The lint is fast. A configuration tree of two hundred YAML files validates in well under a second on commodity hardware. The cost is dominated by the YAML parse for each file; CI budgets the lint at 100–500 ms per file. There is no production cost from running the lint; it is purely a pre-merge gate.
Production guidance
- Run yamllint on every pull request that touches the configuration. The cost is sub-second; the benefit is a class of mistakes that would otherwise cost a reload or a missed metric.
- Start from the
defaultrule set and override the rules that are noisy for the team. Disableline-lengthandquoted-stringsif the team is not ready to enforce them. - Pin the yamllint version in CI to the same version as the pre-commit hook.
- Audit the
ignore:paths quarterly. Remove the ones that are no longer needed. - Wire yamllint at every stage: pre-commit hook for immediate feedback, CI job for the merge gate, nightly run for files the pre-commit hook missed.
Verification
You should now be able to answer:
- What four shapes does yamllint catch that the schema-aware checks do not?
- How do you make a yamllint rule a blocker (exit 1) instead of a warning (exit 0)?
- Why does a Jinja-templated YAML file need to be rendered before linting?
- What is the difference between yamllint and
promtool check config? - Why must the pre-commit hook and the CI job pin the same yamllint version?
Quiz
Knowledge check · 8 questions
Q1. yamllint differs from promtool check config in that yamllint:
Q2. The .yamllint rule that catches a duplicate key in the same mapping is:
Q3. yamllint exits 1 only when a finding has severity error. Warnings and infos print but do not fail the run.
Q4. Which of these findings are caught by yamllint?
Q5. Name the file that controls yamllint behaviour at the repo root.
Q6. The lint flags enabled: yes as a truthy value. The team accepts yes as a boolean in their tooling. The right configuration is:
Q7. yamllint can parse a *.yaml.j2 Jinja template file without any preprocessing.
Q8. The right cadence for yamllint is:
Passing score: 75%. Answers are checked in this browser.