Ansible Development
This skill helps you create, debug, and manage Ansible playbooks, roles, collections, and automation workflows. It provides best practices for infrastructure as code, proper YAML formatting, idempotent task design, and effective use of Ansible's features.
Use this skill when you need to:
Standard Ansible project layout:
ansible-project/
├── ansible.cfg
├── inventory/
│ ├── production/
│ │ ├── hosts.yml
│ │ └── group_vars/
│ └── staging/
│ ├── hosts.yml
│ └── group_vars/
├── playbooks/
│ ├── site.yml
│ ├── webservers.yml
│ └── databases.yml
├── roles/
│ ├── common/
│ │ ├── tasks/
│ │ ├── handlers/
│ │ ├── templates/
│ │ ├── files/
│ │ ├── vars/
│ │ ├── defaults/
│ │ └── meta/
│ └── nginx/
└── group_vars/
├── all.yml
└── webservers.yml
Critical Rule: Every task should be idempotent - running it multiple times should produce the same result without unintended side effects.
Good Examples:
# ✅ Idempotent - uses ansible.builtin.package
- name: Ensure nginx is installed
ansible.builtin.package:
name: nginx
state: present
# ✅ Idempotent - uses creates parameter
- name: Download application
ansible.builtin.command: wget https://example.com/app.tar.gz
args:
creates: /opt/app.tar.gz
# ✅ Idempotent - checks state before acting
- name: Ensure service is running
ansible.builtin.service:
name: nginx
state: started
enabled: yes
Bad Examples:
# ❌ Not idempotent - always executes
- name: Download application
ansible.builtin.command: wget https://example.com/app.tar.gz
# ❌ Not idempotent - always restarts
- name: Restart nginx
ansible.builtin.command: systemctl restart nginx
# ❌ Not idempotent - always appends
- name: Add line to file
ansible.builtin.shell: echo "config=value" >> /etc/app.conf
# ✅ Good - clear, readable, properly structured
---
- name: Configure web servers
hosts: webservers
become: yes
vars:
nginx_port: 80
app_user: webapp
tasks:
- name: Install nginx
ansible.builtin.package:
name: nginx
state: present
notify: Restart nginx
- name: Copy nginx configuration
ansible.builtin.template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
notify: Restart nginx
handlers:
- name: Restart nginx
ansible.builtin.service:
name: nginx
state: restarted
Always use Fully Qualified Collection Names (FQCNs):
# ✅ Good - FQCN format
- name: Create directory
ansible.builtin.file:
path: /opt/app
state: directory
mode: '0755'
# ⚠️ Acceptable but not recommended
- name: Create directory
file:
path: /opt/app
state: directory
Understand Ansible's variable precedence (lowest to highest):
# Ignore errors when acceptable
- name: Check if file exists
ansible.builtin.stat:
path: /tmp/myfile
register: file_check
ignore_errors: yes
# Use failed_when for custom failure conditions
- name: Run command
ansible.builtin.command: /usr/bin/mycommand
register: result
failed_when: "'ERROR' in result.stderr"
# Use changed_when to control change status
- name: Check configuration
ansible.builtin.command: /usr/bin/check_config
register: config_check
changed_when: false
# Use blocks for error handling
- name: Handle errors with blocks
block:
- name: Risky task
ansible.builtin.command: /usr/bin/risky_operation
rescue:
- name: Handle failure
ansible.builtin.debug:
msg: "Operation failed, running recovery"
- name: Recovery task
ansible.builtin.command: /usr/bin/recover
always:
- name: Always runs
ansible.builtin.debug:
msg: "Cleanup complete"
# roles/webserver/tasks/main.yml
---
- name: Include OS-specific variables
ansible.builtin.include_vars: "{{ ansible_os_family }}.yml"
- name: Install web server
ansible.builtin.package:
name: "{{ webserver_package }}"
state: present
- name: Configure web server
ansible.builtin.template:
src: webserver.conf.j2
dest: "{{ webserver_config_path }}"
mode: '0644'
notify: restart webserver
- name: Ensure web server is running
ansible.builtin.service:
name: "{{ webserver_service }}"
state: started
enabled: yes
# When clauses
- name: Install package on RedHat family
ansible.builtin.yum:
name: httpd
state: present
when: ansible_os_family == "RedHat"
# Multiple conditions
- name: Complex conditional
ansible.builtin.debug:
msg: "This runs on Ubuntu 20.04+"
when:
- ansible_distribution == "Ubuntu"
- ansible_distribution_version is version('20.04', '>=')
# Conditional with registered variables
- name: Check if file exists
ansible.builtin.stat:
path: /etc/myapp.conf
register: config_file
- name: Create default config if missing
ansible.builtin.copy:
src: myapp.conf.default
dest: /etc/myapp.conf
when: not config_file.stat.exists
# Simple loop
- name: Install multiple packages
ansible.builtin.package:
name: "{{ item }}"
state: present
loop:
- nginx
- postgresql
- redis
# Loop with dictionary
- name: Create multiple users
ansible.builtin.user:
name: "{{ item.name }}"
groups: "{{ item.groups }}"
state: present
loop:
- { name: 'alice', groups: 'admin,developers' }
- { name: 'bob', groups: 'developers' }
- { name: 'carol', groups: 'operators' }
# Loop with loop_control
- name: Process items with clearer output
ansible.builtin.debug:
msg: "Processing {{ item }}"
loop:
- one
- two
- three
loop_control:
label: "{{ item }}"
pause: 2
{# templates/nginx.conf.j2 #}
user {{ nginx_user }};
worker_processes {{ ansible_processor_vcpus }};
{% if nginx_ssl_enabled %}
ssl_protocols TLSv1.2 TLSv1.3;
ssl_certificate {{ nginx_ssl_cert }};
ssl_certificate_key {{ nginx_ssl_key }};
{% endif %}
upstream backend {
{% for host in groups['backend_servers'] %}
server {{ hostvars[host]['ansible_default_ipv4']['address'] }}:8080;
{% endfor %}
}
server {
listen {{ nginx_port }};
server_name {{ nginx_server_name }};
location / {
proxy_pass http://backend;
}
}
# Basic verbosity
ansible-playbook playbook.yml -v
# More verbose (shows task execution details)
ansible-playbook playbook.yml -vv
# Very verbose (shows connection debugging)
ansible-playbook playbook.yml -vvv
# Maximum verbosity (connection debugging + plugin details)
ansible-playbook playbook.yml -vvvv
- name: Show all facts
ansible.builtin.debug:
var: ansible_facts
- name: Show specific variable
ansible.builtin.debug:
var: my_variable
- name: Show custom message
ansible.builtin.debug:
msg: "The value is {{ my_variable }}"
- name: Debug with verbosity control
ansible.builtin.debug:
msg: "Only shows with -v or higher"
verbosity: 1
# Check mode - don't make changes, just show what would change
ansible-playbook playbook.yml --check
# Diff mode - show file differences
ansible-playbook playbook.yml --diff
# Combine both
ansible-playbook playbook.yml --check --diff
# Start at a specific task
ansible-playbook playbook.yml --start-at-task="Install nginx"
# Use tags to run specific tasks
ansible-playbook playbook.yml --tags "configuration,deployment"
# Skip specific tags
ansible-playbook playbook.yml --skip-tags "testing"
# Check playbook syntax
ansible-playbook playbook.yml --syntax-check
# Lint playbooks with ansible-lint
ansible-lint playbook.yml
# molecule/default/molecule.yml
---