Introduction
ansible-lint checks playbooks, roles, and collections against best practices and known anti-patterns. It catches errors before they hit production: deprecated syntax, missing FQCN, unsafe shell commands, missing changed_when, and more.
Install
# pip (recommended)
pip install ansible-lint
# pipx (isolated)
pipx install ansible-lint
# Verify
ansible-lint --version
Basic Usage
# Lint a playbook
ansible-lint playbook.yml
# Lint all YAML in directory
ansible-lint
# Lint specific files
ansible-lint roles/webserver/tasks/main.yml
# Show all rules
ansible-lint -L
# List only rule tags
ansible-lint -T
Common Rules and Fixes
name[missing] — Tasks Need Names
# ❌ Bad
- apt:
name: nginx
# ✅ Good
- name: Install nginx
ansible.builtin.apt:
name: nginx
fqcn[action-core] — Use Fully Qualified Collection Names
# ❌ Bad
- copy:
src: app.conf
dest: /etc/myapp/
# ✅ Good
- ansible.builtin.copy:
src: app.conf
dest: /etc/myapp/
command-instead-of-module — Use Modules
# ❌ Bad
- ansible.builtin.command: chown root:root /etc/app.conf
# ✅ Good
- ansible.builtin.file:
path: /etc/app.conf
owner: root
group: root
no-changed-when — shell/command Need changed_when
# ❌ Bad
- ansible.builtin.shell: cat /etc/hostname
# ✅ Good
- ansible.builtin.shell: cat /etc/hostname
changed_when: false
# ✅ Also good
- ansible.builtin.shell: deploy.sh
register: deploy
changed_when: "'deployed' in deploy.stdout"
yaml[truthy] — Boolean Values
# ❌ Bad
become: yes
enabled: True
# ✅ Good
become: true
enabled: true
risky-shell-pipe — Pipe Error Handling
# ❌ Bad (pipe hides errors)
- shell: grep "error" /var/log/app.log | wc -l
# ✅ Good (set pipefail)
- shell: set -o pipefail && grep "error" /var/log/app.log | wc -l
args:
executable: /bin/bash
jinja[spacing] — Jinja2 Whitespace
# ❌ Bad
msg: "Hello {{name}}"
# ✅ Good
msg: "Hello {{ name }}"
Configuration
.ansible-lint
# .ansible-lint
---
# Exclude paths
exclude_paths:
- .github/
- molecule/
- tests/
# Skip specific rules
skip_list:
- yaml[line-length] # Allow long lines
- name[casing] # Allow flexible naming
# Warn instead of fail
warn_list:
- experimental # Don't fail on experimental rules
- role-name[path]
# Enable optional rules
enable_list:
- no-same-owner
- no-tabs
# Use specific profile
profile: production # null, min, basic, moderate, safety, shared, production
# Mock roles/modules not installed
mock_roles:
- geerlingguy.docker
- geerlingguy.nginx
mock_modules:
- custom_module
Profiles
| Profile | Strictness | Use Case |
|---|---|---|
null | None | Just parse, no rules |
min | Minimal | Basic syntax only |
basic | Low | Common mistakes |
moderate | Medium | Team projects |
safety | High | Security-focused |
shared | High | Published roles |
production | Highest | Production code |
# .ansible-lint
profile: moderate # Good starting point
Inline Skip
# Skip for single task
- name: Run custom script
ansible.builtin.shell: /opt/scripts/deploy.sh # noqa: command-instead-of-shell
changed_when: false
# Skip for block
- name: Legacy tasks # noqa: fqcn[action-core]
copy:
src: old.conf
dest: /etc/old.conf
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
- uses: ansible/ansible-lint@main
GitLab CI
ansible-lint:
image: python:3.12
script:
- pip install ansible-lint
- ansible-lint
Pre-commit Hook
# .pre-commit-config.yaml
repos:
- repo: https://github.com/ansible/ansible-lint
rev: v24.7.0
hooks:
- id: ansible-lint
Fix Automatically
# Auto-fix what it can
ansible-lint --fix
# Auto-fix specific file
ansible-lint --fix playbook.yml
# Show what would be fixed (dry run)
ansible-lint --fix -x
Custom Rules
# rules/check_tags.py
from ansiblelint.rules import AnsibleLintRule
class CheckTagsRule(AnsibleLintRule):
id = "custom-tags"
shortdesc = "All tasks must have tags"
description = "Every task should have at least one tag for selective execution"
severity = "MEDIUM"
tags = ["custom"]
def matchtask(self, task, file=None):
if not task.get("tags"):
return True
return False
# Use custom rules
ansible-lint -R -r rules/ playbook.yml
Common Workflow
# 1. Run lint
ansible-lint
# 2. Fix auto-fixable issues
ansible-lint --fix
# 3. Review remaining issues
ansible-lint -f codeclimate # Machine-readable output
# 4. Skip false positives with inline comments
# 5. Update .ansible-lint for project-wide skips
Related Articles
Conclusion
ansible-lint catches problems before they reach production. Start with profile: moderate, add it to CI/CD, and address warnings incrementally. Key rules to prioritize: use FQCN (ansible.builtin.*), name every task, add changed_when to shell/command tasks, and use modules over raw commands. The --fix flag handles many issues automatically. Over time, increase the profile strictness as your codebase improves.