Ansible playbooks start simple and grow complex. A quick server setup becomes infrastructure-as-code for dozens of machines. Here are patterns that keep playbooks maintainable as they scale.

Project Structure

ansibaiprflnnloiesvall/ieyeebnpsbswdscnpsltrtoiea/ogo/eooaotbtmis.rdgkesamntcyuhgihgs.eboxgf/cornoryranrgtsogsmvseitu/tuleesospawsprs/n._le._s.yvlbyv.yma.smaymlryelrmlsmrsl/lv/ers.yml

ansible.cfg

1
2
3
4
5
6
7
8
9
[defaults]
inventory = inventory/production
roles_path = roles
host_key_checking = False
retry_files_enabled = False

[ssh_connection]
pipelining = True
control_path = /tmp/ansible-%%r@%%h:%%p

Inventory Patterns

YAML Inventory (Preferred)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# inventory/production/hosts.yml
all:
  children:
    webservers:
      hosts:
        web1.example.com:
        web2.example.com:
      vars:
        http_port: 80
    
    databases:
      hosts:
        db1.example.com:
          postgres_version: 15
        db2.example.com:
          postgres_version: 14
    
    loadbalancers:
      hosts:
        lb1.example.com:

Group Variables

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# inventory/production/group_vars/all.yml
---
ansible_user: deploy
ansible_python_interpreter: /usr/bin/python3
ntp_servers:
  - 0.pool.ntp.org
  - 1.pool.ntp.org

# inventory/production/group_vars/webservers.yml
---
nginx_worker_processes: auto
nginx_worker_connections: 1024
app_root: /var/www/app

Role Structure

rolesdvthtfm/eaaaeienfrsnmltgaskdpeaium/msmlmlnss/mnlaa/aeaag/saxtiiiritili/snnnsnen-n/.../.sxp.yyyy/.aymmmmcrmlllloalnmfs..jc2on#####f#DRTHJReoaaioflsnnlaekdjeulalvee2mtanrertsttvireaaay(mdrbrpailpeltaeosaabsittlnaeae(trsnshtdi(gsdlheeorpwpveerinsicdtoeersnpi,crtiiyeeo)tsrci.t)y)

defaults/main.yml

1
2
3
4
5
6
---
# Overridable defaults
nginx_worker_processes: auto
nginx_worker_connections: 768
nginx_keepalive_timeout: 65
nginx_server_tokens: "off"

tasks/main.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
---
- name: Install nginx
  ansible.builtin.apt:
    name: nginx
    state: present
    update_cache: true
  become: true

- name: Configure nginx
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    owner: root
    group: root
    mode: '0644'
  become: true
  notify: Reload nginx

- name: Enable and start nginx
  ansible.builtin.systemd:
    name: nginx
    enabled: true
    state: started
  become: true

handlers/main.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
---
- name: Reload nginx
  ansible.builtin.systemd:
    name: nginx
    state: reloaded
  become: true

- name: Restart nginx
  ansible.builtin.systemd:
    name: nginx
    state: restarted
  become: true

Task Patterns

Idempotent Tasks

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Good - idempotent
- name: Ensure user exists
  ansible.builtin.user:
    name: deploy
    state: present
    groups: [sudo, docker]

# Avoid - not idempotent
- name: Add user
  ansible.builtin.command: useradd deploy

Conditional Execution

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
- name: Install package (Debian)
  ansible.builtin.apt:
    name: nginx
    state: present
  when: ansible_os_family == "Debian"

- name: Install package (RedHat)
  ansible.builtin.dnf:
    name: nginx
    state: present
  when: ansible_os_family == "RedHat"

Loops

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
- name: Create users
  ansible.builtin.user:
    name: "{{ item.name }}"
    groups: "{{ item.groups }}"
    state: present
  loop:
    - { name: alice, groups: [developers] }
    - { name: bob, groups: [developers, sudo] }
  loop_control:
    label: "{{ item.name }}"  # Cleaner output

Blocks for Error Handling

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
- name: Deploy application
  block:
    - name: Pull latest code
      ansible.builtin.git:
        repo: "{{ app_repo }}"
        dest: "{{ app_root }}"
        version: "{{ app_version }}"
    
    - name: Install dependencies
      ansible.builtin.pip:
        requirements: "{{ app_root }}/requirements.txt"
        virtualenv: "{{ app_root }}/venv"
    
    - name: Run migrations
      ansible.builtin.command:
        cmd: "{{ app_root }}/venv/bin/python manage.py migrate"
        chdir: "{{ app_root }}"

  rescue:
    - name: Notify on failure
      ansible.builtin.debug:
        msg: "Deployment failed, rolling back"
    
    - name: Rollback to previous version
      ansible.builtin.git:
        repo: "{{ app_repo }}"
        dest: "{{ app_root }}"
        version: "{{ previous_version }}"

  always:
    - name: Restart application
      ansible.builtin.systemd:
        name: myapp
        state: restarted

Variable Precedence

From lowest to highest priority:

  1. Role defaults (roles/x/defaults/main.yml)
  2. Inventory group_vars
  3. Inventory host_vars
  4. Playbook vars
  5. Task vars
  6. Extra vars (-e flag) — highest priority

Use this strategically:

1
2
3
4
5
6
7
8
# roles/nginx/defaults/main.yml - sensible defaults
nginx_worker_processes: auto

# inventory/production/group_vars/webservers.yml - environment-specific
nginx_worker_processes: 4

# Command line override for testing
ansible-playbook site.yml -e "nginx_worker_processes=1"

Secret Management

Ansible Vault

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# Create encrypted file
ansible-vault create group_vars/all/vault.yml

# Edit encrypted file
ansible-vault edit group_vars/all/vault.yml

# Run playbook with vault
ansible-playbook site.yml --ask-vault-pass
# Or with password file
ansible-playbook site.yml --vault-password-file ~/.vault_pass
1
2
3
4
5
6
7
# group_vars/all/vault.yml (encrypted)
vault_db_password: "supersecret"
vault_api_key: "abc123"

# group_vars/all/main.yml (reference vault vars)
db_password: "{{ vault_db_password }}"
api_key: "{{ vault_api_key }}"

Tags for Selective Runs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
- name: Install packages
  ansible.builtin.apt:
    name: "{{ item }}"
    state: present
  loop: [nginx, python3, git]
  tags: [packages, setup]

- name: Deploy application
  ansible.builtin.git:
    repo: "{{ app_repo }}"
    dest: "{{ app_root }}"
  tags: [deploy]

- name: Configure nginx
  ansible.builtin.template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  tags: [config, nginx]
1
2
3
4
5
6
7
8
# Run only deploy tasks
ansible-playbook site.yml --tags deploy

# Skip config tasks
ansible-playbook site.yml --skip-tags config

# List available tags
ansible-playbook site.yml --list-tags

Testing with Check Mode

1
2
3
4
5
# Dry run - shows what would change
ansible-playbook site.yml --check

# Diff mode - shows file changes
ansible-playbook site.yml --check --diff

Make tasks check-mode aware:

1
2
3
4
5
6
- name: Run database migration
  ansible.builtin.command:
    cmd: ./manage.py migrate
  when: not ansible_check_mode  # Skip in check mode
  changed_when: "'No migrations' not in migration_result.stdout"
  register: migration_result

Common Anti-Patterns

Don’t Use Shell When Module Exists

1
2
3
4
5
6
7
8
9
# Bad
- name: Install package
  ansible.builtin.shell: apt-get install -y nginx

# Good
- name: Install package
  ansible.builtin.apt:
    name: nginx
    state: present

Don’t Hardcode Paths

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Bad
- name: Copy config
  ansible.builtin.copy:
    src: /home/user/myconfig.conf
    dest: /etc/myapp/config.conf

# Good
- name: Copy config
  ansible.builtin.template:
    src: config.conf.j2
    dest: "{{ config_dir }}/config.conf"

Don’t Ignore Errors Silently

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Bad
- name: Do something risky
  ansible.builtin.command: risky-command
  ignore_errors: true

# Better - handle the error
- name: Do something risky
  ansible.builtin.command: risky-command
  register: result
  failed_when: 
    - result.rc != 0
    - "'expected error' not in result.stderr"

Quick Reference

PatternUse Case
RolesReusable, self-contained functionality
group_varsEnvironment/group-specific config
VaultSecrets management
TagsSelective task execution
HandlersService restarts after changes
BlocksError handling and cleanup
Check modeDry runs and validation

Good Ansible code reads like documentation. Anyone should be able to look at your playbooks and understand what they do. Use meaningful names, organize with roles, and keep tasks idempotent. Your future self will thank you.