Ansible Validator
Comprehensive toolkit for validating, linting, testing, and automating Ansible playbooks, roles, and collections. Use this skill when working with Ansible files (.yml, .yaml playbooks, roles, inventories), validating automation code, debugging playbook execution, performing dry-run testing with check mode, or working with custom modules and collections.
npx skills add github:pantheon-org/tekhne --skill infrastructure--ansible--validatorOverview
Comprehensive toolkit for validating, linting, and testing Ansible playbooks, roles, and collections. Ensures code quality through syntax validation, lint enforcement, dry-run testing, automatic molecule testing for roles, and intelligent documentation lookup for custom modules with version awareness.
Key Behavior: Molecule tests run automatically for roles with molecule/ directories. If blocked by environment issues, document the blocker and continue with other validation steps.
Quick Start
./scripts/setup_tools.sh # diagnose required tools
./scripts/validate_playbook.sh playbook.yml # syntax + yamllint + ansible-lint
./scripts/validate_role.sh roles/webserver/ # structure + syntax + lint
Validation Workflow
Follow this decision tree for comprehensive Ansible validation:
0. Tool Prerequisites Check (RECOMMENDED for first-time validation)
└─> Run bash scripts/setup_tools.sh for diagnostics
1. Identify Ansible files in scope
├─> Single playbook / role / collection / multi-playbook validation
2. Syntax Validation
├─> Run ansible-playbook --syntax-check
└─> Run yamllint for YAML syntax
3. Lint and Best Practices
├─> Run ansible-lint
├─> Check for deprecated modules (references/module_alternatives.md)
├─> Detect non-FQCN usage: bash scripts/check_fqcn.sh
└─> Verify role structure
4. Dry-Run Testing (check mode)
└─> Run ansible-playbook --check --diff (if inventory available)
5. Molecule Testing (for roles)
├─> Check if molecule/ directory exists
├─> If present, run bash scripts/test_role.sh <role-path> automatically
└─> Report results (pass/fail/blocked) with any environmental blockers
6. Custom Module/Collection Analysis (if detected)
├─> Run bash scripts/extract_ansible_info_wrapper.sh <path> to extract info
├─> Lookup docs via Context7 MCP or WebSearch (see Custom Module Lookup below)
└─> Provide version-specific guidance
6. Security and Best Practices Review
├─> Run bash scripts/validate_playbook_security.sh or validate_role_security.sh (Checkov)
└─> Run bash scripts/scan_secrets.sh (detects hardcoded credentials)
7. Reference Documentation
├─> Read references/common_errors.md when errors detected
├─> Read references/best_practices.md when warnings detected
├─> Read references/module_alternatives.md for deprecated/non-FQCN modules
└─> Read references/security_checklist.md when security issues found
Core Capabilities (Summary)
1. YAML Syntax Validation
Run yamllint then ansible-playbook --syntax-check. Fix YAML errors first, then Ansible-specific issues. See references/common_errors.md for solutions.
yamllint -c .yamllint .
ansible-playbook playbook.yml --syntax-check
2. Ansible Lint
Run ansible-lint to enforce best practices. Use --fix for auto-fixable issues; review before applying.
ansible-lint playbook.yml # single playbook
ansible-lint . # entire directory
ansible-lint -x yaml[line-length] playbook.yml # skip rule
ansible-lint -f pep8 playbook.yml # parseable output
3. Security Scanning
Run both Checkov and secrets scanning:
bash scripts/validate_playbook_security.sh playbook.yml
bash scripts/validate_role_security.sh roles/webserver/
bash scripts/scan_secrets.sh <playbook.yml|role-dir|directory>
Checkov covers SSL/TLS, HTTPS enforcement, package GPG verification. Secrets scan catches hardcoded credentials. Key checks: CKV_ANSIBLE_1-6, CKV2_ANSIBLE_1-6. Full policy: <https://www.checkov.io/5.Policy%20Index/ansible.html>
# BAD
- get_url:
url: https://example.com/file.tar.gz
dest: /tmp/file.tar.gz
validate_certs: false
# GOOD
- get_url:
url: https://example.com/file.tar.gz
dest: /tmp/file.tar.gz
# validate_certs: true (default)
4. Dry-Run Testing (Check Mode)
ansible-playbook -i inventory playbook.yml --check --diff
ansible-playbook -i inventory playbook.yml --check -v --limit webservers
ansible-playbook -i inventory playbook.yml --check --tags deploy
Interpret output: ok = no change, changed = would change, failed = would fail (check check_mode support), skipped = conditional skip. Note: not all modules support check mode — use check_mode: no override where needed.
5. Molecule Testing
When molecule/ is detected, bash scripts/test_role.sh <role-path> runs automatically.
molecule init scenario --driver-name docker # initialize
molecule test # full sequence
molecule test -s alternative # specific scenario
molecule converge && molecule login # debug mode
Test sequence: dependency → lint → destroy → syntax → create → prepare → converge → idempotence → verify → destroy. Check molecule/default/molecule.yml for driver and platform configuration.
If molecule fails due to environment (Docker, versions), document the blocker but don’t fail overall validation. If molecule fails due to role code, provide detailed debugging steps.
6. Custom Module and Collection Documentation Lookup
bash scripts/extract_ansible_info_wrapper.sh <playbook-or-role>
# Outputs JSON: modules, collections, versions
For public collections (community.general, ansible.posix, cisco.ios, etc.):
mcp__context7__resolve-library-idwith collection namemcp__context7__get-library-docsfor specific module
For custom/private collections — use WebSearch:
"[collection-namespace].[module-name] ansible version [version] documentation"
"ansible [module-name] error: [error-message]"
Check version compatibility against requirements.yml / galaxy.yml; flag deprecated parameters and breaking changes.
7. Security and Best Practices
Key areas (see references/security_checklist.md and references/best_practices.md for full checklists):
- Secrets: Use Ansible Vault / environment variables; never hardcode credentials
- Privilege escalation: Minimize
become: yesscope; always specifybecome_user - File permissions: Always set
mode:on file/template tasks; encrypt sensitive files - Command injection: Use
quotefilter; avoid raw{{ var }}in shell/command modules - Network: Enforce HTTPS; never disable
validate_certs - Idempotency: Prefer proper modules over
command/shell; usecreates/removeswhen needed - Error handling: Use
block/rescue/always; setfailed_whenfor custom conditions
Tool Prerequisites
ansible --version && ansible-playbook --version
ansible-lint --version
yamllint --version
molecule --version
# Install missing tools
pip install ansible ansible-lint yamllint ansible-compat
pip install molecule molecule-docker
Minimum versions: Ansible >= 2.9 (recommend >= 2.12), ansible-lint >= 6.0.0, yamllint >= 1.26.0, molecule >= 3.4.0
Error Troubleshooting
Consult references/common_errors.md first. Quick reference:
| Error | Solution |
|---|---|
| Module Not Found | ansible-galaxy collection install <ns.name>; check collections/requirements.yml |
| Undefined Variable | Define in vars/defaults/group_vars; use default() filter |
| Template Syntax Error | Verify Jinja2 syntax; check variable types match filters |
| Connection Failed | Verify SSH config/keys; test with ansible -m ping |
| Permission Denied | Add become: yes; verify sudo config |
| Deprecated Module | Check references/module_alternatives.md for FQCN replacement |
Scripts Reference
| Script | Purpose | Usage |
|---|---|---|
setup_tools.sh |
Check/diagnose required tools | bash scripts/setup_tools.sh |
extract_ansible_info_wrapper.sh |
Parse playbooks/roles for module/collection info | bash scripts/extract_ansible_info_wrapper.sh <path> |
validate_playbook.sh |
Syntax + yamllint + ansible-lint | bash scripts/validate_playbook.sh <playbook.yml> |
validate_playbook_security.sh |
Checkov security scan for playbooks | bash scripts/validate_playbook_security.sh <playbook.yml|dir> |
validate_role.sh |
Structure + syntax + lint + molecule config check | bash scripts/validate_role.sh <role-dir> |
validate_role_security.sh |
Checkov security scan for roles | bash scripts/validate_role_security.sh <role-dir> |
test_role.sh |
Molecule test wrapper (auto-installs deps) | bash scripts/test_role.sh <role-dir> [scenario] |
scan_secrets.sh |
Grep-based hardcoded secret detection | bash scripts/scan_secrets.sh <playbook|role-dir|dir> |
check_fqcn.sh |
Detect non-FQCN module usage | bash scripts/check_fqcn.sh <playbook|role-dir|dir> |
All validation scripts auto-install required tools in a temporary venv if not available system-wide.
Integration with Other Skills
- k8s-yaml-validator — when Ansible manages Kubernetes resources
- terraform-validator — when Ansible and Terraform are used together
- k8s-debug — for debugging infrastructure managed by Ansible
Notes
- Validation order: YAML syntax → Ansible syntax → Lint → Security scan → Secrets scan → Check mode → Molecule tests
- Use Ansible Vault for all sensitive data; never commit unencrypted secrets
- Pin collection versions in
requirements.yml; test before upgrading
Philosophy
- Validate statically first, then dynamically: cheap checks (yamllint, syntax, lint) gate the expensive ones (check mode, Molecule).
- Every gate is independent: a passing Molecule run never excuses a lint failure, and a clean lint never excuses a security finding.
- Report environment blockers honestly instead of hiding them or failing the whole run for them.
- Prefer the shipped scripts over ad hoc commands so results are repeatable.
When to Use
- Validating an Ansible playbook, role, inventory or collection before merge.
- Debugging a playbook that fails syntax checks, lint rules or a dry run.
- Running Molecule tests on a role that has a
molecule/directory. - Scanning playbooks or roles for hardcoded secrets and Checkov policy violations.
- Looking up documentation for custom modules or collections and checking version compatibility.
When Not to Use
- Writing new Ansible content from scratch (use the ansible-generator skill).
- Validating Kubernetes manifests or Terraform code (use k8s-yaml-validator or terraform-validator).
- Running a playbook against production hosts: this skill only covers syntax, lint, check mode and tests.
- Debugging cluster or runtime infrastructure problems (use k8s-debug).
Anti-Patterns
NEVER skip ansible-lint because the playbook “works”
- WHY: A playbook can execute successfully while violating idempotency, security, and style rules that cause failures after re-runs or in different environments; lint catches these issues before they reach production.
- BAD: Run the playbook in staging, observe it completing without errors, and skip linting before merging.
- GOOD: Run
ansible-lintin CI before every merge and treat any rule violation as a build failure.
NEVER run --check mode without --diff
- WHY:
--checkreports what would change but without--diffyou cannot see the actual content of the change; the combination is required for a meaningful dry-run review. - BAD:
ansible-playbook --check site.ymlwith no visibility into what file contents would be modified. - GOOD:
ansible-playbook --check --diff site.ymlto see both change detection and exact content diffs.
NEVER use molecule test as a substitute for ansible-lint
- WHY: Molecule tests idempotency and functional correctness via actual execution but does not catch code quality issues, deprecated modules, or style violations that lint detects statically.
- BAD: Skip
ansible-lintand rely only on Molecule passing to consider a role ready for review. - GOOD: Run
ansible-lintfirst (fast, catches obvious errors), then Molecule (slower, catches runtime and idempotency errors); both gates are required.
NEVER ignore no-changed-when lint warnings
- WHY: Tasks using
commandorshellwithoutchanged_whenalways reportchangedeven when nothing changed, breaking idempotency checks, change auditing, and handler triggering logic. - BAD: Dismiss
no-changed-whenas a style warning on acommandtask that only reads system state. - GOOD: Add
changed_when: falsefor genuinely read-only commands, or add an explicitchanged_whencondition that reflects actual state changes (e.g.,changed_when: result.rc == 0).
NEVER disable certificate validation
- WHY:
validate_certs: falseremoves TLS protection from downloads, and the Checkov policies this skill runs flag it as a security finding. - BAD:
validate_certs: falseon aget_urltask to get past a certificate error. - GOOD: Leave
validate_certsat its default of true and fix the certificate chain.
NEVER hardcode credentials in playbooks or roles
- WHY: Plaintext secrets end up in version control and are exactly what
scripts/scan_secrets.shexists to catch. - BAD:
db_password: "hunter2"ingroup_vars/all.yml. - GOOD: Store the value with Ansible Vault or read it from an environment variable, then run
bash scripts/scan_secrets.sh <dir>.
NEVER pass raw variables into shell or command modules
- WHY: Unquoted interpolation allows command injection through variable content.
- BAD:
shell: rm -rf {{ target_dir }} - GOOD:
shell: rm -rf {{ target_dir | quote }}, or use a dedicated module such asansible.builtin.filewithstate: absent.
NEVER use command or shell when a proper module exists
- WHY: Dedicated modules are idempotent and support check mode;
commandandshellare not and trigger lint findings. - BAD:
shell: apt-get install -y nginx - GOOD:
ansible.builtin.packageoransible.builtin.aptwithname: nginxandstate: present.
NEVER leave modules without their fully qualified collection name
- WHY: Short names are ambiguous across collections and are what
scripts/check_fqcn.shreports;references/module_alternatives.mdlists the FQCN replacements. - BAD:
- copy:with the short module name. - GOOD:
- ansible.builtin.copy:with the full collection name.
NEVER fail the whole validation run for a Molecule environment blocker
- WHY: Docker or tool-version problems say nothing about the role code; hiding them or treating them as role defects misleads the reviewer.
- BAD: Report the role as broken because the Docker daemon was not running.
- GOOD: Document the blocker, report Molecule as blocked, and continue with the remaining validation steps.
NEVER ignore a Checkov or secrets-scan finding without recording why
- WHY: Security findings from
scripts/validate_playbook_security.shandscripts/scan_secrets.shpoint to real exposure; silent suppression hides it from later reviewers. - BAD: Delete the failing task or skip the scan so the run goes green.
- GOOD: Fix the finding, or document the justified exception next to the code and in the validation report.
NEVER run ansible-lint --fix and commit without reviewing the diff
- WHY: Auto-fixes rewrite files in bulk and can change behaviour in ways lint cannot judge.
- BAD:
ansible-lint --fix . && git commit -am "lint"with no inspection. - GOOD: Run
ansible-lint --fix, review the resulting diff, then re-runansible-lintto confirm it is clean.
References
| File | Purpose |
|---|---|
references/security_checklist.md |
Security vulnerabilities checklist |
references/best_practices.md |
Ansible coding standards |
references/common_errors.md |
Common errors and solutions |
references/module_alternatives.md |
Deprecated module → FQCN migration guide |
assets/.yamllint |
Pre-configured yamllint rules |
assets/.ansible-lint |
Pre-configured ansible-lint configuration |
assets/molecule.yml.template |
Molecule configuration template |
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Runtime validation via eval scenarios proving the skill actually changes agent behaviour. Requires evals/ directory, instructions.json, ≥3 scenarios, and ≥80% instruction coverage in summary.json.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
No eval scenarios available.
Ansible Best Practices
Overview
This guide provides comprehensive best practices for writing clean, maintainable, and reliable Ansible playbooks, roles, and collections.
Playbook Organization
Directory Structure
ansible-project/
├── ansible.cfg # Ansible configuration
├── inventory/ # Inventory files
│ ├── production/
│ │ ├── hosts # Production inventory
│ │ └── group_vars/
│ │ └── all.yml
│ └── staging/
│ ├── hosts # Staging inventory
│ └── group_vars/
│ └── all.yml
├── group_vars/ # Group-specific variables
│ ├── all.yml
│ ├── webservers.yml
│ └── databases.yml
├── host_vars/ # Host-specific variables
│ └── server1.yml
├── roles/ # Reusable roles
│ ├── common/
│ ├── webserver/
│ └── database/
├── playbooks/ # Playbooks
│ ├── site.yml # Master playbook
│ ├── webservers.yml
│ └── databases.yml
├── files/ # Static files
├── templates/ # Jinja2 templates
├── vars/ # Additional variables
│ └── external_vars.yml
└── requirements.yml # Collection dependencies
Role Structure
roles/webserver/
├── README.md # Role documentation
├── defaults/
│ └── main.yml # Default variables (lowest precedence)
├── vars/
│ └── main.yml # Role variables (higher precedence)
├── tasks/
│ ├── main.yml # Main task list
│ ├── install.yml # Installation tasks
│ └── configure.yml # Configuration tasks
├── handlers/
│ └── main.yml # Handlers
├── templates/
│ └── nginx.conf.j2 # Template files
├── files/
│ └── index.html # Static files
├── meta/
│ └── main.yml # Role metadata and dependencies
└── molecule/ # Molecule test scenarios
└── default/
├── molecule.yml
├── converge.yml
└── verify.yml
Task Naming and Documentation
✅ Good Task Names
# Descriptive, action-oriented names
- name: Install nginx web server
apt:
name: nginx
state: present
- name: Configure nginx virtual host for example.com
template:
src: vhost.conf.j2
dest: /etc/nginx/sites-available/example.com
- name: Enable and start nginx service
systemd:
name: nginx
state: started
enabled: yes
- name: Create application user with limited privileges
user:
name: appuser
system: yes
shell: /bin/false
home: /var/lib/app
❌ Bad Task Names
# Vague, uninformative names
- name: Install package
apt:
name: nginx
- name: Configure
template:
src: vhost.conf.j2
dest: /etc/nginx/sites-available/example.com
- name: Service
systemd:
name: nginx
state: started
# No name at all
- apt:
name: nginx
Best Practices
- Always name your tasks - makes output readable
- Use action verbs - Install, Configure, Enable, Create, etc.
- Be specific - mention what is being installed/configured
- Keep names concise - but not at the expense of clarity
- Use consistent naming - across all playbooks
Variable Management
Variable Naming Conventions
# ✅ Good - Descriptive, namespaced
nginx_version: "1.18.0"
nginx_worker_processes: 4
nginx_worker_connections: 1024
app_database_host: "db.example.com"
app_database_port: 5432
# ❌ Bad - Generic, collision-prone
version: "1.18.0" # Too generic
workers: 4 # Unclear
db: "db.example.com" # Vague
Variable Precedence
Understand variable precedence (from lowest to highest):
- role defaults (defaults/main.yml)
- inventory file or script group vars
- inventory group_vars/all
- playbook group_vars/all
- inventory group_vars/*
- playbook group_vars/*
- inventory file or script host vars
- inventory host_vars/*
- playbook host_vars/*
- host facts / cached set_facts
- play vars
- play vars_prompt
- play vars_files
- role vars (vars/main.yml)
- block vars
- task vars
- include_vars
- set_facts / registered vars
- role (and include_role) params
- include params
- extra vars (always win precedence)
Variable Organization
# defaults/main.yml - Intended to be overridden
---
nginx_port: 80
nginx_user: www-data
nginx_worker_processes: "auto"
# vars/main.yml - Should not be overridden
---
nginx_config_dir: /etc/nginx
nginx_log_dir: /var/log/nginx
nginx_pid_file: /run/nginx.pid
Using Defaults and Required Variables
# Use default filter for optional variables
- name: Set API endpoint
set_fact:
api_endpoint: "{{ custom_api_endpoint | default('https://api.example.com') }}"
# Use required filter for mandatory variables
- name: Configure database
template:
src: db.conf.j2
dest: /etc/app/database.conf
vars:
db_password: "{{ database_password | required('database_password must be defined') }}"
Idempotency
What is Idempotency?
Idempotency means running the same playbook multiple times produces the same result without making unnecessary changes.
✅ Idempotent Tasks
# File module - inherently idempotent
- name: Ensure configuration directory exists
file:
path: /etc/myapp
state: directory
mode: '0755'
# Template module - only changes if content differs
- name: Configure application
template:
src: app.conf.j2
dest: /etc/myapp/app.conf
mode: '0644'
# Package module - idempotent
- name: Install required packages
apt:
name:
- nginx
- python3
- git
state: present
# Service module - idempotent
- name: Ensure service is running
systemd:
name: myapp
state: started
enabled: yes
⚠️ Non-Idempotent Tasks (Need Fixes)
# Command/shell without creates/removes
- name: Download file
command: curl -o /tmp/file.tar.gz https://example.com/file.tar.gz
# This runs every time!
# Fix with creates
- name: Download file
command: curl -o /tmp/file.tar.gz https://example.com/file.tar.gz
args:
creates: /tmp/file.tar.gz
# Or better - use get_url module
- name: Download file
get_url:
url: https://example.com/file.tar.gz
dest: /tmp/file.tar.gz
checksum: sha256:abc123...
# Command that always reports changed
- name: Check service status
command: systemctl status myapp
register: service_status
# Always shows as changed!
# Fix with changed_when
- name: Check service status
command: systemctl status myapp
register: service_status
changed_when: false
failed_when: service_status.rc not in [0, 3]
Best Practices for Idempotency
- Use modules instead of command/shell whenever possible
- Use creates/removes parameters for command/shell when necessary
- Set changed_when appropriately for read-only commands
- Test idempotency - run playbook twice, second run should show no changes
- Use check mode to verify idempotency without making changes
Module Selection
Prefer Modules Over Commands
# ❌ Bad - Using shell/command
- name: Create directory
shell: mkdir -p /opt/myapp
- name: Install package
command: apt-get install -y nginx
- name: Add line to file
shell: echo "export PATH=$PATH:/opt/bin" >> ~/.bashrc
# ✅ Good - Using appropriate modules
- name: Create directory
file:
path: /opt/myapp
state: directory
mode: '0755'
- name: Install package
apt:
name: nginx
state: present
- name: Add line to file
lineinfile:
path: ~/.bashrc
line: 'export PATH=$PATH:/opt/bin'
create: yes
Module Hierarchy
- First choice: Specific module (apt, yum, systemd, copy, etc.)
- Second choice: Generic module (package, service, etc.)
- Last resort: command or shell module
Error Handling
Using Blocks
- name: Handle errors gracefully
block:
- name: Attempt risky operation
command: /usr/local/bin/risky-operation.sh
register: result
- name: Process successful result
debug:
msg: "Operation succeeded: {{ result.stdout }}"
rescue:
- name: Handle failure
debug:
msg: "Operation failed, applying fallback"
- name: Apply fallback configuration
copy:
src: fallback.conf
dest: /etc/app/config.conf
always:
- name: Cleanup temporary files
file:
path: /tmp/operation.lock
state: absent
Failed When and Changed When
# Custom failure conditions
- name: Check disk space
shell: df -h / | tail -1 | awk '{print $5}' | sed 's/%//'
register: disk_usage
failed_when: disk_usage.stdout | int > 90
# Custom changed conditions
- name: Verify configuration
command: /usr/local/bin/check-config.sh
register: config_check
changed_when: false
failed_when: config_check.rc != 0
# Multiple conditions
- name: Run healthcheck
uri:
url: http://localhost:8080/health
method: GET
register: health
failed_when:
- health.status != 200
- "'healthy' not in health.json.status"
Ignoring Errors (Use Sparingly)
# Only when failure is acceptable
- name: Try to stop service (may not exist)
systemd:
name: old-service
state: stopped
ignore_errors: yes
# Better approach - check first
- name: Check if service exists
systemd:
name: old-service
register: service_status
failed_when: false
- name: Stop service if it exists
systemd:
name: old-service
state: stopped
when: service_status.status.ActiveState is defined
Conditionals and Loops
When Conditions
# Simple condition
- name: Install Apache (Debian)
apt:
name: apache2
state: present
when: ansible_os_family == "Debian"
# Multiple conditions (AND)
- name: Install package on Ubuntu 20.04
apt:
name: package
state: present
when:
- ansible_distribution == "Ubuntu"
- ansible_distribution_version == "20.04"
# OR conditions
- name: Install on RHEL or CentOS
yum:
name: package
state: present
when: ansible_distribution == "RedHat" or ansible_distribution == "CentOS"
# Complex conditions
- name: Configure firewall
ufw:
rule: allow
port: '443'
when:
- ansible_os_family == "Debian"
- firewall_enabled | default(true) | bool
- ansible_virtualization_type != "docker"
Loops
# Simple loop
- name: Install packages
apt:
name: "{{ item }}"
state: present
loop:
- nginx
- python3
- git
# Loop with hash
- name: Create users
user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
state: present
loop:
- { name: 'alice', groups: 'developers' }
- { name: 'bob', groups: 'operators' }
# Loop with dict
- name: Create directories
file:
path: "{{ item.path }}"
state: directory
mode: "{{ item.mode }}"
loop:
- { path: '/opt/app', mode: '0755' }
- { path: '/var/log/app', mode: '0755' }
- { path: '/etc/app', mode: '0750' }
# Loop with conditional
- name: Install debug tools (dev only)
apt:
name: "{{ item }}"
state: present
loop:
- strace
- tcpdump
- gdb
when: environment == "development"
Templates and Jinja2
Template Best Practices
{# templates/nginx.conf.j2 #}
{# Use comments to explain complex logic #}
user {{ nginx_user }};
worker_processes {{ nginx_worker_processes }};
pid {{ nginx_pid_file }};
{# Conditionals in templates #}
{% if nginx_enable_ssl %}
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
{% endif %}
{# Loops in templates #}
{% for vhost in nginx_vhosts %}
server {
listen {{ vhost.port }};
server_name {{ vhost.server_name }};
root {{ vhost.document_root }};
{% if vhost.ssl_enabled | default(false) %}
ssl_certificate {{ vhost.ssl_cert }};
ssl_certificate_key {{ vhost.ssl_key }};
{% endif %}
}
{% endfor %}
{# Filters #}
upstream_servers = {{ backend_servers | join(',') }}
max_connections = {{ max_connections | default(1024) }}
Useful Jinja2 Filters
# String manipulation
- debug:
msg: "{{ 'hello' | upper }}" # HELLO
msg: "{{ 'HELLO' | lower }}" # hello
msg: "{{ ' hello ' | trim }}" # hello
# List operations
- debug:
msg: "{{ [1,2,3] | first }}" # 1
msg: "{{ [1,2,3] | last }}" # 3
msg: "{{ [1,2,3] | length }}" # 3
msg: "{{ [1,2,3] | join(',') }}" # 1,2,3
# Default values
- debug:
msg: "{{ undefined_var | default('default_value') }}"
# Type conversion
- debug:
msg: "{{ '123' | int }}" # 123
msg: "{{ 'true' | bool }}" # True
# JSON and YAML
- debug:
msg: "{{ my_dict | to_json }}"
msg: "{{ my_dict | to_nice_json }}"
msg: "{{ my_dict | to_yaml }}"
Tags
Using Tags Effectively
---
- name: Configure web server
hosts: webservers
tasks:
- name: Install nginx
apt:
name: nginx
tags:
- packages
- nginx
- name: Configure nginx
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
tags:
- configuration
- nginx
- name: Start nginx
systemd:
name: nginx
state: started
tags:
- services
- nginx
- name: Configure firewall
ufw:
rule: allow
port: '80'
tags:
- security
- firewall
Running with Tags
# Run only nginx tasks
ansible-playbook site.yml --tags nginx
# Run configuration tasks only
ansible-playbook site.yml --tags configuration
# Skip certain tags
ansible-playbook site.yml --skip-tags packages
# Multiple tags
ansible-playbook site.yml --tags "nginx,firewall"
Handlers
Handler Best Practices
# tasks/main.yml
- name: Configure nginx
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify:
- Validate nginx configuration
- Restart nginx
- name: Add virtual host
template:
src: vhost.conf.j2
dest: "/etc/nginx/sites-available/{{ vhost_name }}"
notify:
- Reload nginx
# handlers/main.yml
- name: Validate nginx configuration
command: nginx -t
changed_when: false
- name: Restart nginx
systemd:
name: nginx
state: restarted
- name: Reload nginx
systemd:
name: nginx
state: reloaded
Handler Facts
- Handlers run once at the end of a play, even if notified multiple times
- Handlers run in order they’re defined, not in order they’re notified
- Use listen for handler groups
- Flush handlers with
meta: flush_handlersto run immediately
Check Mode and Diff Mode
Supporting Check Mode
# Task that supports check mode naturally (file module)
- name: Create directory
file:
path: /opt/myapp
state: directory
# Task that doesn't support check mode, but can run anyway
- name: Check service status
command: systemctl status myapp
check_mode: no # Always run, even in check mode
changed_when: false
# Task that should be skipped in check mode
- name: Apply complex changes
command: /usr/local/bin/complex-script.sh
when: not ansible_check_mode
Using Check Mode
# Run in check mode (dry-run)
ansible-playbook site.yml --check
# Check mode with diff (show changes)
ansible-playbook site.yml --check --diff
# See what would change
ansible-playbook site.yml --check --diff | grep -A 10 "changed:"
Documentation
Playbook Documentation
---
# site.yml - Master playbook for deploying web application
#
# This playbook:
# - Configures common settings on all hosts
# - Deploys web servers
# - Configures databases
# - Sets up load balancers
#
# Usage:
# ansible-playbook -i inventory/production site.yml
#
# Tags:
# - common: Common configuration tasks
# - webserver: Web server setup
# - database: Database configuration
#
# Variables (see group_vars/all.yml):
# - app_version: Application version to deploy
# - environment: Environment name (production/staging)
- name: Configure common settings
hosts: all
roles:
- common
tags: common
- name: Deploy web servers
hosts: webservers
roles:
- webserver
tags: webserver
Role Documentation (README.md)
# Webserver Role
## Description
Installs and configures Nginx web server with virtual hosts and SSL support.
## Requirements
- Ansible >= 2.9
- Supported OS: Ubuntu 20.04, Debian 11
## Role Variables
### Required Variables
- `nginx_vhosts`: List of virtual hosts to configure (see example)
### Optional Variables
- `nginx_worker_processes`: Number of worker processes (default: auto)
- `nginx_worker_connections`: Max connections per worker (default: 1024)
- `nginx_enable_ssl`: Enable SSL support (default: false)
## Dependencies
None
## Example Playbook
```yaml
- hosts: webservers
roles:
- role: webserver
vars:
nginx_vhosts:
- server_name: example.com
port: 80
document_root: /var/www/example
License
MIT
Author
Your Name
## Testing Best Practices
See the molecule configuration and testing section in the main skill.md for comprehensive testing guidance.
## Performance Tips
1. **Use pipelining** in ansible.cfg
```ini
[ssh_connection]
pipelining = True
-
Enable fact caching
[defaults] gathering = smart fact_caching = jsonfile fact_caching_connection = /tmp/ansible_facts fact_caching_timeout = 86400 -
Limit fact gathering
- hosts: all gather_facts: no # Don't gather if not needed -
Use async for long-running tasks
- name: Long running task command: /usr/local/bin/long-task.sh async: 3600 poll: 0 register: long_task - name: Check on long task async_status: jid: "{{ long_task.ansible_job_id }}" register: job_result until: job_result.finished retries: 30
Summary Checklist
- Playbooks and roles have clear directory structure
- All tasks have descriptive names
- Variables use namespacing (role_variable_name)
- Sensitive data encrypted with Ansible Vault
- Playbooks are idempotent (can run multiple times safely)
- Using modules instead of shell/command where possible
- Error handling with blocks, failed_when, changed_when
- Conditionals used appropriately
- Templates properly commented
- Tags used for granular execution
- Handlers used for service restarts
- Check mode supported
- Documentation complete (README, comments)
- Tested with molecule or similar framework
- No hardcoded secrets
- File permissions explicitly set
Common Ansible Errors and Solutions
Overview
This document provides solutions to common Ansible errors, including syntax errors, module errors, connection issues, and runtime problems.
Syntax Errors
Error: mapping values are not allowed here
ERROR! Syntax Error while loading YAML.
mapping values are not allowed here
Cause: YAML indentation error or missing quote
Example Problem:
- name: Configure app
template:
src: config.j2
dest: /etc/app/config.yml
vars:
db_host: localhost:5432 # WRONG: colon not quoted
Solution:
- name: Configure app
template:
src: config.j2
dest: /etc/app/config.yml
vars:
db_host: "localhost:5432" # Quoted
Error: found undefined alias
ERROR! Syntax Error while loading YAML.
found undefined alias 'anchor'
Cause: Using YAML anchor/alias incorrectly
Solution: Ensure anchors are defined before use
# Define anchor
common_packages: &common_packages
- git
- curl
- vim
# Use alias
- name: Install common packages
apt:
name: *common_packages
Error: could not find expected ‘:’
ERROR! could not find expected ':'
Cause: Missing colon or improper YAML structure
Example Problem:
- name Install package # Missing colon after name
apt:
name nginx # Missing colon after name
Solution:
- name: Install package
apt:
name: nginx
Module Errors
Error: Unsupported parameters for module
ERROR! Unsupported parameters for (module) module: parameter_name
Cause: Using wrong parameter name or typo
Example Problem:
- name: Create file
file:
path: /tmp/test
state: present
mod: '0644' # WRONG: should be 'mode'
Solution:
- name: Create file
file:
path: /tmp/test
state: present
mode: '0644' # Correct parameter name
How to check: Use ansible-doc module_name to see correct parameters
Error: MODULE FAILURE
fatal: [host]: FAILED! => {"changed": false, "module_stderr": "..."}
Common Causes:
- Python not installed on target
- Wrong Python interpreter
- SELinux blocking module execution
Solutions:
# Specify Python interpreter in inventory
[webservers]
server1 ansible_python_interpreter=/usr/bin/python3
# Or in playbook
- hosts: all
vars:
ansible_python_interpreter: /usr/bin/python3
Error: Missing required arguments
fatal: [host]: FAILED! => {"changed": false, "msg": "missing required arguments: name"}
Cause: Required module parameter not provided
Solution: Add the required parameter
# Wrong
- name: Install package
apt:
state: present
# Correct
- name: Install package
apt:
name: nginx
state: present
Template Errors
Error: template error while templating string
fatal: [host]: FAILED! => {"msg": "An unhandled exception occurred while templating..."}
Common Causes:
- Undefined variable
- Wrong filter syntax
- Jinja2 syntax error
Example Problem:
- name: Configure app
template:
src: config.j2
dest: /etc/app/config.yml
vars:
port: "{{ app_port }}" # app_port undefined
Solutions:
# Use default filter
vars:
port: "{{ app_port | default(8080) }}"
# Or use required filter
vars:
port: "{{ app_port | required('app_port must be defined') }}"
# Or check if defined
- name: Configure app
template:
src: config.j2
dest: /etc/app/config.yml
when: app_port is defined
Error: Unexpected templating type error
fatal: [host]: FAILED! => {"msg": "Unexpected templating type error occurred on (...)"}
Cause: Wrong variable type (e.g., trying to use int as string)
Solution: Use type conversion filters
# Convert to string
port: "{{ app_port | string }}"
# Convert to int
replicas: "{{ replica_count | int }}"
# Convert to bool
enabled: "{{ feature_enabled | bool }}"
Connection Errors
Error: Failed to connect to the host via ssh
fatal: [host]: UNREACHABLE! => {"msg": "Failed to connect to the host via ssh"}
Common Causes:
- Host not accessible
- Wrong SSH key
- Wrong username
- SSH not running on host
Solutions:
# Test SSH connectivity
ssh user@host
# Check Ansible can ping
ansible host -m ping
# Use correct SSH key
ansible-playbook -i inventory playbook.yml --private-key=~/.ssh/id_rsa
# Specify user in inventory
[webservers]
server1 ansible_user=ubuntu ansible_ssh_private_key_file=~/.ssh/id_rsa
Error: Permission denied (publickey)
fatal: [host]: UNREACHABLE! => {"msg": "Failed to connect to the host via ssh: Permission denied (publickey)."}
Solutions:
# Ensure SSH key is added to target
ssh-copy-id user@host
# Or specify key in inventory
[webservers]
server1 ansible_ssh_private_key_file=~/.ssh/custom_key
# Check SSH agent
ssh-add -l
ssh-add ~/.ssh/id_rsa
Error: Authentication or permission failure
fatal: [host]: UNREACHABLE! => {"msg": "Authentication or permission failure."}
Solutions:
# Use password authentication (less secure)
- hosts: all
vars:
ansible_ssh_pass: password # Better to use vault
ansible_become_pass: password
# Or use ask-pass
ansible-playbook -i inventory playbook.yml --ask-pass --ask-become-pass
Privilege Escalation Errors
Error: Missing sudo password
fatal: [host]: FAILED! => {"msg": "Missing sudo password"}
Solutions:
# Provide sudo password at runtime
ansible-playbook -i inventory playbook.yml --ask-become-pass
# Or configure passwordless sudo on target
# /etc/sudoers.d/ansible
ansible_user ALL=(ALL) NOPASSWD: ALL
Error: you must be root
fatal: [host]: FAILED! => {"msg": "Could not create file: Permission denied"}
Solution: Add become: yes to task or play
- name: Install package
apt:
name: nginx
state: present
become: yes
# Or for entire play
- hosts: all
become: yes
tasks:
- name: Install package
apt:
name: nginx
Variable Errors
Error: The task includes an option with an undefined variable
fatal: [host]: FAILED! => {"msg": "The task includes an option with an undefined variable. The error was: 'variable' is undefined"}
Solutions:
# Use default filter
- name: Use variable with default
debug:
msg: "{{ my_var | default('default_value') }}"
# Check if defined before use
- name: Use variable conditionally
debug:
msg: "{{ my_var }}"
when: my_var is defined
# Use required filter to make it explicit
- name: Require variable
debug:
msg: "{{ my_var | required('my_var must be defined') }}"
Error: Conflicting variable name
[WARNING]: Invalid characters were found in group names but not replaced, use -vvvv to see details
Cause: Variable or group name contains invalid characters (hyphens, spaces)
Solution: Use underscores instead
# Wrong
[web-servers]
# Correct
[web_servers]
Inventory Errors
Error: Could not match supplied host pattern
[WARNING]: Could not match supplied host pattern, ignoring: webservers
Cause: Host group not defined in inventory
Solution: Check inventory file
# inventory/hosts
[webservers]
web1.example.com
web2.example.com
[databases]
db1.example.com
Error: Unable to parse inventory
[WARNING]: Unable to parse /path/to/inventory as an inventory source
Cause: Invalid inventory format
Solution: Fix inventory syntax
# Wrong - mixing styles
[webservers]
web1 ansible_host=192.168.1.10
web2
ansible_host: 192.168.1.11 # YAML syntax in INI file
# Correct - consistent INI format
[webservers]
web1 ansible_host=192.168.1.10
web2 ansible_host=192.168.1.11
Loop Errors
Error: Invalid data passed to ‘loop’
fatal: [host]: FAILED! => {"msg": "Invalid data passed to 'loop', it requires a list"}
Cause: Loop variable is not a list
Solution: Ensure loop variable is a list
# Wrong
- name: Install packages
apt:
name: "{{ item }}"
loop: nginx # String, not list
# Correct
- name: Install packages
apt:
name: "{{ item }}"
loop:
- nginx
- python3
Error: with_items is deprecated
[DEPRECATION WARNING]: with_items is deprecated, use loop instead
Solution: Replace with_items with loop
# Old style (deprecated)
- name: Install packages
apt:
name: "{{ item }}"
with_items:
- nginx
- python3
# New style
- name: Install packages
apt:
name: "{{ item }}"
loop:
- nginx
- python3
Handler Errors
Error: Handler not found
ERROR! The requested handler 'restart nginx' was not found
Cause: Handler name mismatch or handler not defined
Solution: Ensure handler name matches exactly
# tasks/main.yml
- name: Configure nginx
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx # Must match handler name exactly
# handlers/main.yml
- name: restart nginx # Must match notification exactly
systemd:
name: nginx
state: restarted
Include/Import Errors
Error: Unable to retrieve file contents
fatal: [host]: FAILED! => {"msg": "Unable to retrieve file contents. Could not find or access 'file.yml'"}
Cause: File path incorrect or file doesn’t exist
Solution: Check file path (relative to playbook location)
# Wrong
- include_tasks: tasks/install.yml # If tasks/ doesn't exist
# Correct
- include_tasks: install.yml # File in same directory
# Or
- include_tasks: roles/common/tasks/install.yml # Full path
Error: Include/Import loop detected
ERROR! Recursively included/imported file is causing infinite loop
Cause: Circular dependency (file A includes file B, file B includes file A)
Solution: Restructure includes to avoid circular dependencies
Collection Errors
Error: couldn’t resolve module/action
ERROR! couldn't resolve module/action 'community.general.docker_container'
Cause: Collection not installed
Solution: Install required collection
# Install single collection
ansible-galaxy collection install community.general
# Install from requirements.yml
# requirements.yml
collections:
- name: community.general
version: ">=5.0.0"
ansible-galaxy collection install -r requirements.yml
Error: Collection version conflict
ERROR! Requirement already satisfied by a different version
Solution: Update or downgrade collection
# Force reinstall
ansible-galaxy collection install community.general --force
# Install specific version
ansible-galaxy collection install community.general:5.0.0
Dry-Run / Check Mode Errors
Error: This module does not support check mode
fatal: [host]: FAILED! => {"msg": "This module does not support check mode"}
Cause: Module doesn’t support check mode
Solution: Skip check mode for this task
- name: Command that doesn't support check mode
command: /usr/local/bin/custom-script.sh
check_mode: no # Always run, even in check mode
Debugging Tips
Enable Verbose Output
# Basic verbosity
ansible-playbook playbook.yml -v
# More details
ansible-playbook playbook.yml -vv
# Very verbose (shows module arguments)
ansible-playbook playbook.yml -vvv
# Connection debugging
ansible-playbook playbook.yml -vvvv
Use Debug Module
# Print variable
- name: Debug variable
debug:
var: my_variable
# Print message
- name: Debug message
debug:
msg: "Value is {{ my_variable }}"
# Print all facts
- name: Print all facts
debug:
var: ansible_facts
# Conditional debug
- name: Debug when condition met
debug:
msg: "Debug message"
when: ansible_distribution == "Ubuntu"
Use assert Module
# Validate conditions
- name: Assert variables are defined
assert:
that:
- app_version is defined
- app_version | length > 0
- app_port | int > 0
- app_port | int < 65536
fail_msg: "Invalid configuration"
success_msg: "Configuration validated"
Performance Issues
Slow Playbook Execution
Solutions:
- Enable SSH pipelining
# ansible.cfg
[ssh_connection]
pipelining = True
- Use fact caching
# ansible.cfg
[defaults]
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 86400
- Disable fact gathering if not needed
- hosts: all
gather_facts: no
- Use async for long tasks
- name: Long running task
command: /usr/bin/long-task
async: 3600
poll: 0
High Memory Usage
Solutions:
- Process hosts in batches
- hosts: all
serial: 10 # Process 10 hosts at a time
- Use free strategy
- hosts: all
strategy: free # Don't wait for all hosts to complete task
Error Prevention Checklist
- Run yamllint before ansible-playbook
- Run ansible-lint on all playbooks
- Use –syntax-check before execution
- Test with –check mode first
- Start with limited host scope (–limit)
- Use tags for incremental testing
- Enable verbose mode for debugging (-vvv)
- Validate variables with assert
- Use molecule for role testing
- Test in staging before production
- Keep collections up to date
- Document custom variables
- Use version control for all playbooks
Quick Reference Commands
# Syntax check
ansible-playbook playbook.yml --syntax-check
# Dry run
ansible-playbook playbook.yml --check --diff
# Run with tags
ansible-playbook playbook.yml --tags webserver
# Limit to specific hosts
ansible-playbook playbook.yml --limit webserver1
# Verbose output
ansible-playbook playbook.yml -vvv
# List tasks
ansible-playbook playbook.yml --list-tasks
# List hosts
ansible-playbook playbook.yml --list-hosts
# Step through tasks
ansible-playbook playbook.yml --step
# Start at specific task
ansible-playbook playbook.yml --start-at-task="Install nginx"
Ansible Module Alternatives
Overview
This guide provides replacement alternatives for deprecated or legacy Ansible modules. Use this reference when ansible-lint reports deprecated module warnings or when updating older playbooks to modern best practices.
Quick Detection
Use the FQCN checker script to automatically detect non-FQCN module usage:
# Scan a playbook
bash scripts/check_fqcn.sh playbook.yml
# Scan a role
bash scripts/check_fqcn.sh roles/webserver/
# Scan entire directory
bash scripts/check_fqcn.sh .
The script identifies modules using short names and provides specific FQCN migration recommendations.
Deprecated Modules and Replacements
Package Management
| Deprecated Module | Replacement | Notes |
|---|---|---|
apt (short name) |
ansible.builtin.apt |
Use FQCN for clarity |
yum (short name) |
ansible.builtin.yum or ansible.builtin.dnf |
dnf preferred for RHEL 8+ |
pip (short name) |
ansible.builtin.pip |
Use FQCN |
easy_install |
ansible.builtin.pip |
easy_install is deprecated in Python |
homebrew |
community.general.homebrew |
Moved to community.general |
zypper |
community.general.zypper |
Moved to community.general |
apk |
community.general.apk |
Moved to community.general |
File Operations
| Deprecated Module | Replacement | Notes |
|---|---|---|
copy (short name) |
ansible.builtin.copy |
Use FQCN |
file (short name) |
ansible.builtin.file |
Use FQCN |
template (short name) |
ansible.builtin.template |
Use FQCN |
lineinfile (short name) |
ansible.builtin.lineinfile |
Use FQCN |
blockinfile (short name) |
ansible.builtin.blockinfile |
Use FQCN |
synchronize |
ansible.posix.synchronize |
Moved to ansible.posix |
acl |
ansible.posix.acl |
Moved to ansible.posix |
Service Management
| Deprecated Module | Replacement | Notes |
|---|---|---|
service (short name) |
ansible.builtin.service or ansible.builtin.systemd |
Use systemd for systemd-based systems |
systemd (short name) |
ansible.builtin.systemd |
Use FQCN |
sysvinit |
ansible.builtin.service |
service module handles sysvinit |
User and Group Management
| Deprecated Module | Replacement | Notes |
|---|---|---|
user (short name) |
ansible.builtin.user |
Use FQCN |
group (short name) |
ansible.builtin.group |
Use FQCN |
authorized_key (short name) |
ansible.posix.authorized_key |
Moved to ansible.posix |
Networking
| Deprecated Module | Replacement | Notes |
|---|---|---|
get_url (short name) |
ansible.builtin.get_url |
Use FQCN |
uri (short name) |
ansible.builtin.uri |
Use FQCN |
iptables |
ansible.builtin.iptables |
Use FQCN |
ufw |
community.general.ufw |
Moved to community.general |
firewalld |
ansible.posix.firewalld |
Moved to ansible.posix |
Command Execution
| Deprecated Module | Replacement | Notes |
|---|---|---|
command (short name) |
ansible.builtin.command |
Use FQCN; prefer specific modules |
shell (short name) |
ansible.builtin.shell |
Use FQCN; prefer specific modules |
raw (short name) |
ansible.builtin.raw |
Use FQCN; use only when necessary |
script (short name) |
ansible.builtin.script |
Use FQCN |
Cloud Providers
| Deprecated Module | Replacement | Notes |
|---|---|---|
ec2 |
amazon.aws.ec2_instance |
Use amazon.aws collection |
ec2_ami |
amazon.aws.ec2_ami |
Use amazon.aws collection |
ec2_vpc |
amazon.aws.ec2_vpc_net |
Use amazon.aws collection |
azure_rm_* |
azure.azcollection.* |
Use azure.azcollection |
gcp_* |
google.cloud.* |
Use google.cloud collection |
docker_container |
community.docker.docker_container |
Use community.docker collection |
docker_image |
community.docker.docker_image |
Use community.docker collection |
Database
| Deprecated Module | Replacement | Notes |
|---|---|---|
mysql_db |
community.mysql.mysql_db |
Use community.mysql collection |
mysql_user |
community.mysql.mysql_user |
Use community.mysql collection |
postgresql_db |
community.postgresql.postgresql_db |
Use community.postgresql collection |
postgresql_user |
community.postgresql.postgresql_user |
Use community.postgresql collection |
mongodb_* |
community.mongodb.* |
Use community.mongodb collection |
Monitoring and Logging
| Deprecated Module | Replacement | Notes |
|---|---|---|
nagios |
community.general.nagios |
Use community.general collection |
zabbix_* |
community.zabbix.* |
Use community.zabbix collection |
FQCN Migration
Why Use Fully Qualified Collection Names (FQCN)?
- Clarity: Explicitly shows which collection provides the module
- Conflict Prevention: Avoids naming conflicts between collections
- Future-Proofing: Prevents breakage when modules move between collections
- Best Practice: Recommended by Ansible for all new playbooks
Migration Examples
# Old style (deprecated)
- name: Install nginx
apt:
name: nginx
state: present
# New style (recommended)
- name: Install nginx
ansible.builtin.apt:
name: nginx
state: present
# Old style (deprecated)
- name: Configure firewall
ufw:
rule: allow
port: '443'
# New style (recommended)
- name: Configure firewall
community.general.ufw:
rule: allow
port: '443'
Installing Required Collections
When migrating to FQCN modules, ensure the required collections are installed:
# Install common collections
ansible-galaxy collection install ansible.posix
ansible-galaxy collection install community.general
ansible-galaxy collection install community.docker
ansible-galaxy collection install community.mysql
ansible-galaxy collection install community.postgresql
ansible-galaxy collection install amazon.aws
ansible-galaxy collection install azure.azcollection
ansible-galaxy collection install google.cloud
Or create a requirements.yml:
---
collections:
- name: ansible.posix
version: ">=1.5.0"
- name: community.general
version: ">=6.0.0"
- name: community.docker
version: ">=3.0.0"
- name: community.mysql
version: ">=3.0.0"
- name: community.postgresql
version: ">=2.0.0"
Then install with:
ansible-galaxy collection install -r requirements.yml
Checking for Deprecated Modules
Use ansible-lint to identify deprecated modules in your playbooks:
# Check for deprecated module usage
ansible-lint --profile production playbook.yml
# Show rule documentation for deprecated modules
ansible-lint -L | grep deprecated
Version Compatibility Notes
- Ansible 2.9: Last version with many modules in ansible.builtin
- Ansible 2.10+: Collections separated from core
- Ansible 2.12+: Many deprecated modules removed from core
- Ansible 2.14+: FQCN strongly recommended for all modules
Resources
Ansible Security Checklist
Overview
This checklist provides comprehensive security validation guidelines for Ansible playbooks, roles, and collections. Use this as a reference when reviewing Ansible code for security vulnerabilities.
Secrets Management
❌ Bad Practices
# Hardcoded passwords
- name: Create user
user:
name: admin
password: "P@ssw0rd123" # NEVER DO THIS
# Hardcoded API keys
- name: Configure API
template:
src: config.j2
dest: /etc/app/config.yml
vars:
api_key: "sk-1234567890abcdef" # NEVER DO THIS
# Credentials in variables
vars:
db_password: "secret123" # NEVER DO THIS
aws_secret_key: "AKIAIOSFODNN7EXAMPLE" # NEVER DO THIS
✅ Good Practices
# Use Ansible Vault for sensitive data
- name: Create user
user:
name: admin
password: "{{ admin_password | password_hash('sha512') }}"
no_log: true
# Load vaulted variables
- name: Include vaulted vars
include_vars:
file: secrets.yml # This file is encrypted with ansible-vault
# Use environment variables
- name: Configure API
template:
src: config.j2
dest: /etc/app/config.yml
environment:
API_KEY: "{{ lookup('env', 'API_KEY') }}"
no_log: true
# Use external secret management
- name: Fetch secret from HashiCorp Vault
set_fact:
db_password: "{{ lookup('hashi_vault', 'secret=secret/data/db:password') }}"
no_log: true
Best Practices
-
Always use Ansible Vault for sensitive data
ansible-vault create secrets.yml ansible-vault encrypt existing_file.yml -
Never commit unencrypted secrets to version control
-
Use
no_log: truefor tasks handling sensitive data- name: Set database password set_fact: db_password: "{{ vault_db_password }}" no_log: true -
Rotate secrets regularly and use version control for vault IDs
-
Use different vault passwords for different environments
Privilege Escalation
❌ Bad Practices
# Running entire playbook as root unnecessarily
- hosts: all
become: yes
become_user: root
tasks:
- name: Check application status
command: systemctl status myapp
- name: Read configuration
slurp:
src: /etc/myapp/config.yml
# No privilege escalation when needed
- name: Install package
apt:
name: nginx
state: present
# This will fail without become
✅ Good Practices
# Only use become when necessary
- hosts: all
tasks:
- name: Check application status
command: systemctl status myapp
# No become needed for read-only systemctl
- name: Install package
apt:
name: nginx
state: present
become: yes
# Only escalate for this task
- name: Configure application
template:
src: config.j2
dest: /etc/myapp/config.yml
owner: myapp
group: myapp
mode: '0640'
become: yes
Best Practices
- Principle of least privilege - only escalate when necessary
- Use specific become_user instead of always root
- Limit sudo access to specific commands in sudoers
- Audit all become usage in playbooks
- Use become_flags carefully and document why
File Permissions
❌ Bad Practices
# World-readable sensitive files
- name: Create SSH key
copy:
src: id_rsa
dest: /home/user/.ssh/id_rsa
mode: '0644' # WRONG: Private key readable by all
# No mode specified
- name: Create config file
template:
src: database.conf.j2
dest: /etc/app/database.conf
# Missing mode - depends on umask
# Overly permissive
- name: Create script
copy:
src: deploy.sh
dest: /usr/local/bin/deploy.sh
mode: '0777' # WRONG: World writable
✅ Good Practices
# Appropriate permissions for private keys
- name: Create SSH key
copy:
src: id_rsa
dest: /home/user/.ssh/id_rsa
owner: user
group: user
mode: '0600'
# Explicit permissions for config files
- name: Create config file
template:
src: database.conf.j2
dest: /etc/app/database.conf
owner: appuser
group: appgroup
mode: '0640'
# Minimal necessary permissions
- name: Create script
copy:
src: deploy.sh
dest: /usr/local/bin/deploy.sh
owner: root
group: root
mode: '0755'
# Set directory permissions properly
- name: Create secure directory
file:
path: /etc/app/secrets
state: directory
owner: appuser
group: appgroup
mode: '0750'
Permission Guidelines
| File Type | Recommended Mode | Owner | Group |
|---|---|---|---|
| Private keys | 0600 | user | user |
| Public keys | 0644 | user | user |
| Config files (sensitive) | 0640 | app | app |
| Config files (public) | 0644 | app | app |
| Executables | 0755 | root | root |
| Directories (sensitive) | 0750 | app | app |
| Directories (public) | 0755 | app | app |
| Log files | 0640 | app | app |
Command Injection Prevention
❌ Bad Practices
# Unvalidated user input in commands
- name: Process user file
shell: "cat {{ user_provided_filename }}"
# VULNERABLE: User could provide "; rm -rf /"
# Direct variable interpolation
- name: Search logs
command: "grep {{ search_term }} /var/log/app.log"
# VULNERABLE: User could inject commands
# Using shell when not needed
- name: Create directory
shell: "mkdir -p {{ directory_name }}"
# RISKY: Use file module instead
✅ Good Practices
# Use quote filter for variables in shell
- name: Process user file
shell: "cat {{ user_provided_filename | quote }}"
when: user_provided_filename is match('^[a-zA-Z0-9._-]+$')
# Better: Use modules instead of shell/command
- name: Create directory
file:
path: "{{ directory_name }}"
state: directory
mode: '0755'
# Validate input before use
- name: Search logs
command: "grep {{ search_term }} /var/log/app.log"
when:
- search_term is defined
- search_term | length > 0
- search_term is match('^[a-zA-Z0-9 ]+$')
args:
warn: false
# Use args for command parameters
- name: Run script with arguments
command: /usr/local/bin/script.sh
args:
stdin: "{{ user_input }}"
Best Practices
- Prefer modules over command/shell whenever possible
- Always use quote filter for variables in shell commands
- Validate input with regex patterns
- Use whitelist validation not blacklist
- Never trust user input without validation
Network Security
❌ Bad Practices
# Unencrypted protocols
- name: Download file
get_url:
url: http://example.com/file.tar.gz # WRONG: HTTP not HTTPS
dest: /tmp/file.tar.gz
# Disabled SSL verification
- name: Call API
uri:
url: https://api.example.com/data
validate_certs: no # WRONG: Disables security
# Exposing on all interfaces unnecessarily
- name: Configure service
template:
src: config.j2
dest: /etc/app/config.yml
vars:
bind_address: "0.0.0.0" # RISKY: Expose to all
✅ Good Practices
# Use HTTPS
- name: Download file
get_url:
url: https://example.com/file.tar.gz
dest: /tmp/file.tar.gz
checksum: sha256:abc123...
# Validate SSL certificates
- name: Call API
uri:
url: https://api.example.com/data
validate_certs: yes
client_cert: /path/to/cert.pem
client_key: /path/to/key.pem
# Bind to specific interface
- name: Configure service
template:
src: config.j2
dest: /etc/app/config.yml
vars:
bind_address: "127.0.0.1" # Localhost only
# Use firewall rules
- name: Configure firewall
ufw:
rule: allow
port: '443'
proto: tcp
src: '10.0.0.0/8' # Only from internal network
Best Practices
- Always use HTTPS for external communications
- Validate SSL certificates - only disable for testing
- Bind services to specific interfaces when possible
- Use firewall rules to restrict access
- Encrypt sensitive data in transit (TLS/SSL)
SELinux and AppArmor
Best Practices
# Don't disable SELinux
- name: Configure SELinux
selinux:
policy: targeted
state: enforcing # Not permissive or disabled
# Set proper SELinux contexts
- name: Set SELinux context for web content
sefcontext:
target: '/web/content(/.*)?'
setype: httpd_sys_content_t
state: present
- name: Apply SELinux context
command: restorecon -Rv /web/content
# Manage AppArmor profiles
- name: Load AppArmor profile
command: apparmor_parser -r /etc/apparmor.d/usr.bin.myapp
Audit and Logging
Best Practices
# Log security-relevant actions
- name: Create admin user
user:
name: admin
groups: sudo
state: present
register: admin_user_result
- name: Log user creation
lineinfile:
path: /var/log/ansible-changes.log
line: "{{ ansible_date_time.iso8601 }} - Admin user created by {{ ansible_user_id }}"
create: yes
when: admin_user_result.changed
# Use tags for security-related tasks
- name: Configure SSH
template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
tags:
- security
- ssh
Security Validation Checklist
Before running playbooks in production, verify:
- No hardcoded secrets (passwords, API keys, tokens)
- All sensitive data encrypted with Ansible Vault
-
no_log: trueused for tasks handling secrets - Privilege escalation only where necessary
- File permissions explicitly set (not relying on umask)
- Private keys have mode 0600
- No world-writable files or directories
- Input validation for user-provided variables
- Using modules instead of shell/command where possible
- Quote filter used for variables in shell commands
- HTTPS used instead of HTTP
- SSL certificate validation enabled
- Services bound to specific interfaces, not 0.0.0.0
- Firewall rules configured appropriately
- SELinux/AppArmor not disabled
- Security contexts set correctly
- Security-relevant actions logged
- Regular security updates applied
- Unused packages removed
- Default credentials changed
- Unnecessary services disabled
Tools for Security Scanning
-
ansible-lint - Includes security-focused rules
ansible-lint --profile security playbook.yml -
Ansible Galaxy Security Scan
ansible-galaxy collection scan namespace.collection -
Git-secrets - Prevent committing secrets
git secrets --scan -
Trivy - Scan for vulnerabilities
trivy config .