DevOpsil
90%
Needs Review

Ansible Playbooks and Roles: Writing Reusable Infrastructure Automation

Zara BlackwoodZara Blackwood5 min read

Ansible's Mental Model

Ansible is agentless — it SSH's into target machines and runs Python code. The building blocks:

  • Inventory: The list of hosts you manage
  • Playbook: One or more plays; a play maps hosts to tasks
  • Task: A single action (install a package, write a file, restart a service)
  • Role: A reusable bundle of tasks, variables, templates, and handlers
  • Handler: A task triggered only when something changed (restart nginx after config change)

The key property Ansible enforces is idempotency — running a playbook twice produces the same result. Modules check state before acting.


Inventory

Defines your hosts and groups. Static inventory in INI format:

# inventory/hosts

[webservers]
web01.example.com
web02.example.com ansible_user=deploy ansible_port=2222

[dbservers]
db01.example.com
db02.example.com

[production:children]
webservers
dbservers

[all:vars]
ansible_python_interpreter=/usr/bin/python3

Or YAML format (preferred for large inventories):

# inventory/hosts.yml
all:
  children:
    webservers:
      hosts:
        web01.example.com:
        web02.example.com:
          ansible_user: deploy
          ansible_port: 2222
    dbservers:
      hosts:
        db01.example.com:
        db02.example.com:
  vars:
    ansible_python_interpreter: /usr/bin/python3

Test connectivity:

ansible -i inventory/hosts.yml all -m ping
ansible -i inventory/hosts.yml webservers -m shell -a "uptime"

Basic Playbook

# site.yml
---
- name: Configure web servers
  hosts: webservers
  become: yes    # sudo

  vars:
    nginx_port: 80
    app_root: /var/www/app

  tasks:
    - name: Install nginx
      ansible.builtin.package:
        name: nginx
        state: present

    - name: Create application directory
      ansible.builtin.file:
        path: "{{ app_root }}"
        state: directory
        owner: www-data
        group: www-data
        mode: '0755'

    - name: Deploy nginx config
      ansible.builtin.template:
        src: templates/nginx.conf.j2
        dest: /etc/nginx/nginx.conf
        owner: root
        group: root
        mode: '0644'
      notify: Restart nginx

    - name: Ensure nginx is running
      ansible.builtin.service:
        name: nginx
        state: started
        enabled: yes

  handlers:
    - name: Restart nginx
      ansible.builtin.service:
        name: nginx
        state: restarted

Run it:

# Dry run (check mode)
ansible-playbook -i inventory/hosts.yml site.yml --check --diff

# Apply
ansible-playbook -i inventory/hosts.yml site.yml

# Target specific hosts
ansible-playbook -i inventory/hosts.yml site.yml --limit web01.example.com

# Run specific tags
ansible-playbook -i inventory/hosts.yml site.yml --tags nginx

Jinja2 Templates

Templates use Jinja2 syntax and live in templates/:

{# templates/nginx.conf.j2 #}
user www-data;
worker_processes {{ ansible_processor_vcpus }};    {# fact from target host #}
error_log /var/log/nginx/error.log;

events {
    worker_connections {{ nginx_worker_connections | default(1024) }};
}

http {
    server {
        listen {{ nginx_port }};
        server_name {{ inventory_hostname }};    {# hostname from inventory #}
        root {{ app_root }};

        {% if ssl_enabled | default(false) %}
        ssl_certificate {{ ssl_cert_path }};
        ssl_certificate_key {{ ssl_key_path }};
        {% endif %}
    }
}

Roles: Structuring Reusable Automation

A role bundles related tasks, variables, templates, and handlers:

roles/
  nginx/
    tasks/
      main.yml        # main entry point (required)
      install.yml     # included by main
      configure.yml   # included by main
    handlers/
      main.yml        # handlers for this role
    templates/
      nginx.conf.j2
      vhost.conf.j2
    files/
      htpasswd        # static files to copy
    vars/
      main.yml        # role variables (high precedence)
    defaults/
      main.yml        # default variables (lowest precedence)
    meta/
      main.yml        # role metadata, dependencies

Role Tasks

# roles/nginx/tasks/main.yml
---
- name: Install nginx
  ansible.builtin.include_tasks: install.yml
  tags: [nginx, install]

- name: Configure nginx
  ansible.builtin.include_tasks: configure.yml
  tags: [nginx, configure]
# roles/nginx/tasks/install.yml
---
- name: Install nginx package
  ansible.builtin.package:
    name: "{{ nginx_package | default('nginx') }}"
    state: present

Role Defaults

# roles/nginx/defaults/main.yml
---
nginx_port: 80
nginx_worker_connections: 1024
nginx_user: www-data
ssl_enabled: false

Defaults are overridden by playbook vars, inventory vars, or vars/main.yml.

Using Roles in a Playbook

# site.yml
---
- name: Web server setup
  hosts: webservers
  become: yes
  roles:
    - role: common      # runs first
    - role: nginx
      vars:
        nginx_port: 8080
    - role: app_deploy
      tags: [deploy]

Variable Precedence

Ansible has 22 levels of variable precedence (lowest to highest):

  1. Role defaults (defaults/main.yml)
  2. Inventory group vars
  3. Inventory host vars
  4. Playbook group vars
  5. Playbook host vars
  6. Role vars (vars/main.yml)
  7. Play vars
  8. Task vars
  9. --extra-vars (highest)

Practical rule: use defaults/ for user-overridable settings, vars/ for role-internal constants.


Inventory Group and Host Vars

inventory/
  hosts.yml
  group_vars/
    all.yml            # applies to all hosts
    webservers.yml     # applies to webservers group
    webservers/
      nginx.yml        # can split into multiple files
  host_vars/
    web01.example.com.yml    # specific to one host
# inventory/group_vars/webservers.yml
nginx_port: 80
ssl_enabled: true
ssl_cert_path: /etc/letsencrypt/live/example.com/fullchain.pem

Common Modules

# Package management
- ansible.builtin.package:
    name: [nginx, curl, git]
    state: present

# File operations
- ansible.builtin.copy:
    src: files/myfile.conf
    dest: /etc/myapp/myfile.conf
    mode: '0644'

- ansible.builtin.template:
    src: templates/config.j2
    dest: /etc/myapp/config.conf

- ansible.builtin.lineinfile:
    path: /etc/ssh/sshd_config
    regexp: '^PermitRootLogin'
    line: 'PermitRootLogin no'

# Service management
- ansible.builtin.service:
    name: nginx
    state: started
    enabled: yes

# User management
- ansible.builtin.user:
    name: deploy
    shell: /bin/bash
    groups: sudo
    append: yes

# Run commands (use sparingly — prefer dedicated modules)
- ansible.builtin.command:
    cmd: /usr/bin/some-script.sh
    creates: /var/run/script.pid   # skip if this file exists (idempotency)

# Register output from commands
- ansible.builtin.command:
    cmd: openssl x509 -enddate -noout -in /etc/ssl/cert.pem
  register: cert_expiry

- ansible.builtin.debug:
    msg: "Cert expires: {{ cert_expiry.stdout }}"

Ansible Galaxy: Using Community Roles

# Install a role from Ansible Galaxy
ansible-galaxy install geerlingguy.nginx
ansible-galaxy install geerlingguy.docker

# Install from requirements file
ansible-galaxy install -r requirements.yml

# requirements.yml
roles:
  - name: geerlingguy.nginx
    version: 3.2.0
  - name: geerlingguy.docker
    version: 6.1.0

collections:
  - name: community.docker
    version: ">=3.0.0"

Testing with Molecule

Molecule tests your roles in ephemeral environments (Docker, Vagrant, cloud):

pip install molecule molecule-docker

# Initialize molecule in a role
cd roles/nginx
molecule init scenario

# Project structure added:
# molecule/default/
#   molecule.yml
#   converge.yml
#   verify.yml
# molecule/default/molecule.yml
driver:
  name: docker

platforms:
  - name: ubuntu-24.04
    image: geerlingguy/docker-ubuntu2404-ansible:latest
    pre_build_image: true
  - name: rocky-linux-9
    image: geerlingguy/docker-rockylinux9-ansible:latest
    pre_build_image: true

provisioner:
  name: ansible

verifier:
  name: ansible
# Run the full test cycle
molecule test

# Debug: create container and run converge only
molecule converge
molecule login   # SSH into test container
molecule destroy
Share:

Was this article helpful?

Zara Blackwood
Zara Blackwood

Platform Engineer

Terraform enthusiast, platform builder, DRY advocate. I believe infrastructure should be versioned, reviewed, and deployed like any other code. GitOps or bust.

Related Articles

Alibaba CloudTutorialBeginnerNeeds Review

Alibaba Cloud ACK: Managed Kubernetes in Asia's Largest Cloud

Set up and manage a production Kubernetes cluster on Alibaba Cloud ACK (Container Service for Kubernetes). Covers cluster types, node pools, SLB ingress, RRSA workload identity, Container Registry, and Terraform configuration.

Aareez Asif·
6 min read

Discussion