Ansible review criteria
Review criteria for Ansible playbooks and roles — the Zen of Ansible, YAML formatting, naming conventions, task structure, variables, conditionals, loops, handlers, error handling, idempotency, and role structure. Use when reviewing or refactoring Ansible code. Also loaded by the ansible-review agent as its knowledge base.From its SKILL.md
npx -y skills add CowDogMoo/squad-agents --skill ansible-review-criteriaAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 4 stars4 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
32.7 KB, as published. Nobody here has run it
Idiomatic Ansible Review Criteria
This document provides comprehensive criteria for reviewing Ansible code. It covers style conventions, structural patterns, security practices, and common anti-patterns.
Companion document: See ansible-standards.md for detailed coverage of Collections, Roles with argument_specs, and Molecule testing configuration.
Table of Contents
- The Zen of Ansible
- YAML Formatting
- Naming Conventions
- Task Structure
- Variables
- Conditionals
- Loops
- Handlers
- Error Handling
- Idempotency
- Role Structure
- Collections and FQCN
- Inventory Best Practices
- Security
- Performance
- Testing
- Common Anti-patterns
The Zen of Ansible
Core principles that guide idiomatic Ansible:
- Ansible is not Python — YAML is challenging for coding
- Playbooks are not for programming — most users are not programmers
- Clear is better than cluttered
- Concise is better than verbose
- Simple is better than complex
- Readability counts
- Practicality beats purity
Sources: Red Hat Good Practices, Ansible Best Practices Guide
YAML Formatting
Indentation
- Use 2 spaces for indentation (never tabs)
- Be consistent throughout all files
- Configure editor to enforce this
Boolean Values
# CORRECT: Use true/false
enabled: true
disabled: false
# WRONG: Avoid yes/no or 1/0
enabled: yes # Avoid
disabled: no # Avoid
Key-Value Spacing
# CORRECT: Single space after colon
name: Install package
# WRONG: Multiple spaces or no space
name:Install package
name: Install package
File Extensions
- All Ansible YAML files:
.yml(not.yaml,.YML) - All Jinja2 templates:
.j2
Blank Lines
- One blank line before
vars,pre_tasks,roles,tasks,handlers - One blank line between each task
Multi-line Strings
# Literal block scalar (|) - preserves newlines
description: |
This is a multi-line
description that keeps
each line separate.
# Folded block scalar (>) - folds newlines to spaces
description: >
This is a long description
that will be folded into
a single line.
# Block chomping indicators
content: |+ # Keep trailing newlines
content: |- # Strip trailing newlines
Sources: Ansible YAML Syntax, Jeff Geerling YAML Best Practices
Naming Conventions
Variables
# CORRECT: snake_case
http_port: 8080
database_host: localhost
max_connections: 100
# WRONG: camelCase, kebab-case, or spaces
httpPort: 8080 # Wrong
database-host: localhost # Wrong
Role Names
- Use lowercase with underscores (snake_case) and singular form
- Examples:
nginx_server,database_backup,user_management - Note: Some style guides recommend kebab-case, but snake_case is more common and aligns with variable naming conventions
Role Variables
- Prefix with role name to avoid collisions
- Use double underscore for internal variables
# Public role variables (in defaults/main.yml)
nginx_port: 80
nginx_worker_processes: auto
# Internal variables (not meant for users)
__nginx_config_path: /etc/nginx
__nginx_temp_dir: /tmp/nginx
Task Names
# CORRECT: Action verb + description, sentence case
- name: Install nginx package
- name: Configure SSH daemon
- name: Create application user
- name: Ensure nginx configuration directory exists
# WRONG: Missing name, ALL CAPS, or comments instead of names
- apt:
name: nginx # Missing name!
- name: INSTALL NGINX # Wrong: ALL CAPS
# Wrong: Don't use name as a comment
# Install nginx package
- apt:
name: nginx
Task naming rules:
- Always provide a
name:parameter — no exceptions - Start with an action verb (install, configure, create, remove, ensure)
- Use sentence case (capitalize first letter, lowercase rest except proper nouns)
- Keep names unique within a play
- Do not use templating in task names (e.g.,
name: "Install {{ package }}") - The
name[casing]ansible-lint rule enforces capitalization
Sources: Ansible Lint name rules, Ansible Junky Standards
Task Structure
Task Ordering
- name: Ensure nginx package is installed
ansible.builtin.package:
name: nginx
state: present
become: true
when: install_nginx | bool
notify: Restart nginx
tags:
- nginx
- packages
Recommended key order within a task:
name- Module name (FQCN)
- Module parameters
become/become_userwhenregisterchanged_when/failed_whennotifytagsloop/with_*
One Task Per Action
# CORRECT: Separate tasks for separate actions
- name: Install required packages
ansible.builtin.package:
name: "{{ item }}"
state: present
loop:
- nginx
- certbot
- name: Start nginx service
ansible.builtin.service:
name: nginx
state: started
# WRONG: Combining unrelated actions
- name: Install and configure nginx # Too broad
block:
# ... 20 tasks
Variables
Variable Precedence (Lowest to Highest)
- Role defaults (
roles/x/defaults/main.yml) - Inventory file or script group vars
group_vars/allgroup_vars/group_name- Inventory file or script host vars
host_vars/hostname- Host facts / cached set_facts
- Play vars
- Play vars_prompt
- Play vars_files
- Role vars (
roles/x/vars/main.yml) - Block vars
- Task vars
- include_vars
- set_facts / registered vars
- Role params
- Include params
- Extra vars (
-e)
Best Practices
# Set defaults in role defaults
# roles/myapp/defaults/main.yml
myapp_port: 8080
myapp_user: myapp
myapp_log_level: info
# Override in group_vars for specific environments
# group_vars/production.yml
myapp_log_level: warn
# Use default filter for optional variables
port: "{{ custom_port | default(8080) }}"
# Use mandatory filter for required variables
database_password: "{{ db_pass | mandatory }}"
Avoid Hardcoding
# WRONG: Hardcoded values
- name: Create user
ansible.builtin.user:
name: john
uid: 1001
# CORRECT: Use variables
- name: Create application user
ansible.builtin.user:
name: "{{ app_user }}"
uid: "{{ app_user_uid }}"
Sources: Ansible Variables Documentation, Spacelift Ansible Variables Guide
Conditionals
Boolean Handling
# For string booleans, use | bool filter
- name: Enable feature
ansible.builtin.debug:
msg: Feature enabled
when: enable_feature | bool
# Direct boolean variables don't need filter
- name: Skip if disabled
ansible.builtin.debug:
msg: Running
when: is_enabled # Already a boolean
Syntax
# CORRECT: No {{ }} in when clauses
when: ansible_os_family == "Debian"
# WRONG: Using {{ }} in when
when: "{{ ansible_os_family == 'Debian' }}" # Wrong!
Multiple Conditions
# Using 'and' (both must be true)
when:
- ansible_os_family == "RedHat"
- ansible_distribution_major_version | int >= 8
# Using 'or'
when: ansible_os_family == "Debian" or ansible_os_family == "Ubuntu"
# Complex logic
when: >
(ansible_os_family == "RedHat" and ansible_distribution_major_version | int >= 8)
or
(ansible_os_family == "Debian" and ansible_distribution_major_version | int >= 10)
Testing Variables
# Check if defined
when: my_var is defined
# Check if undefined
when: my_var is not defined
# Check if empty
when: my_var | length > 0
# Check task results
when: previous_task is succeeded
when: previous_task is failed
when: previous_task is skipped
when: previous_task is changed
Sources: Ansible Conditionals
Loops
Prefer loop Over with_items
# CORRECT: Modern loop syntax
- name: Install packages
ansible.builtin.package:
name: "{{ item }}"
state: present
loop:
- nginx
- certbot
- python3
# LEGACY: with_items (still works but prefer loop)
- name: Install packages
ansible.builtin.package:
name: "{{ item }}"
state: present
with_items:
- nginx
- certbot
Loop Control
- name: Create users
ansible.builtin.user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
loop: "{{ users }}"
loop_control:
label: "{{ item.name }}" # Show only name in output
pause: 2 # Pause between iterations
index_var: idx # Access loop index
Flattening Behavior
# with_items flattens single level automatically
# loop does NOT flatten - add | flatten(1) if needed
- name: Install packages
ansible.builtin.package:
name: "{{ item }}"
loop: "{{ package_lists | flatten(1) }}"
Dictionary Loops
- name: Create users with attributes
ansible.builtin.user:
name: "{{ item.key }}"
comment: "{{ item.value.comment }}"
loop: "{{ users | dict2items }}"
Sources: Ansible Loops
Handlers
Basic Handler Pattern
# In tasks
- name: Update nginx configuration
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: Restart nginx
# In handlers/main.yml
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restarted
become: true
Handler Best Practices
- Unique names — handlers must have globally unique names
- Order matters — handlers run in definition order, not notification order
- Single execution — handlers run once regardless of how many times notified
Using listen for Handler Groups
# Multiple handlers can listen to the same topic
handlers:
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restarted
listen: web server changed
- name: Clear nginx cache
ansible.builtin.file:
path: /var/cache/nginx
state: absent
listen: web server changed
# Task notifies the topic
- name: Update config
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: web server changed
Flushing Handlers
# Force handlers to run immediately
- name: Update critical config
ansible.builtin.template:
src: app.conf.j2
dest: /etc/app/app.conf
notify: Restart application
- name: Flush handlers
ansible.builtin.meta: flush_handlers
- name: Run health check
ansible.builtin.uri:
url: http://localhost:8080/health
Sources: Ansible Handlers
Error Handling
Block / Rescue / Always
- name: Deploy application
block:
- name: Download application
ansible.builtin.get_url:
url: "{{ app_url }}"
dest: /tmp/app.tar.gz
- name: Extract application
ansible.builtin.unarchive:
src: /tmp/app.tar.gz
dest: /opt/app
remote_src: true
rescue:
- name: Log deployment failure
ansible.builtin.debug:
msg: "Deployment failed: {{ ansible_failed_task.name }}"
- name: Rollback to previous version
ansible.builtin.copy:
src: /opt/app.backup
dest: /opt/app
remote_src: true
always:
- name: Clean up temp files
ansible.builtin.file:
path: /tmp/app.tar.gz
state: absent
- name: Send notification
ansible.builtin.uri:
url: "{{ webhook_url }}"
method: POST
body: '{"status": "completed"}'
failed_when and changed_when
- name: Check if service is running
ansible.builtin.command: systemctl is-active nginx
register: nginx_status
changed_when: false
failed_when: nginx_status.rc not in [0, 3]
- name: Run migration
ansible.builtin.command: ./migrate.sh
register: migration_result
changed_when: "'Applied' in migration_result.stdout"
failed_when: migration_result.rc != 0 and 'already up to date' not in migration_result.stdout
ignore_errors (Use Sparingly)
# Only use when failure is expected and handled
- name: Check optional service
ansible.builtin.command: systemctl status optional-service
register: optional_check
ignore_errors: true
- name: Configure if available
ansible.builtin.template:
src: optional.conf.j2
dest: /etc/optional/config
when: optional_check is succeeded
Sources: Ansible Error Handling, Ansible Blocks
Idempotency
Core Principle
Running a playbook multiple times should produce the same result. The first run makes changes; subsequent runs report "ok" with no changes.
Command/Shell Module Idempotency
# WRONG: Always reports changed
- name: Create directory
ansible.builtin.command: mkdir -p /opt/myapp
# CORRECT: Use appropriate module
- name: Create directory
ansible.builtin.file:
path: /opt/myapp
state: directory
# If command/shell is necessary, use creates/removes
- name: Initialize database
ansible.builtin.command: /opt/db/init.sh
args:
creates: /opt/db/.initialized
# Or use check-then-act pattern
- name: Check current hostname
ansible.builtin.command: hostname
register: current_hostname
changed_when: false
- name: Set hostname
ansible.builtin.command: hostnamectl set-hostname "{{ target_hostname }}"
when: current_hostname.stdout != target_hostname
Idempotent Patterns
# Package management - already idempotent
- name: Ensure nginx is installed
ansible.builtin.package:
name: nginx
state: present # Not 'latest' unless you want updates
# File operations - use state
- name: Ensure config exists
ansible.builtin.copy:
src: app.conf
dest: /etc/app/app.conf
# Service management
- name: Ensure service is running
ansible.builtin.service:
name: nginx
state: started
enabled: true
Sources: Ansible Idempotency
Role Structure
Standard Directory Layout
roles/
└── my_role/
├── defaults/
│ └── main.yml # Default variables (lowest precedence)
├── files/
│ └── app.conf # Static files for copy module
├── handlers/
│ └── main.yml # Handler definitions
├── meta/
│ └── main.yml # Role metadata, dependencies
├── tasks/
│ └── main.yml # Main task list
├── templates/
│ └── config.j2 # Jinja2 templates
├── tests/
│ ├── inventory
│ └── test.yml # Test playbook
├── vars/
│ └── main.yml # Role variables (high precedence)
└── README.md # Documentation
Role Design Principles
- Single responsibility — one role, one purpose
- Self-contained — include all necessary tasks, templates, files
- Configurable — expose variables in defaults/
- Documented — README with examples
Role Dependencies (meta/main.yml)
---
galaxy_info:
author: Your Name
description: Role description
license: MIT
min_ansible_version: "2.10"
platforms:
- name: Ubuntu
versions:
- focal
- jammy
- name: EL
versions:
- "8"
- "9"
dependencies:
- role: common
- role: firewall
vars:
firewall_allowed_ports:
- 80
- 443
Galaxy Versioning
- Use strict X.Y.Z format (e.g., 1.0.0, 2.1.3)
- Follow semantic versioning
Sources: Ansible Roles, Galaxy Developer Guide
Collections and FQCN
Always Use Fully Qualified Collection Names
# CORRECT: FQCN
- name: Install package
ansible.builtin.package:
name: nginx
state: present
- name: Gather AWS EC2 facts
amazon.aws.ec2_instance_info:
region: us-east-1
# WRONG: Short module names (deprecated)
- name: Install package
package: # Ambiguous!
name: nginx
# WRONG: Using collections keyword
collections:
- amazon.aws # Avoid this pattern
Common Collection Namespaces
ansible.builtin— core Ansible modulesansible.posix— POSIX modules (acl, cron, etc.)ansible.netcommon— network common modulescommunity.general— community maintained modulescommunity.mysql— MySQL modulescommunity.postgresql— PostgreSQL modulesamazon.aws— AWS modulesgoogle.cloud— GCP modulesazure.azcollection— Azure modules
Installing Collections
# requirements.yml
---
collections:
- name: community.general
version: ">=6.0.0"
- name: amazon.aws
version: ">=5.0.0"
# Install: ansible-galaxy collection install -r requirements.yml
Sources: Ansible FQCN Lint Rule, Using Collections
Inventory Best Practices
Directory Structure
inventory/
├── production/
│ ├── hosts.yml
│ ├── group_vars/
│ │ ├── all.yml
│ │ ├── webservers.yml
│ │ └── databases.yml
│ └── host_vars/
│ └── web01.example.com.yml
└── staging/
├── hosts.yml
├── group_vars/
│ └── all.yml
└── host_vars/
YAML Inventory Format
# inventory/production/hosts.yml
---
all:
children:
webservers:
hosts:
web01.example.com:
web02.example.com:
vars:
http_port: 80
databases:
hosts:
db01.example.com:
mysql_port: 3306
Variable Organization
# group_vars/all.yml - Common defaults
---
ntp_servers:
- 0.pool.ntp.org
- 1.pool.ntp.org
# group_vars/webservers.yml - Web server specific
---
nginx_worker_processes: auto
nginx_worker_connections: 1024
# host_vars/web01.example.com.yml - Host specific
---
nginx_worker_processes: 4 # Override for this host
Best Practices
- Use version control for inventory
- Separate environments (production, staging, development)
- Use dynamic inventory for cloud providers
- Set common defaults in
group_vars/all.yml - Host vars override group vars
Sources: Ansible Inventory Guide
Security
Ansible Vault for Secrets
# Create encrypted file
ansible-vault create secrets.yml
# Encrypt existing file
ansible-vault encrypt vars/credentials.yml
# Edit encrypted file
ansible-vault edit secrets.yml
# Run playbook with vault password
ansible-playbook site.yml --ask-vault-pass
ansible-playbook site.yml --vault-password-file ~/.vault_pass
no_log for Sensitive Tasks
- name: Set database password
ansible.builtin.mysql_user:
name: app_user
password: "{{ db_password }}"
priv: "app_db.*:ALL"
no_log: true
Privilege Escalation (become)
# At play level
- hosts: webservers
become: true
become_user: root
tasks:
- name: Install package
ansible.builtin.package:
name: nginx
# At task level (preferred for minimal privilege)
- name: Read user file
ansible.builtin.slurp:
src: /home/app/config
become: true
become_user: app
Security Best Practices
- Never commit secrets — use Vault
- Minimal become — only escalate when necessary
- Avoid shell/command — prefer idempotent modules
- Validate input — especially from external sources
- Secure file permissions — use mode parameter
- name: Create sensitive config
ansible.builtin.template:
src: database.conf.j2
dest: /etc/app/database.conf
owner: app
group: app
mode: "0600" # Only owner can read
Sources: Ansible Vault, Privilege Escalation
Performance
Fact Gathering
# Disable if not needed
- hosts: webservers
gather_facts: false
tasks:
- name: Quick task
ansible.builtin.ping:
# Gather only what you need
- hosts: webservers
gather_subset:
- network
- virtual
Fact Caching
# ansible.cfg
[defaults]
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 86400
SSH Pipelining
# ansible.cfg
[ssh_connection]
pipelining = True
Parallelism
# ansible.cfg
[defaults]
forks = 20 # Default is 5
Strategy
# Free strategy - tasks run as fast as possible
- hosts: webservers
strategy: free
tasks:
- name: Independent task
ansible.builtin.package:
name: nginx
Sources: Ansible Tuning, Spacelift Best Practices
Testing
Ansible Lint
# Install
pip install ansible-lint
# Run on playbook
ansible-lint playbook.yml
# Run on role
ansible-lint roles/my_role/
Lint Configuration (.ansible-lint)
---
profile: production
skip_list:
- yaml[line-length] # With comment explaining why
warn_list:
- experimental
exclude_paths:
- .cache/
- .github/
Molecule for Role Testing
# Install
pip install molecule molecule-docker
# Initialize role with Molecule
molecule init role my_role
# Run tests
molecule test
# Develop interactively
molecule converge
molecule verify
molecule destroy
Check Mode (Dry Run)
# Preview changes without applying
ansible-playbook site.yml --check
# With diff to see changes
ansible-playbook site.yml --check --diff
Assert Module for Validation
- name: Validate configuration
ansible.builtin.assert:
that:
- app_port > 1024
- app_user is defined
- app_user | length > 0
fail_msg: "Invalid configuration: port must be > 1024 and user must be defined"
success_msg: "Configuration validated successfully"
quiet: true # Only show on failure
Sources: Ansible Lint, Molecule Documentation
Common Anti-patterns
1. Using shell/command When a Module Exists
# WRONG
- name: Install package
ansible.builtin.shell: apt-get install nginx -y
# CORRECT
- name: Install package
ansible.builtin.apt:
name: nginx
state: present
2. Not Using FQCN
# WRONG
- name: Copy file
copy:
src: file.txt
dest: /tmp/file.txt
# CORRECT
- name: Copy file
ansible.builtin.copy:
src: file.txt
dest: /tmp/file.txt
3. Hardcoding Values
# WRONG
- name: Create user
ansible.builtin.user:
name: admin
uid: 1000
# CORRECT
- name: Create application user
ansible.builtin.user:
name: "{{ app_user }}"
uid: "{{ app_user_uid }}"
4. Missing Task Names
# WRONG
- ansible.builtin.package:
name: nginx
state: present
# CORRECT
- name: Install nginx web server
ansible.builtin.package:
name: nginx
state: present
5. Using import_tasks with Loops
# WRONG - import_tasks doesn't support loops
- import_tasks: user.yml
loop: "{{ users }}"
# CORRECT - use include_tasks for loops
- include_tasks: user.yml
loop: "{{ users }}"
6. Storing Secrets in Plain Text
# WRONG
vars:
database_password: "supersecret123"
# CORRECT - Use Vault
vars_files:
- vault/secrets.yml # Encrypted with ansible-vault
7. Ignoring Errors Without Handling
# WRONG - Silent failure
- name: Run script
ansible.builtin.command: /opt/script.sh
ignore_errors: true
# CORRECT - Genuinely handle the failure (block/rescue)
- block:
- name: Run script
ansible.builtin.command: /opt/script.sh
rescue:
- name: Recover from script failure
ansible.builtin.fail:
msg: "Script failed: {{ ansible_failed_result.stderr | default('unknown') }}"
# ALSO CORRECT - failed_when with a real condition (specific rc allowed)
- name: Run script (rc 0 or 2 are acceptable)
ansible.builtin.command: /opt/script.sh
register: script_result
failed_when: script_result.rc not in [0, 2]
Note:
failed_when: falseis suppression, not handling — it hides every non-zero rc. Only use it when a non-zero rc is genuinely expected, and ALWAYS act on the result (inspectregisteroutput, thenfail/assertor branch).
8. Not Making Tasks Idempotent
# WRONG - Runs every time
- name: Add line to file
ansible.builtin.shell: echo "config=value" >> /etc/app.conf
# CORRECT - Idempotent
- name: Configure application
ansible.builtin.lineinfile:
path: /etc/app.conf
line: "config=value"
9. Overusing become: true
# WRONG - Play-level become when not all tasks need it
- hosts: webservers
become: true
tasks:
- name: Check free space # Doesn't need root
ansible.builtin.command: df -h
- name: Install package # Needs root
ansible.builtin.package:
name: nginx
# CORRECT - Task-level become
- hosts: webservers
tasks:
- name: Check free space
ansible.builtin.command: df -h
changed_when: false
- name: Install package
ansible.builtin.package:
name: nginx
become: true
10. Not Using Handlers for Restarts
# WRONG - Always restarts
- name: Update config
ansible.builtin.template:
src: app.conf.j2
dest: /etc/app/app.conf
- name: Restart service
ansible.builtin.service:
name: app
state: restarted
# CORRECT - Only restarts on change
- name: Update config
ansible.builtin.template:
src: app.conf.j2
dest: /etc/app/app.conf
notify: Restart app
handlers:
- name: Restart app
ansible.builtin.service:
name: app
state: restarted
Sources: Ansible Lint Rules
Tags Best Practices
Special Tags
always— runs unless explicitly skipped with--skip-tags alwaysnever— skipped unless explicitly requested with--tags never
- name: Always run health check
ansible.builtin.uri:
url: http://localhost/health
tags:
- always
- health
- name: Debug task (only when requested)
ansible.builtin.debug:
var: all_vars
tags:
- never
- debug
Tag Best Practices
- Always add a specific named tag alongside
alwaysornever - Use consistent tag names across playbooks
- Document tag meanings in README
# Run only specific tags
ansible-playbook site.yml --tags "deploy,config"
# Skip specific tags
ansible-playbook site.yml --skip-tags "debug"
# List available tags
ansible-playbook site.yml --list-tags
Sources: Ansible Tags
Register and Return Values
Common Return Values
- name: Run command
ansible.builtin.command: whoami
register: result
# Access return values
- name: Show results
ansible.builtin.debug:
msg: |
stdout: {{ result.stdout }}
stderr: {{ result.stderr }}
rc: {{ result.rc }}
changed: {{ result.changed }}
stdout_lines: {{ result.stdout_lines }}
Best Practices
# Use changed_when with register for commands
- name: Check status
ansible.builtin.command: systemctl is-active nginx
register: nginx_status
changed_when: false
failed_when: nginx_status.rc not in [0, 3]
# Use stdout_lines for multi-line output
- name: List files
ansible.builtin.command: ls /opt
register: files_list
changed_when: false
- name: Process each file
ansible.builtin.debug:
msg: "Found: {{ item }}"
loop: "{{ files_list.stdout_lines }}"
Sources: Ansible Return Values
Jinja2 Templates
Template Best Practices
{# templates/app.conf.j2 #}
# Managed by Ansible - DO NOT EDIT
# Generated: {{ ansible_date_time.iso8601 }}
# Host: {{ inventory_hostname }}
[application]
port = {{ app_port | default(8080) }}
host = {{ app_host | mandatory }}
debug = {{ app_debug | default(false) | lower }}
{% if app_features is defined %}
[features]
{% for feature in app_features %}
{{ feature.name }} = {{ feature.enabled | lower }}
{% endfor %}
{% endif %}
Useful Filters
# Default values
"{{ variable | default('fallback') }}"
# Type conversion
"{{ string_number | int }}"
"{{ value | bool }}"
# String operations
"{{ name | upper }}"
"{{ name | lower }}"
"{{ name | capitalize }}"
"{{ list | join(', ') }}"
# List operations
"{{ list | first }}"
"{{ list | last }}"
"{{ list | unique }}"
"{{ list | sort }}"
# JSON/YAML
"{{ dict | to_json }}"
"{{ dict | to_nice_yaml }}"
# Path operations
"{{ path | basename }}"
"{{ path | dirname }}"
# Password hashing
"{{ password | password_hash('sha512') }}"
Sources: Ansible Templating, Ansible Filters
include_tasks vs import_tasks
Key Differences
| Feature | import_tasks | include_tasks |
|---|---|---|
| Processing | Static (parse time) | Dynamic (runtime) |
| Loops | Not supported | Supported |
| Conditionals | Applied to each task | Applied to include itself |
| Tags | Inherited by imported tasks | Not inherited |
--list-tasks | Shows tasks | Doesn't show tasks |
When to Use Each
# Use import_tasks for static includes
- import_tasks: common.yml
# Use include_tasks for dynamic/looped includes
- include_tasks: user.yml
loop: "{{ users }}"
loop_control:
loop_var: user
# Use include_tasks with conditionals
- include_tasks: "{{ ansible_os_family | lower }}.yml"
General Recommendation
Prefer include_tasks unless you specifically need static parsing behavior.
The limitations of import_tasks (no loops, no dynamic file names) are more
impactful than the minor overhead of dynamic includes.
Sources: Ansible Include vs Import
References
Companion Document
See also: ansible-standards.md for detailed coverage of:
- Collection structure and
galaxy.ymlrequirements - Role
argument_specs.ymlfor input validation - Molecule test configuration and multi-platform testing
.yamllintconfiguration
Official Documentation
Best Practices Guides
Style Guides
Testing
Last updated: 2026-02-06