AnsiblePilot — Master Ansible Automation

AnsiblePilot is the leading resource for learning Ansible automation, DevOps, and infrastructure as code. Browse over 1,400 tutorials covering Ansible modules, playbooks, roles, collections, and real-world examples. Whether you are a beginner or an experienced engineer, our step-by-step guides help you automate Linux, Windows, cloud, containers, and network infrastructure.

Popular Topics

About Luca Berton

Luca Berton is an Ansible automation expert, author of 8 Ansible books published by Apress and Leanpub including "Ansible for VMware by Examples" and "Ansible for Kubernetes by Example", and creator of the Ansible Pilot YouTube channel. He shares practical automation knowledge through tutorials, books, and video courses to help IT professionals and DevOps engineers master infrastructure automation.

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

TagDescriptionExample
command-instead-of-moduleUse modules instead of shell/commandUse ansible.builtin.apt not apt-get install
deprecatedDeprecated syntax or featureswith_itemsloop
experimentalNew rules being testedVarious
formattingYAML formatting issuesIndentation, trailing spaces
idempotencyNon-idempotent tasksMissing creates/removes on command
metadataRole/collection metadata issuesMissing meta/main.yml
nameTask naming conventionsMissing or badly formatted names
no-changed-whenMissing changed_whenShell/command without change detection
partial-becomeInconsistent privilege escalationbecome on some tasks but not play
riskyPotentially dangerous operationsignore_errors: true
schemaSchema validation failuresInvalid playbook structure
syntaxYAML/Ansible syntax errorsBad indentation, invalid keywords
unpredictabilityNon-deterministic behaviorgit without version
yamlYAML best practicesTruthy 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:

ProfileRulesBest For
minSyntax onlyLegacy codebases
basicmin + essential rulesGetting started
moderatebasic + style rulesMost teams
safetymoderate + security rulesSecurity-conscious teams
sharedsafety + collaboration rulesOpen source / shared content
productionAll rulesEnterprise / 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/notrue/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.

Category: installation

Browse all Ansible tutorials · AnsiblePilot Home