Ansible is easy to start, hard to scale. Here’s how to structure playbooks that don’t become unmaintainable nightmares.

Directory Structure

Start organized, stay organized:

ansibaiprclnnlooesvall/ieyelbnpsbswdscnpeltrtoiea/ogoceooaotbtmist.rdgkesamnticyuhgihgs.eboxgof/cornoryranrngtsogsmvsesitu/tulees/ospawdsprsqn._lea._s.lyvlbtyv.y/ma.samaymlryeblrmlsmrasl/lvs/eerss..yymmll

Inventory Patterns

Static YAML Inventory

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# inventory/production/hosts.yml
all:
  children:
    webservers:
      hosts:
        web1.example.com:
        web2.example.com:
      vars:
        http_port: 80
    databases:
      hosts:
        db1.example.com:
          postgresql_port: 5432
        db2.example.com:
          postgresql_port: 5433

Dynamic Inventory

For cloud infrastructure, use dynamic inventory:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# AWS EC2
ansible-inventory -i aws_ec2.yml --list

# aws_ec2.yml
plugin: amazon.aws.aws_ec2
regions:
  - us-east-1
filters:
  tag:Environment: production
keyed_groups:
  - key: tags.Role
    prefix: role

Group Variables Hierarchy

groupawp_lervlboa.sdryeusmrc/lvteirosn..yymmll###AWPlerlbosdheuorcsvtteisrongreonuvpironment

Variables merge with precedence:

all<group<host<playbook<extravars

Role Design

Minimal Role Structure

rolesdthtm/eaaeenfsnmtgakdpaiumsmlmln/mnla/aeaagaxtiiritii/snnsnenn/../.sx.yyy/.ymmmcmlllolnf.j2#####DMHJDeaaiefinnpandjeulanlte2dtaresstnvkeca(mirlrpeiielsassa,btttlaemersestt,a(dlraoetwlaeosatd)precedence)

defaults/main.yml

Sensible defaults that users can override:

1
2
3
4
5
# roles/nginx/defaults/main.yml
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
nginx_server_names_hash_bucket_size: 64

tasks/main.yml

Keep it focused:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# roles/nginx/tasks/main.yml
---
- name: Include OS-specific variables
  include_vars: "{{ ansible_os_family }}.yml"

- name: Install nginx
  package:
    name: nginx
    state: present

- name: Configure nginx
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
    validate: nginx -t -c %s
  notify: Restart nginx

- name: Ensure nginx is running
  service:
    name: nginx
    state: started
    enabled: true

handlers/main.yml

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# roles/nginx/handlers/main.yml
---
- name: Restart nginx
  service:
    name: nginx
    state: restarted

- name: Reload nginx
  service:
    name: nginx
    state: reloaded

Task Patterns

Idempotency

Every task should be safe to run multiple times:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
# Good: Idempotent
- name: Ensure directory exists
  file:
    path: /opt/myapp
    state: directory
    mode: '0755'

# Bad: Not idempotent (creates duplicates)
- name: Add line to file
  shell: echo "export PATH=/opt/bin:$PATH" >> ~/.bashrc

# Good: Idempotent line management
- name: Add to PATH
  lineinfile:
    path: ~/.bashrc
    line: 'export PATH=/opt/bin:$PATH'
    state: present

Conditionals

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
- name: Install on Debian
  apt:
    name: nginx
  when: ansible_os_family == "Debian"

- name: Install on RedHat
  yum:
    name: nginx
  when: ansible_os_family == "RedHat"

# Or use package module (cross-platform)
- name: Install nginx
  package:
    name: nginx
    state: present

Loops

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Simple loop
- name: Create users
  user:
    name: "{{ item }}"
    state: present
  loop:
    - alice
    - bob
    - charlie

# Loop with dictionaries
- name: Create users with groups
  user:
    name: "{{ item.name }}"
    groups: "{{ item.groups }}"
  loop:
    - { name: alice, groups: admin }
    - { name: bob, groups: developers }

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
- name: Deploy application
  block:
    - name: Download artifact
      get_url:
        url: "{{ artifact_url }}"
        dest: /tmp/app.tar.gz

    - name: Extract artifact
      unarchive:
        src: /tmp/app.tar.gz
        dest: /opt/app
        remote_src: yes

  rescue:
    - name: Rollback on failure
      copy:
        src: /opt/app.backup
        dest: /opt/app
        remote_src: yes

  always:
    - name: Clean up temp files
      file:
        path: /tmp/app.tar.gz
        state: absent

Variable Management

Vault for Secrets

1
2
3
4
5
6
7
8
# Create encrypted file
ansible-vault create secrets.yml

# Edit encrypted file
ansible-vault edit secrets.yml

# Use in playbook
ansible-playbook site.yml --ask-vault-pass
1
2
3
# group_vars/all/secrets.yml (encrypted)
db_password: supersecret
api_key: abc123

Variable Precedence Strategy

1
2
3
4
5
6
7
8
9
# defaults/main.yml - Role defaults (override freely)
nginx_port: 80

# vars/main.yml - Role internals (don't override)
nginx_config_path: /etc/nginx/nginx.conf

# group_vars/ - Environment-specific
# host_vars/ - Host-specific
# --extra-vars - Runtime overrides (highest precedence)

Playbook Organization

Site Playbook

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# playbooks/site.yml
---
- name: Apply common configuration
  hosts: all
  roles:
    - common
    - security

- name: Configure web servers
  hosts: webservers
  roles:
    - nginx
    - app

- name: Configure databases
  hosts: databases
  roles:
    - postgresql

Tags for Selective Runs

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
- name: Install packages
  package:
    name: nginx
  tags:
    - install
    - nginx

- name: Configure nginx
  template:
    src: nginx.conf.j2
    dest: /etc/nginx/nginx.conf
  tags:
    - configure
    - nginx
1
2
3
4
5
# Run only nginx tasks
ansible-playbook site.yml --tags nginx

# Skip installation
ansible-playbook site.yml --skip-tags install

Testing

Molecule for Role Testing

1
2
3
4
5
6
# Initialize molecule scenario
cd roles/nginx
molecule init scenario -d docker

# Run tests
molecule test
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# molecule/default/molecule.yml
dependency:
  name: galaxy
driver:
  name: docker
platforms:
  - name: instance
    image: ubuntu:22.04
provisioner:
  name: ansible
verifier:
  name: ansible

Ansible-lint

1
ansible-lint playbooks/site.yml
1
2
3
4
# .ansible-lint
skip_list:
  - yaml[line-length]
  - name[casing]

Performance

Parallelism

1
2
3
# ansible.cfg
[defaults]
forks = 20  # Parallel hosts

Pipelining

1
2
[ssh_connection]
pipelining = True  # Reduces SSH operations

Async Tasks

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
- name: Long running task
  command: /opt/scripts/backup.sh
  async: 3600  # Max runtime in seconds
  poll: 0      # Don't wait

- name: Check backup status
  async_status:
    jid: "{{ backup_job.ansible_job_id }}"
  register: job_result
  until: job_result.finished
  retries: 60
  delay: 60

Caching Facts

1
2
3
4
5
[defaults]
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts
fact_caching_timeout = 86400

Common Mistakes

1. Hardcoded Values

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
# Bad
- name: Configure app
  template:
    src: app.conf.j2
    dest: /etc/myapp/app.conf
    owner: appuser
    group: appuser

# Good
- name: Configure app
  template:
    src: app.conf.j2
    dest: "{{ app_config_path }}"
    owner: "{{ app_user }}"
    group: "{{ app_group }}"

2. Missing Handlers

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Bad: Service not restarted after config change
- name: Update config
  template:
    src: app.conf.j2
    dest: /etc/app.conf

# Good: Notify handler
- name: Update config
  template:
    src: app.conf.j2
    dest: /etc/app.conf
  notify: Restart app

3. Command Module Overuse

1
2
3
4
5
6
7
8
9
# Bad: Using command when module exists
- name: Install package
  command: apt-get install -y nginx

# Good: Use the module
- name: Install package
  apt:
    name: nginx
    state: present

The Ansible Checklist

  • Organized directory structure
  • Roles for reusable components
  • Defaults for overridable values
  • Vault for secrets
  • Idempotent tasks
  • Handlers for service restarts
  • Tags for selective runs
  • Tests with Molecule
  • Linting in CI

Start simple, add complexity only when needed. The best Ansible code is boring and predictable.


Ansible’s power is simplicity. The goal isn’t clever automation — it’s reliable, readable automation that your team can maintain.