All skills
Skillintermediate
Ansible Best Practices
```yaml # Good - idempotent - name: Create directory ansible.builtin.file: path: /opt/app state: directory mode: '0755'
Claude Code Knowledge Pack7/10/2026
Overview
Ansible Best Practices
Directory Structure
Standard Playbook Structure
playbook.yml
roles/
common/
tasks/
main.yml
handlers/
main.yml
templates/
files/
vars/
main.yml
defaults/
main.yml
meta/
main.yml
inventory/
production/
hosts
group_vars/
host_vars/
staging/
hosts
group_vars/
host_vars/
Role Structure
Each role should have:
tasks/main.yml- Main task listhandlers/main.yml- Handlers triggered by taskstemplates/- Jinja2 templatesfiles/- Static files to copyvars/main.yml- Role-specific variables (high priority)defaults/main.yml- Default variables (low priority, overridable)meta/main.yml- Role dependencies and metadata
Naming Conventions
Files and Directories
- Use lowercase with underscores:
install_nginx.yml,backup_database.yml - Playbook files: descriptive names ending in
.yml - Role names: short, descriptive, lowercase with underscores
Variables
- Use descriptive names:
nginx_port,db_backup_dir,app_version - Prefix role-specific variables with role name:
nginx_worker_processes - Use snake_case, not camelCase or kebab-case
- Group related variables with common prefixes
Tasks
- Use descriptive names that explain what the task does
- Start with a verb: "Install nginx", "Copy configuration file", "Start service"
Task Writing Best Practices
Always Use State Declaration
# Good
- name: Ensure nginx is installed
ansible.builtin.package:
name: nginx
state: present
# Bad
- name: Install nginx
ansible.builtin.package:
name: nginx
Use Fully Qualified Collection Names (FQCN)
# Good - FQCN (Ansible 2.10+)
- name: Copy configuration file
ansible.builtin.copy:
src: nginx.conf
dest: /etc/nginx/nginx.conf
# Avoid - Short names (deprecated)
- name: Copy configuration file
copy:
src: nginx.conf
dest: /etc/nginx/nginx.conf
Idempotency
- All tasks should be idempotent (safe to run multiple times)
- Use
state: present/absentinstead of imperative commands - Avoid using
commandorshellmodules when builtin modules exist - When using
command/shell, usecreates,removes, orchanged_when
# Good - idempotent
- name: Create directory
ansible.builtin.file:
path: /opt/app
state: directory
mode: '0755'
# Bad - not idempotent
- name: Create directory
ansible.builtin.command: mkdir -p /opt/app
Error Handling
- name: Attempt to start service
ansible.builtin.service:
name: myapp
state: started
register: service_result
failed_when: false
changed_when: service_result.rc == 0
- name: Handle service failure
ansible.builtin.debug:
msg: "Service failed to start: {{ service_result.msg }}"
when: service_result.failed
Variables and Facts
Variable Precedence (High to Low)
- Extra vars (
-ein CLI) - Task vars
- Block vars
- Role and include vars
- Set_facts / registered vars
- Play vars
- Play vars_files
- Role defaults
- Inventory vars (host_vars, group_vars)
Using Variables
# Use default values
- name: Set port with default
ansible.builtin.set_fact:
app_port: "{{ custom_port | default(8080) }}"
# Combine variables
- name: Create full path
ansible.builtin.set_fact:
config_path: "{{ base_dir }}/{{ app_name }}/config.yml"
Conditionals and Loops
When Statements
- name: Install on Debian-based systems
ansible.builtin.apt:
name: nginx
state: present
when: ansible_os_family == "Debian"
- name: Install on RedHat-based systems (RHEL 8+)
ansible.builtin.dnf:
name: nginx
state: present
when: ansible_os_family == "RedHat"
Loops
# Good - using loop
- name: Install packages
ansible.builtin.package:
name: "{{ item }}"
state: present
loop:
- nginx
- postgresql
- redis
# Complex loop with dict
- name: Create users
ansible.builtin.user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
state: present
loop:
- { name: 'alice', groups: 'admin,developers' }
- { name: 'bob', groups: 'developers' }
Handlers
Naming and Usage
# In tasks/main.yml
- name: Copy nginx configuration
ansible.builtin.copy:
src: nginx.conf
dest: /etc/nginx/nginx.conf
notify: Restart nginx
# In handlers/main.yml
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restarted
Handler Best Practices
- Handlers run once at the end of a play
- Use descriptive names
- Listen to multiple notifications with same handler name
- Use
meta: flush_handlersto run handlers immediately if needed
Templates
Jinja2 Templates
# Task
- name: Deploy configuration from template
ansible.builtin.template:
src: app_config.j2
dest: /etc/app/config.yml
mode: '0644'
backup: true
# Template file: templates/app_config.j2
server:
port: {{ app_port }}
host: {{ ansible_default_ipv4.address }}
database:
host: {{ db_host }}
port: {{ db_port | default(5432) }}
name: {{ db_name }}
{% if enable_ssl %}
ssl:
enabled: true
cert: {{ ssl_cert_path }}
key: {{ ssl_key_path }}
{% endif %}
Advanced Jinja2 Templating
Common Filters
Data Format Conversion
- name: Convert to JSON
ansible.builtin.copy:
content: "{{ my_dict | to_json }}"
dest: /tmp/config.json
- name: Convert to YAML
ansible.builtin.copy:
content: "{{ my_dict | to_yaml }}"
dest: /tmp/config.yml
- name: Convert to pretty JSON
ansible.builtin.copy:
content: "{{ my_dict | to_nice_json }}"
dest: /tmp/config.json
# Parse JSON/YAML strings
- name: Parse JSON string
ansible.builtin.set_fact:
parsed_data: "{{ json_string | from_json }}"
- name: Parse YAML string
ansible.builtin.set_fact:
parsed_data: "{{ yaml_string | from_yaml }}"
String Manipulation
# Regex operations
- name: Replace text
ansible.builtin.set_fact:
new_string: "{{ original | regex_replace('^old', 'new') }}"
- name: Extract with regex
ansible.builtin.set_fact:
extracted: "{{ text | regex_search('version: (\\\\d+\\\\.\\\\d+)', '\\\\1') }}"
# Case conversion
- name: Convert case
ansible.builtin.set_fact:
upper: "{{ text | upper }}"
lower: "{{ text | lower }}"
title: "{{ text | title }}"
# String operations
- name: String operations
ansible.builtin.set_fact:
trimmed: "{{ ' text ' | trim }}"
replaced: "{{ text | replace('old', 'new') }}"
split_list: "{{ 'a,b,c' | split(',') }}"
joined: "{{ ['a', 'b', 'c'] | join('-') }}"
Hashing and Encoding
# Hash values
- name: Generate hashes
ansible.builtin.set_fact:
md5_hash: "{{ 'mystring' | hash('md5') }}"
sha256_hash: "{{ 'mystring' | hash('sha256') }}"
# Password hashing
- name: Hash password
ansible.builtin.user:
name: myuser
password: "{{ user_password | password_hash('sha512', 'mysecretsalt') }}"
# Encoding
- name: Encode/decode
ansible.builtin.set_fact:
base64_encoded: "{{ 'text' | b64encode }}"
base64_decoded: "{{ encoded_value | b64decode }}"
url_encoded: "{{ url_string | urlencode }}"
List and Dict Operations
# List operations
- name: List operations
ansible.builtin.set_fact:
unique_items: "{{ my_list | unique }}"
sorted_items: "{{ my_list | sort }}"
first_item: "{{ my_list | first }}"
last_item: "{{ my_list | last }}"
list_length: "{{ my_list | length }}"
flattened: "{{ nested_list | flatten }}"
# Dict operations
- name: Dict operations
ansible.builtin.set_fact:
dict_keys: "{{ my_dict | dict2items }}"
dict_values: "{{ my_dict | list }}"
combined: "{{ dict1 | combine(dict2) }}"
# Extract values
- name: Extract from list of dicts
ansible.builtin.set_fact:
names: "{{ users | map(attribute='name') | list }}"
ids: "{{ items | map(attribute='id') | list }}"
Network Filters
# IP address operations (requires netaddr Python package)
- name: IP operations
ansible.builtin.set_fact:
is_valid: "{{ ip_address | ipaddr }}"
network: "{{ ip_address | ipaddr('network') }}"
netmask: "{{ ip_address | ipaddr('netmask') }}"
broadcast: "{{ ip_address | ipaddr('broadcast') }}"
host_ip: "{{ ip_address | ipaddr('address') }}"
# CIDR operations
- name: CIDR operations
ansible.builtin.set_fact:
hosts_in_network: "{{ '192.168.1.0/24' | ipaddr('size') }}"
first_host: "{{ '192.168.1.0/24' | ipaddr('1') | ipaddr('address') }}"
File and Math Filters
# File size formatting
- name: Format file size
ansible.builtin.debug:
msg: "File size: {{ file_stat.stat.size | filesizeformat }}"
# Math operations
- name: Math operations
ansible.builtin.set_fact:
sum: "{{ [1, 2, 3] | sum }}"
min: "{{ [5, 2, 8] | min }}"
max: "{{ [5, 2, 8] | max }}"
rounded: "{{ 3.14159 | round(2) }}"
absolute: "{{ -42 | abs }}"
Default and Mandatory Values
# Provide defaults
- name: Use default values
ansible.builtin.set_fact:
port: "{{ custom_port | default(8080) }}"
config: "{{ app_config | default({}) }}"
# Nested defaults (Ansible 2.8+)
- name: Nested default
ansible.builtin.set_fact:
value: "{{ foo.bar.baz | default('fallback') }}"
# Mandatory values
- name: Require variable
ansible.builtin.set_fact:
required_value: "{{ must_be_defined | mandatory }}"
Lookup Plugins
File and Environment Lookups
# Read file content
- name: Read SSH public key
ansible.builtin.authorized_key:
user: deploy
key: "{{ lookup('file', '/home/user/.ssh/id_rsa.pub') }}"
# Environment variables
- name: Get environment variable
ansible.builtin.set_fact:
home_dir: "{{ lookup('env', 'HOME') }}"
path: "{{ lookup('env', 'PATH') }}"
# Pipe command output
- name: Get command output
ansible.builtin.set_fact:
current_date: "{{ lookup('pipe', 'date +%Y-%m-%d') }}"
git_commit: "{{ lookup('pipe', 'git rev-parse HEAD') }}"
Template and URL Lookups
# Template lookup
- name: Inline template
ansible.builtin.set_fact:
greeting: "{{ lookup('template', 'greeting.j2') }}"
# URL content
- name: Fetch URL content
ansible.builtin.set_fact:
remote_content: "{{ lookup('url', 'https://api.example.com/config') }}"
Password and Random Lookups
# Generate random password
- name: Generate password
ansible.builtin.set_fact:
random_password: "{{ lookup('password', '/dev/null length=32 chars=ascii_letters,digits') }}"
# Random choice
- name: Pick random item
ansible.builtin.set_fact:
random_server: "{{ lookup('random_choice', ['server1', 'server2', 'server3']) }}"
Query vs Lookup
# lookup returns comma-separated string
- name: Using lookup
ansible.builtin.debug:
msg: "{{ lookup('file', 'file1.txt', 'file2.txt') }}"
# Returns: "content1,content2"
# query always returns list
- name: Using query
ansible.builtin.debug:
msg: "{{ query('file', 'file1.txt', 'file2.txt') }}"
# Returns: ["content1", "content2"]
# Prefer query for loops
- name: Loop with query
ansible.builtin.debug:
msg: "{{ item }}"
loop: "{{ query('inventory_hostnames', 'all') }}"
Template Control Structures
Loops in Templates
{# templates/config.j2 #}
# User list
{% for user in users %}
user {{ user.name }}:
uid: {{ user.uid }}
groups: {{ user.groups | join(',') }}
{% endfor %}
# Conditional in loop
{% for item in items if item.enabled %}
- {{ item.name }}: {{ item.value }}
{% endfor %}
# Loop with index
{% for server in servers %}
server_{{ loop.index }}: {{ server.hostname }}
{% endfor %}
Conditionals in Templates
{# templates/app_config.j2 #}
{% if environment == 'production' %}
log_level: warning
max_connections: 1000
{% elif environment == 'staging' %}
log_level: info
max_connections: 500
{% else %}
log_level: debug
max_connections: 100
{% endif %}
# Complex conditions
{% if ansible_os_family == 'Debian' and ansible_distribution_major_version|int >= 20 %}
use_modern_config: true
{% endif %}
# Check if defined
{% if custom_setting is defined %}
custom_setting: {{ custom_setting }}
{% endif %}
# Check if none
{% if database_host is none %}
database_host: localhost
{% else %}
database_host: {{ database_host }}
{% endif %}
Whitespace Control
{# Remove whitespace before #}
{%- if condition %}
content
{% endif %}
{# Remove whitespace after #}
{% if condition -%}
content
{% endif %}
{# Remove both #}
{%- if condition -%}
content
{%- endif -%}
Macros and Includes
{# Define macro #}
{% macro render_user(name, uid) -%}
user: {{ name }}
uid: {{ uid }}
{%- endmacro %}
{# Use macro #}
{{ render_user('alice', 1000) }}
{{ render_user('bob', 1001) }}
{# Include other template #}
{% include 'header.j2' %}
{# Import macros from other template #}
{% from 'macros.j2' import render_user %}
Advanced Template Patterns
Multi-line Strings
server {
listen 80;
server_name {{ server_name }};
{% if ssl_enabled %}
listen 443 ssl;
ssl_certificate {{ ssl_cert_path }};
ssl_certificate_key {{ ssl_key_path }};
{% endif %}
location / {
proxy_pass http://{{ backend_host }}:{{ backend_port }};
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Complex Data Structures
{# Nested loops for complex config #}
{% for service in services %}
[{{ service.name }}]
{% for key, value in service.config.items() %}
{{ key }} = {{ value }}
{% endfor %}
{% endfor %}
{# Generate from dict #}
{% for key, value in app_settings.items() %}
{% endfor %}
Security Best Practices
Sensitive Data
# Use no_log for sensitive operations
- name: Set database password
ansible.builtin.user:
name: dbadmin
password: "{{ db_password | password_hash('sha512') }}"
no_log: true
# Use ansible-vault for secrets
# Encrypt with: ansible-vault encrypt secrets.yml
# Include encrypted vars
- name: Include vault variables
ansible.builtin.include_vars:
file: secrets.yml
File Permissions
- name: Copy sensitive file
ansible.builtin.copy:
src: private_key
dest: /etc/ssl/private/app.key
mode: '0600'
owner: root
group: root
Tags
Using Tags
- name: Install packages
ansible.builtin.package:
name: nginx
state: present
tags:
- packages
- nginx
- install
# Run with: ansible-playbook playbook.yml --tags "install"
# Skip with: ansible-playbook playbook.yml --skip-tags "install"
Tag Categories
install- Installation tasksconfigure- Configuration tasksupdate- Update tasksbackup- Backup tasksalways- Always run (special tag)- `n