Ansible Playbooks and Roles: Writing Reusable Infrastructure Automation
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):
- Role defaults (
defaults/main.yml) - Inventory group vars
- Inventory host vars
- Playbook group vars
- Playbook host vars
- Role vars (
vars/main.yml) - Play vars
- Task vars
--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
Was this article helpful?
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
Ansible for Infrastructure Automation: Dynamic Inventory, Vault, and CI/CD Integration
Advanced Ansible patterns for production infrastructure — dynamic inventory from AWS/GCP, encrypting secrets with Ansible Vault, integrating with CI/CD pipelines, and structuring large projects with collections.
Ansible Fundamentals: Your First Playbook to Production
Learn Ansible from scratch — install Ansible, write your first playbook, understand modules, manage inventories, and automate server configuration.
Ansible Roles and Galaxy: Structuring Automation at Scale
Structure your Ansible automation with roles for reusability. Learn role directory structure, Galaxy usage, dependencies, and testing with Molecule.
HashiCorp Vault: Secrets Management from First Run to Production
How to set up and use HashiCorp Vault for secrets management — KV secrets engine, dynamic credentials, policies, AppRole authentication, and production hardening. Stop hardcoding credentials in environment variables.
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.
Alibaba Cloud ECS: Deploy and Manage Virtual Machines at Scale
Deploy and manage Alibaba Cloud ECS instances — instance families, storage types, security groups, auto-scaling groups, SLB load balancers, image management, and Terraform configuration for production workloads.