Ansible Lint: Analyze & Fix Playbooks for Best Practices (Complete Guide)
By Luca Berton · Published 2024-01-01 · Category: installation
How to use ansible-lint to analyze and fix Ansible playbooks. Detect errors, enforce best practices, custom rules, CI/CD integration.
What Is ansible-lint?
ansible-lint is the official linting tool for Ansible content. It checks playbooks, roles, and collections against a set of rules that enforce best practices, catch common mistakes, and improve code quality.
Think of it as ESLint for Ansible — it won't run your playbooks, but it will tell you what's wrong before you do.
See also: Ansible-Lint: Complete Guide to Linting Playbooks & Roles
Install
# pip (recommended)
pip install ansible-lint
# pipx (isolated)
pipx install ansible-lint
# Verify
ansible-lint --version
# ansible-lint 24.12.2 using ansible-core 2.18.1
Basic Usage
# Lint a playbook
ansible-lint site.yml
# Lint all YAML files in current directory
ansible-lint
# Lint a role
ansible-lint roles/webserver/
# Lint with specific profile
ansible-lint -p production
# Fix auto-fixable issues
ansible-lint --fix
# Show all rules
ansible-lint -L
# List rules by tag
ansible-lint -T
See also: Ansible Lint: Check & Fix Playbooks for Best Practices (2026 Guide)
Understanding Rules
Rule Categories
| Tag | Description | Example |
|-----|-------------|---------|
| command-instead-of-module | Use modules instead of shell/command | Use ansible.builtin.apt not apt-get install |
| deprecated | Deprecated syntax or features | with_items → loop |
| experimental | New rules being tested | Various |
| formatting | YAML formatting issues | Indentation, trailing spaces |
| idempotency | Non-idempotent tasks | Missing creates/removes on command |
| metadata | Role/collection metadata issues | Missing meta/main.yml |
| name | Task naming conventions | Missing or badly formatted names |
| no-changed-when | Missing changed_when | Shell/command without change detection |
| partial-become | Inconsistent privilege escalation | become on some tasks but not play |
| risky | Potentially dangerous operations | ignore_errors: true |
| schema | Schema validation failures | Invalid playbook structure |
| syntax | YAML/Ansible syntax errors | Bad indentation, invalid keywords |
| unpredictability | Non-deterministic behavior | git without version |
| yaml | YAML best practices | Truthy values, line length |
Most Common Warnings and Fixes
name[missing] — Tasks should be named
# ❌ Bad
- ansible.builtin.apt:
name: nginx
state: present
# ✅ Good
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
fqcn[action-core] — Use FQCN for modules
# ❌ Bad
- name: Install package
apt:
name: nginx
# ✅ Good
- name: Install package
ansible.builtin.apt:
name: nginx
no-changed-when — Commands need change detection
# ❌ Bad
- name: Check disk space
ansible.builtin.command: df -h
# ✅ Good
- name: Check disk space
ansible.builtin.command: df -h
changed_when: false
# ✅ Also good (for commands that change things)
- name: Run database migration
ansible.builtin.command: python manage.py migrate
changed_when: "'No migrations to apply' not in migration_result.stdout"
register: migration_result
command-instead-of-module — Use modules
# ❌ Bad
- name: Install nginx
ansible.builtin.shell: apt-get install -y nginx
# ✅ Good
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
# ❌ Bad
- name: Create directory
ansible.builtin.command: mkdir -p /opt/app
# ✅ Good
- name: Create directory
ansible.builtin.file:
path: /opt/app
state: directory
mode: '0755'
yaml[truthy] — Avoid truthy values
# ❌ Bad
become: yes
ignore_errors: yes
# ✅ Good
become: true
ignore_errors: true
risky-file-permissions — Set explicit permissions
# ❌ Bad (inherits umask)
- name: Create config file
ansible.builtin.copy:
content: "config data"
dest: /etc/app/config.yml
# ✅ Good
- name: Create config file
ansible.builtin.copy:
content: "config data"
dest: /etc/app/config.yml
mode: '0644'
no-free-form — Avoid free-form syntax
# ❌ Bad
- name: Check uptime
ansible.builtin.command: uptime
# ✅ Good
- name: Check uptime
ansible.builtin.command:
cmd: uptime
jinja[spacing] — Consistent Jinja2 spacing
# ❌ Bad
dest: "{{item.path}}"
# ✅ Good
dest: "{{ item.path }}"
Configuration
.ansible-lint Configuration File
# .ansible-lint
---
# Profiles: min, basic, moderate, safety, shared, production
profile: moderate
# Exclude paths
exclude_paths:
- .github/
- molecule/
- tests/
- "*.md"
# Skip specific rules
skip_list:
- yaml[line-length] # Allow long lines
- name[casing] # Allow any case in task names
# Warn instead of error
warn_list:
- no-changed-when
- command-instead-of-module
- experimental
# Enable optional rules
enable_list:
- args
- empty-string-compare
- no-log-password
- no-same-owner
# Custom rules directory
# rulesdir:
# - ./custom_rules/
# Offline mode (don't download schemas)
offline: false
# Use default rules
use_default_rules: true
# Ansible configuration
# parseable: true
# quiet: true
# strict: false
Profiles
Profiles are preset rule bundles, from permissive to strict:
| Profile | Rules | Best For |
|---------|-------|----------|
| min | Syntax only | Legacy codebases |
| basic | min + essential rules | Getting started |
| moderate | basic + style rules | Most teams |
| safety | moderate + security rules | Security-conscious teams |
| shared | safety + collaboration rules | Open source / shared content |
| production | All rules | Enterprise / certified content |
# Use a specific profile
ansible-lint -p production site.yml
See also: Ansible-Lint Rule Analysis and Best Practices
Auto-Fix
ansible-lint can automatically fix many issues:
# Preview fixes (dry-run)
ansible-lint --fix=all --diff site.yml
# Apply fixes
ansible-lint --fix site.yml
# Fix specific rule
ansible-lint --fix=yaml[truthy] site.yml
Auto-fixable rules include:
• yaml[truthy] — yes/no → true/false
• yaml[octal-values] — Octal file modes
• fqcn[action-core] — Short module names → FQCNs
• name[casing] — Task name capitalization
• jinja[spacing] — Jinja2 template spacing
• no-free-form — Free-form to structured arguments
CI/CD Integration
GitHub Actions
# .github/workflows/lint.yml
name: Ansible Lint
on: [push, pull_request]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run ansible-lint
uses: ansible/ansible-lint@v24
# Or manually:
# - run: pip install ansible-lint
# - run: ansible-lint
GitLab CI
# .gitlab-ci.yml
ansible-lint:
image: python:3.12
stage: test
before_script:
- pip install ansible-lint
script:
- ansible-lint
rules:
- changes:
- "**/*.yml"
- "**/*.yaml"
- roles/**/*
Pre-commit Hook
# .pre-commit-config.yaml
repos:
- repo: https://github.com/ansible/ansible-lint
rev: v24.12.2
hooks:
- id: ansible-lint
additional_dependencies:
- ansible-core>=2.18
pip install pre-commit
pre-commit install
# Now ansible-lint runs automatically before each commit
Custom Rules
Create custom rules for your organization's standards:
# custom_rules/check_company_tags.py
"""Ensure all tasks have company-standard tags."""
from ansiblelint.rules import AnsibleLintRule
class CompanyTagsRule(AnsibleLintRule):
id = "company-tags"
shortdesc = "Tasks must have team and service tags"
description = (
"All tasks in production playbooks must include "
"'team' and 'service' tags for accountability."
)
severity = "MEDIUM"
tags = ["company", "metadata"]
def matchtask(self, task, file=None):
if file and "production" in str(file.path):
tags = task.get("tags", [])
if not any(t.startswith("team:") for t in tags):
return "Task missing 'team:' tag"
if not any(t.startswith("service:") for t in tags):
return "Task missing 'service:' tag"
return False
# .ansible-lint
rulesdir:
- ./custom_rules/
ansible-lint with VS Code
Install the Ansible extension for VS Code:
// .vscode/settings.json
{
"ansible.validation.lint.enabled": true,
"ansible.validation.lint.path": "ansible-lint",
"ansible.validation.lint.arguments": "-p moderate",
"ansible.ansible.useFullyQualifiedCollectionNames": true
}
This gives you real-time linting feedback as you write playbooks — red squiggles under issues, hover for explanations, quick-fix suggestions.
Integrating with Molecule
# molecule/default/molecule.yml
lint: |
set -e
ansible-lint
Or in newer Molecule versions, add to your CI pipeline to run lint before converge.
Common Workflow
# 1. Write playbook
vim roles/webserver/tasks/main.yml
# 2. Lint locally
ansible-lint roles/webserver/
# 3. Auto-fix what you can
ansible-lint --fix roles/webserver/
# 4. Fix remaining issues manually
# 5. Commit
git add -A && git commit -m "feat: add webserver role"
# 6. CI runs ansible-lint again to verify
FAQ
What's the difference between ansible-lint and yamllint?
yamllint checks generic YAML syntax and formatting. ansible-lint checks Ansible-specific best practices — module usage, task naming, idempotency, deprecations, and security. ansible-lint includes yamllint rules via the yaml[] rule set, so you typically only need ansible-lint.
Should I use --fix in CI/CD?
No. Use --fix locally during development. In CI/CD, run ansible-lint without --fix — it should only report errors, not modify code. If it finds issues, the PR fails and the developer fixes them locally.
How do I ignore a specific rule for one task?
Add a # noqa comment:
- name: Run legacy script # noqa: command-instead-of-module
ansible.builtin.shell: /opt/legacy/run.sh
Or for all rules on a task: # noqa: all
Which profile should my team use?
Start with basic or moderate. Move to safety or production as your codebase matures. Avoid starting with production on a legacy codebase — you'll get hundreds of warnings and developers will just skip it.
Does ansible-lint slow down CI pipelines?
No. ansible-lint is fast — it typically completes in seconds even for large codebases. It only parses YAML files, it doesn't execute anything.
Conclusion
ansible-lint is essential for maintaining quality Ansible automation. Start with a moderate profile, integrate into your CI/CD pipeline and pre-commit hooks, use --fix for auto-fixable issues, and gradually increase strictness as your codebase improves. The key rules to enforce from day one: FQCN module names, task names, changed_when on commands, explicit file permissions, and true/false instead of yes/no.
Related Articles
• Ansible Documentation Complete Guide • Ansible Collection Role Testing with Molecule • Ansible CI/CD Pipeline Integration • Ansible Best Practices: ignore_errors • Install Ansible Complete Guide • Install ansible-lint on macOS • Ansible-Lint Rule Analysis and Best Practices • Exploring Ansible-Lint ProfilesCategory: installation