Automate Code Quality: ansible-lint, yaml-lint, and CI/CD Integration
Code quality is crucial in network automation and DevOps practices. When working with Ansible playbooks and YAML files, automated linting helps catch errors early, enforce best practices, and maintain consistent code standards. In this comprehensive guide, we'll explore how to use ansible-lint and yaml-lint, and integrate them into your CI/CD pipelines for automated code quality checks.
What is Linting?
Linting is a static code analysis tool that checks your code for potential errors, style violations, and suspicious constructs. For Ansible and YAML files, linting helps:
- Catch syntax errors before deployment
 - Enforce coding standards and best practices
 - Improve code readability and maintainability
 - Prevent common mistakes that could cause runtime issues
 - Ensure consistency across team members
 
ansible-lint: Ansible Code Quality Tool
ansible-lint is the official linting tool for Ansible playbooks, roles, and collections. It checks your Ansible code against a set of rules and best practices.
Installation
# Install via pip
pip install ansible-lint
# Install via package manager (Ubuntu/Debian)
sudo apt install ansible-lint
# Install via package manager (macOS)
brew install ansible-lint
Basic Usage
# Lint a single playbook
ansible-lint playbook.yml
# Lint an entire directory
ansible-lint .
# Lint with specific rules
ansible-lint --rules=no-tabs,no-jinja-when playbook.yml
# Generate a report
ansible-lint --format=json playbook.yml > lint-report.json
Configuration File (.ansible-lint)
Create a .ansible-lint file in your project root to customize linting behavior:
---
# Enable/disable specific rules
enable_list:
  - no-tabs
  - no-jinja-when
  - no-handler
  - no-changed-when
  - no-jinja-nesting
# Disable specific rules
disable_list:
  - no-log-password  # If you need to log passwords for debugging
# Customize rule severity
warn_list:
  - no-tabs
  - no-jinja-when
# Set minimum Ansible version
min_ansible_version: "2.10"
# Customize output format
format: rich  # Options: rich, json, codeclimate, quiet, parseable
# Exclude files/directories
exclude_paths:
  - "tests/"
  - "molecule/"
  - "*.j2"
# Set custom rules directory
rulesdir: "custom_rules/"
Common ansible-lint Rules
Here are some essential rules to enable:
enable_list:
  # Code style
  - no-tabs                    # No tabs in YAML files
  - no-jinja-when             # Avoid Jinja2 in when conditions
  - no-handler                # Avoid handlers when possible
  - no-changed-when           # Always specify changed_when
  - no-jinja-nesting          # Avoid nested Jinja2 expressions
  # Security
  - no-log-password           # Don't log passwords
  - no-command                # Avoid raw commands
  - no-shell                  # Avoid shell module
  # Best practices
  - no-relative-paths         # Use absolute paths
  - no-risky-file-permissions # Avoid risky file permissions
  - no-risky-shell-pipe       # Avoid shell pipes
  - no-unsafe-reads           # Avoid unsafe file reads
yaml-lint: YAML Syntax Validation
yaml-lint is a Python-based linter for YAML files that checks syntax, formatting, and style.
Installation
# Install via pip
pip install yamllint
# Install via package manager (Ubuntu/Debian)
sudo apt install yamllint
# Install via package manager (macOS)
brew install yamllint
Basic Usage
# Lint a single file
yamllint playbook.yml
# Lint entire directory
yamllint .
# Lint with specific configuration
yamllint -c .yamllint playbook.yml
# Generate detailed output
yamllint --format=parsable playbook.yml
Configuration File (.yamllint)
Create a .yamllint file to customize YAML linting rules:
---
extends: default
rules:
  # Line length
  line-length:
    max: 120
    level: warning
  # Indentation
  indentation:
    spaces: 2
    indent-sequences: true
  # Trailing spaces
  trailing-spaces: enable
  # Empty lines
  empty-lines:
    max: 1
    max-end: 1
  # Comments
  comments:
    min-spaces-from-content: 1
  # Document start
  document-start: disable
  # Truthy values
  truthy:
    check-keys: false
  # Hyphens
  hyphens:
    max-spaces-before: 1
    max-spaces-after: 1
  # Commas
  commas:
    max-spaces-before: 0
    min-spaces-after: 1
  # Colons
  colons:
    max-spaces-before: 0
    max-spaces-after: 1
  # Braces
  braces:
    min-spaces-inside: 0
    max-spaces-inside: 1
  # Brackets
  brackets:
    min-spaces-inside: 0
    max-spaces-inside: 1
GitHub Actions Integration
GitHub Actions provides excellent support for automated linting. Here's a comprehensive workflow:
Complete GitHub Actions Workflow
name: Code Quality Checks
on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
jobs:
  lint:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: [3.9, 3.10, 3.11]
    steps:
    - name: Checkout code
      uses: actions/checkout@v4
    - name: Set up Python ${{ matrix.python-version }}
      uses: actions/setup-python@v5
      with:
        python-version: ${{ matrix.python-version }}
    - name: Cache pip dependencies
      uses: actions/cache@v4
      with:
        path: ~/.cache/pip
        key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
        restore-keys: |
          ${{ runner.os }}-pip-
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install ansible ansible-lint yamllint
    - name: Run yamllint
      run: |
        yamllint -c .yamllint .
    - name: Run ansible-lint
      run: |
        ansible-lint --format=rich .
    - name: Upload lint results
      uses: actions/upload-artifact@v4
      if: always()
      with:
        name: lint-results-${{ matrix.python-version }}
        path: |
          lint-report.json
          yamllint-report.txt
        retention-days: 7
Advanced GitHub Actions with Multiple Tools
name: Advanced Code Quality
on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]
jobs:
  yaml-lint:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-python@v5
      with:
        python-version: '3.11'
    - run: pip install yamllint
    - run: yamllint -c .yamllint .
  ansible-lint:
    runs-on: ubuntu-latest
    needs: yaml-lint
    steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-python@v5
      with:
        python-version: '3.11'
    - run: pip install ansible ansible-lint
    - run: ansible-lint --format=rich .
  security-scan:
    runs-on: ubuntu-latest
    needs: [yaml-lint, ansible-lint]
    steps:
    - uses: actions/checkout@v4
    - name: Run Bandit security scan
      uses: python-security/bandit@main
      with:
        args: -r . -f json -o bandit-report.json
    - name: Upload security report
      uses: actions/upload-artifact@v4
      with:
        name: security-report
        path: bandit-report.json
GitLab CI/CD Integration
GitLab CI/CD provides robust pipeline capabilities for linting:
Basic GitLab CI Pipeline
stages:
  - lint
  - test
  - deploy
variables:
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"
cache:
  paths:
    - .pip-cache/
yamllint:
  stage: lint
  image: python:3.11-slim
  before_script:
    - pip install yamllint
  script:
    - yamllint -c .yamllint .
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
ansible-lint:
  stage: lint
  image: python:3.11-slim
  before_script:
    - pip install ansible ansible-lint
  script:
    - ansible-lint --format=rich .
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
  artifacts:
    reports:
      junit: ansible-lint-report.xml
    expire_in: 1 week
Advanced GitLab CI with Parallel Jobs
stages:
  - lint
  - test
  - security
  - deploy
variables:
  PIP_CACHE_DIR: "$CI_PROJECT_DIR/.pip-cache"
cache:
  paths:
    - .pip-cache/
.yamllint_template: &yamllint_template
  stage: lint
  image: python:3.11-slim
  before_script:
    - pip install yamllint
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
yamllint-playbooks:
  <<: *yamllint_template
  script:
    - yamllint -c .yamllint playbooks/
  artifacts:
    reports:
      junit: yamllint-playbooks.xml
yamllint-roles:
  <<: *yamllint_template
  script:
    - yamllint -c .yamllint roles/
  artifacts:
    reports:
      junit: yamllint-roles.xml
.ansible_lint_template: &ansible_lint_template
  stage: lint
  image: python:3.11-slim
  before_script:
    - pip install ansible ansible-lint
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
ansible-lint-playbooks:
  <<: *ansible_lint_template
  script:
    - ansible-lint playbooks/
  artifacts:
    reports:
      junit: ansible-lint-playbooks.xml
ansible-lint-roles:
  <<: *ansible_lint_template
  script:
    - ansible-lint roles/
  artifacts:
    reports:
      junit: ansible-lint-roles.xml
security-scan:
  stage: security
  image: python:3.11-slim
  before_script:
    - pip install bandit safety
  script:
    - bandit -r . -f json -o bandit-report.json
    - safety check --json --output safety-report.json
  artifacts:
    reports:
      junit: security-report.xml
    paths:
      - bandit-report.json
      - safety-report.json
    expire_in: 1 week
Pre-commit Hooks
Install pre-commit hooks to catch issues before committing:
.pre-commit-config.yaml
repos:
  - repo: https://github.com/ansible/ansible-lint
    rev: v6.22.1
    hooks:
      - id: ansible-lint
        args: [--format=rich]
  - repo: https://github.com/adrienverge/yamllint
    rev: v1.35.1
    hooks:
      - id: yamllint
        args: [-c, .yamllint]
  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.5.0
    hooks:
      - id: trailing-whitespace
      - id: end-of-file-fixer
      - id: check-yaml
      - id: check-added-large-files
      - id: check-merge-conflict
      - id: check-case-conflict
      - id: check-docstring-first
      - id: check-json
      - id: check-merge-conflict
      - id: debug-statements
      - id: name-tests-test
      - id: requirements-txt-fixer
      - id: fix-byte-order-marker
Installation and Usage
# Install pre-commit
pip install pre-commit
# Install the git hook scripts
pre-commit install
# Run against all files
pre-commit run --all-files
# Run specific hook
pre-commit run ansible-lint --all-files
Best Practices and Tips
1. Progressive Rule Adoption
Start with essential rules and gradually add more:
# Start with these basic rules
enable_list:
  - no-tabs
  - no-jinja-when
  - no-log-password
# Gradually add more rules
enable_list:
  - no-tabs
  - no-jinja-when
  - no-log-password
  - no-command
  - no-shell
  - no-relative-paths
2. Custom Rules for Your Organization
Create custom rules in a custom_rules/ directory:
# custom_rules/custom_rule.py
from ansiblelint.rules import AnsibleLintRule
class CustomRule(AnsibleLintRule):
    id = 'custom-rule'
    shortdesc = 'Custom rule description'
    description = 'Detailed description of the custom rule'
    tags = ['custom']
    def match(self, file, line):
        # Your custom logic here
        return False
3. Integration with IDE
Configure your IDE for real-time linting:
VS Code Settings (.vscode/settings.json):
{
    "ansible.ansibleLint.enabled": true,
    "ansible.ansibleLint.path": "ansible-lint",
    "ansible.ansibleLint.configFile": ".ansible-lint",
    "yaml.validate": true,
    "yaml.schemas": {
        "https://json.schemastore.org/ansible-stable-2.9.json": "**/tasks/*.yml",
        "https://json.schemastore.org/ansible-stable-2.9.json": "**/handlers/*.yml"
    }
}
4. Performance Optimization
For large projects, optimize linting performance:
# Use parallel processing
ansible-lint --parallel .
# Exclude unnecessary directories
ansible-lint --exclude=test/ --exclude=docs/ .
# Use specific file patterns
ansible-lint "**/*.yml" "**/*.yaml"
5. Reporting and Metrics
Generate detailed reports for analysis:
# Generate JSON report
ansible-lint --format=json . > lint-report.json
# Generate CodeClimate format
ansible-lint --format=codeclimate . > codeclimate.json
# Generate JUnit XML for CI
ansible-lint --format=junit . > ansible-lint.xml
Troubleshooting Common Issues
1. False Positives
Handle false positives by disabling specific rules:
# In .ansible-lint
disable_list:
  - no-log-password  # If logging is required for debugging
  - no-command       # If raw commands are necessary
2. Performance Issues
Optimize for large codebases:
# Use caching
ansible-lint --cache .ansible-lint-cache .
# Limit file types
ansible-lint --exclude="*.j2" --exclude="*.md" .
# Use specific directories
ansible-lint playbooks/ roles/
3. Integration Issues
Common CI/CD integration problems:
# GitHub Actions - Handle failures gracefully
- name: Run ansible-lint
  run: |
    ansible-lint --format=rich . || {
      echo "Linting found issues. Check the output above."
      exit 1
    }
  continue-on-error: false
Conclusion
Automated linting with ansible-lint and yaml-lint is essential for maintaining code quality in Ansible projects. By integrating these tools into your CI/CD pipelines, you can:
- Catch errors early in the development process
 - Enforce consistent coding standards across your team
 - Improve code maintainability and readability
 - Reduce deployment failures caused by syntax errors
 - Build confidence in your automation code
 
Start with basic linting rules and gradually expand your quality checks. Remember that the goal is to improve code quality, not to create unnecessary friction in your development workflow.
Resources
- ansible-lint Documentation
 - yamllint Documentation
 - GitHub Actions Documentation
 - GitLab CI/CD Documentation
 - Pre-commit Framework
 
For more automation and DevOps content, check out our Ansible tutorials and network automation guides.