Ansible Roles
What are Roles?
Roles provide a way to organize playbooks into reusable components. They allow you to automatically load variables, files, templates, and tasks based on a known file structure.
Role Directory Structure
roles/
└── webserver/
├── defaults/ # Default variables (lowest priority)
│ └── main.yml
├── files/ # Static files to copy
│ └── index.html
├── handlers/ # Handler definitions
│ └── main.yml
├── meta/ # Role metadata and dependencies
│ └── main.yml
├── tasks/ # Main task list
│ └── main.yml
├── templates/ # Jinja2 templates
│ └── nginx.conf.j2
├── tests/ # Test playbooks
│ ├── inventory
│ └── test.yml
└── vars/ # Role variables (higher priority)
└── main.yml
Creating a Role
Using ansible-galaxy
# Create role structure
ansible-galaxy init webserver
# Create role in specific directory
ansible-galaxy init roles/webserver
Example Role: webserver
tasks/main.yml
---
- name: Install Nginx
package:
name: nginx
state: present
- name: Copy Nginx configuration
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
notify: restart nginx
- name: Copy website files
copy:
src: "{{ item }}"
dest: /var/www/html/
with_fileglob:
- "files/*"
- name: Ensure Nginx is running
service:
name: nginx
state: started
enabled: yes
handlers/main.yml
---
- name: restart nginx
service:
name: nginx
state: restarted
- name: reload nginx
service:
name: nginx
state: reloaded
defaults/main.yml
---
nginx_port: 80
nginx_user: www-data
worker_processes: auto
worker_connections: 1024
vars/main.yml
---
nginx_config_path: /etc/nginx/nginx.conf
nginx_log_path: /var/log/nginx
templates/nginx.conf.j2
user {{ nginx_user }};
worker_processes {{ worker_processes }};
events {
worker_connections {{ worker_connections }};
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log {{ nginx_log_path }}/access.log;
error_log {{ nginx_log_path }}/error.log;
server {
listen {{ nginx_port }};
server_name _;
location / {
root /var/www/html;
index index.html;
}
}
}
meta/main.yml
---
galaxy_info:
author: Your Name
description: Nginx web server role
license: MIT
min_ansible_version: 2.9
platforms:
- name: Ubuntu
versions:
- focal
- jammy
- name: EL
versions:
- 8
- 9
galaxy_tags:
- web
- nginx
dependencies: []
Using Roles in Playbooks
Basic Usage
---
- name: Configure web servers
hosts: webservers
roles:
- webserver
With Variables
---
- name: Configure web servers
hosts: webservers
roles:
- role: webserver
nginx_port: 8080
nginx_user: nginx
Using vars
---
- name: Configure web servers
hosts: webservers
roles:
- role: webserver
vars:
nginx_port: 8080
worker_processes: 4
Conditional Role Execution
---
- name: Configure servers
hosts: all
roles:
- role: webserver
when: "'webservers' in group_names"
- role: database
when: "'databases' in group_names"
Using tags
---
- name: Configure servers
hosts: all
roles:
- { role: common, tags: ['common'] }
- { role: webserver, tags: ['web'] }
- { role: database, tags: ['db'] }
Run specific roles:
ansible-playbook site.yml --tags web
Role Dependencies
In meta/main.yml:
---
dependencies:
- role: common
vars:
ntp_server: time.example.com
- role: firewall
firewall_rules:
- { port: 80, protocol: tcp }
- { port: 443, protocol: tcp }
Ansible Galaxy
Install Roles from Galaxy
# Install specific role
ansible-galaxy install geerlingguy.nginx
# Install from requirements file
ansible-galaxy install -r requirements.yml
# Install to specific directory
ansible-galaxy install geerlingguy.mysql -p ./roles/
requirements.yml
---
# From Ansible Galaxy
- name: geerlingguy.nginx
version: 3.1.4
- name: geerlingguy.mysql
# From Git repository
- src: https://github.com/user/ansible-role-app.git
name: app
version: main
# From Tar archive
- src: https://example.com/roles/myrole.tar.gz
name: myrole
Search Roles
ansible-galaxy search nginx
ansible-galaxy search --author geerlingguy
Get Role Info
ansible-galaxy info geerlingguy.nginx
Role Collections
Collections are a distribution format for Ansible content including roles, modules, and plugins.
Install Collection
# Install from Galaxy
ansible-galaxy collection install community.general
# Install from requirements
ansible-galaxy collection install -r requirements.yml
requirements.yml for Collections
---
collections:
- name: community.general
version: 6.1.0
- name: ansible.posix
version: ">=1.3.0"
- name: https://github.com/user/my-collection.git
type: git
version: main
Use Collection in Playbook
---
- name: Using collections
hosts: all
collections:
- community.general
tasks:
- name: Use module from collection
docker_container:
name: myapp
image: nginx
state: started
Best Practices
- Single purpose: Each role should do one thing well
- Default values: Use defaults/main.yml for configurable parameters
- Documentation: Add README.md explaining role usage
- Idempotency: Ensure roles can be run multiple times safely
- Testing: Create tests in tests/ directory
- Dependencies: Declare role dependencies in meta/main.yml
- Variables: Use role-specific variable prefixes to avoid conflicts
- Handlers: Use handlers for service restarts
- Version control: Keep roles in separate git repositories
- Galaxy: Leverage existing roles from Ansible Galaxy
Complete Example
---
# site.yml
- name: Configure all servers
hosts: all
roles:
- common
- name: Configure web servers
hosts: webservers
roles:
- webserver
- { role: ssl, when: use_ssl }
- name: Configure databases
hosts: databases
roles:
- database
- backup
Advanced Role Patterns
Role Composition
---
# roles/application/meta/main.yml
dependencies:
- role: common
- role: users
users_list:
- { name: 'appuser', uid: 1001 }
- role: firewall
firewall_allowed_ports:
- 8080
- 8443
- role: monitoring
monitoring_checks:
- http_port_8080
- process_java
# The application role inherits all dependent roles
Multi-Environment Roles
---
# roles/application/defaults/main.yml
app_version: "1.0.0"
app_port: 8080
app_memory: "2g"
app_replicas: 2
# Environment-specific overrides
app_config:
development:
debug: true
log_level: DEBUG
db_pool_size: 5
staging:
debug: false
log_level: INFO
db_pool_size: 10
production:
debug: false
log_level: WARN
db_pool_size: 50
# Usage in tasks
- name: Deploy configuration
template:
src: app.conf.j2
dest: /etc/app/config.conf
vars:
env_config: "{{ app_config[environment | default('development')] }}"
Role Includes for Modularity
---
# roles/webserver/tasks/main.yml
- name: Include OS-specific tasks
include_tasks: "{{ ansible_os_family }}.yml"
- name: Include installation tasks
include_tasks: install.yml
- name: Include configuration tasks
include_tasks: configure.yml
- name: Include SSL setup if enabled
include_tasks: ssl.yml
when: webserver_ssl_enabled | default(false)
# roles/webserver/tasks/RedHat.yml
- name: Install EPEL repository
yum:
name: epel-release
state: present
# roles/webserver/tasks/Debian.yml
- name: Update apt cache
apt:
update_cache: yes
cache_valid_time: 3600
Parameterized Role Invocation
---
- name: Deploy multiple applications
hosts: appservers
tasks:
- name: Deploy application instances
include_role:
name: application
vars:
app_name: "{{ item.name }}"
app_port: "{{ item.port }}"
app_version: "{{ item.version }}"
loop:
- { name: 'api', port: 8080, version: '2.1.0' }
- { name: 'worker', port: 8081, version: '2.0.5' }
- { name: 'scheduler', port: 8082, version: '1.9.3' }
- name: Deploy with dynamic variables
include_role:
name: database
tasks_from: backup.yml
vars:
backup_destination: "/backup/{{ inventory_hostname }}/{{ ansible_date_time.date }}"
Advanced Role Testing with Molecule
Complete Molecule Setup
# Install Molecule with Docker driver
pip install molecule molecule-docker pytest-testinfra
# Initialize Molecule in role
cd roles/webserver
molecule init scenario --driver-name docker
# Directory structure created:
# molecule/
# └── default/
# ├── converge.yml
# ├── molecule.yml
# ├── prepare.yml
# └── verify.yml
molecule/default/molecule.yml
---
dependency:
name: galaxy
options:
requirements-file: requirements.yml
driver:
name: docker
platforms:
- name: ubuntu20
image: geerlingguy/docker-ubuntu2004-ansible:latest
pre_build_image: true
privileged: true
command: /lib/systemd/systemd
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
- name: centos8
image: geerlingguy/docker-centos8-ansible:latest
pre_build_image: true
privileged: true
command: /usr/sbin/init
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
- name: debian11
image: geerlingguy/docker-debian11-ansible:latest
pre_build_image: true
privileged: true
command: /lib/systemd/systemd
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:ro
provisioner:
name: ansible
config_options:
defaults:
callbacks_enabled: profile_tasks, timer
inventory:
host_vars:
ubuntu20:
nginx_port: 8080
centos8:
nginx_port: 8081
verifier:
name: testinfra
options:
v: 1
scenario:
test_sequence:
- dependency
- cleanup
- destroy
- syntax
- create
- prepare
- converge
- idempotence
- side_effect
- verify
- cleanup
- destroy
molecule/default/converge.yml
---
- name: Converge
hosts: all
become: true
pre_tasks:
- name: Update apt cache
apt:
update_cache: yes
when: ansible_os_family == 'Debian'
changed_when: false
roles:
- role: webserver
nginx_port: "{{ nginx_port | default(80) }}"
worker_processes: 2
post_tasks:
- name: Wait for nginx to start
wait_for:
port: "{{ nginx_port | default(80) }}"
delay: 2
timeout: 30
molecule/default/verify.yml
---
- name: Verify
hosts: all
become: true
tasks:
- name: Check nginx is installed
package_facts:
manager: auto
- name: Verify nginx package
assert:
that:
- "'nginx' in ansible_facts.packages"
fail_msg: "Nginx is not installed"
- name: Check nginx service status
service_facts:
- name: Verify nginx is running
assert:
that:
- ansible_facts.services['nginx.service'].state == 'running'
fail_msg: "Nginx service is not running"
- name: Test HTTP response
uri:
url: "http://localhost:{{ nginx_port | default(80) }}"
return_content: yes
status_code: 200
register: http_response
- name: Verify HTTP content
assert:
that:
- http_response.status == 200
fail_msg: "HTTP response not successful"
- name: Check nginx configuration syntax
command: nginx -t
register: nginx_test
changed_when: false
failed_when: false
- name: Verify nginx config is valid
assert:
that:
- nginx_test.rc == 0
fail_msg: "Nginx configuration is invalid"
- name: Check nginx process
shell: ps aux | grep -v grep | grep nginx
register: nginx_process
changed_when: false
- name: Verify nginx process exists
assert:
that:
- nginx_process.rc == 0
fail_msg: "Nginx process not found"
Python Testinfra Tests
# molecule/default/tests/test_default.py
import os
import testinfra.utils.ansible_runner
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
os.environ['MOLECULE_INVENTORY_FILE']
).get_hosts('all')
def test_nginx_package_installed(host):
"""Verify nginx package is installed"""
nginx = host.package('nginx')
assert nginx.is_installed
def test_nginx_service_running(host):
"""Verify nginx service is running and enabled"""
nginx = host.service('nginx')
assert nginx.is_running
assert nginx.is_enabled
def test_nginx_config_file_exists(host):
"""Verify nginx configuration file exists"""
config = host.file('/etc/nginx/nginx.conf')
assert config.exists
assert config.is_file
assert config.user == 'root'
assert config.group == 'root'
def test_nginx_listening_on_port(host):
"""Verify nginx is listening on configured port"""
# Get port from ansible variables
port = host.ansible.get_variables().get('nginx_port', 80)
assert host.socket(f'tcp://0.0.0.0:{port}').is_listening
def test_nginx_http_response(host):
"""Verify nginx returns HTTP 200"""
port = host.ansible.get_variables().get('nginx_port', 80)
cmd = host.run(f'curl -s -o /dev/null -w "%{{http_code}}" http://localhost:{port}')
assert cmd.stdout.strip() == '200'
def test_nginx_user_exists(host):
"""Verify nginx user exists"""
user = host.user('www-data')
assert user.exists
def test_nginx_directories_exist(host):
"""Verify required directories exist"""
directories = [
'/var/www/html',
'/var/log/nginx',
]
for directory in directories:
dir_check = host.file(directory)
assert dir_check.exists
assert dir_check.is_directory
def test_nginx_process_count(host):
"""Verify nginx has multiple worker processes"""
processes = host.process.filter(comm='nginx')
# Should have master + worker processes
assert len(processes) >= 2
Running Molecule Tests
# Full test sequence
molecule test
# Individual test stages
molecule create # Create test instances
molecule converge # Run the role
molecule idempotence # Test idempotence
molecule verify # Run verification tests
molecule destroy # Destroy test instances
# Test on specific platform
molecule test --platform-name ubuntu20
# Debug mode
molecule --debug test
# Keep instances for debugging
molecule converge
molecule verify
# Debug...
molecule destroy
Publishing Roles to Ansible Galaxy
Prepare Role for Publishing
# 1. Create comprehensive README.md
# README.md
# Ansible Role: webserver
Installs and configures Nginx web server.
## Requirements
- Ansible 2.9+
- Supported platforms: Ubuntu 20.04+, CentOS 8+, Debian 11+
## Role Variables
Available variables with default values (see `defaults/main.yml`):
```yaml
nginx_port: 80
nginx_user: www-data
worker_processes: auto
worker_connections: 1024
```
## Dependencies
None.
## Example Playbook
```yaml
- hosts: webservers
roles:
- role: username.webserver
nginx_port: 8080
worker_processes: 4
```
## License
MIT
## Author Information
Created by [Your Name](https://github.com/username)
Complete meta/main.yml for Galaxy
---
galaxy_info:
role_name: webserver
namespace: username
author: Your Name
description: Production-ready Nginx web server role
company: Your Company
license: MIT
min_ansible_version: "2.9"
platforms:
- name: Ubuntu
versions:
- focal
- jammy
- name: EL
versions:
- "8"
- "9"
- name: Debian
versions:
- bullseye
- bookworm
galaxy_tags:
- web
- nginx
- webserver
- http
- https
- ssl
- proxy
- loadbalancer
# GitHub repository info
issue_tracker_url: https://github.com/username/ansible-role-webserver/issues
documentation: https://github.com/username/ansible-role-webserver/blob/main/README.md
dependencies: []
# Optional: Role allows duplicate execution
allow_duplicates: false
Publishing Workflow
# 1. Initialize Git repository
git init
git add .
git commit -m "Initial commit"
# 2. Create GitHub repository
# Create repo at github.com/username/ansible-role-webserver
# 3. Push to GitHub
git remote add origin https://github.com/username/ansible-role-webserver.git
git push -u origin main
# 4. Create Git tag for version
git tag 1.0.0
git push origin 1.0.0
# 5. Import to Galaxy
# Visit: https://galaxy.ansible.com/
# Sign in with GitHub
# Go to "My Content" > "Add Content"
# Select your repository
# Or use CLI
ansible-galaxy role import username ansible-role-webserver
# 6. Verify import
ansible-galaxy info username.webserver
Automated Publishing with GitHub Actions
# .github/workflows/release.yml
---
name: Release to Galaxy
on:
push:
tags:
- '*'
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install Ansible
run: pip install ansible
- name: Run Molecule tests
run: |
pip install molecule molecule-docker pytest-testinfra
molecule test
- name: Import role to Galaxy
run: |
ansible-galaxy role import \
--api-key ${{ secrets.GALAXY_API_KEY }} \
${{ github.repository_owner }} \
$(echo ${{ github.repository }} | cut -d'/' -f2)
Creating Ansible Collections
Collection Structure
my_namespace/
└── my_collection/
├── galaxy.yml # Collection metadata
├── README.md
├── plugins/
│ ├── modules/ # Custom modules
│ │ └── my_module.py
│ ├── inventory/ # Inventory plugins
│ ├── filter/ # Filter plugins
│ └── lookup/ # Lookup plugins
├── roles/
│ ├── role1/
│ └── role2/
├── playbooks/ # Example playbooks
│ └── example.yml
├── docs/ # Documentation
└── tests/ # Collection tests
galaxy.yml
---
namespace: my_namespace
name: my_collection
version: 1.0.0
readme: README.md
authors:
- Your Name
description: Comprehensive collection for application deployment
license:
- MIT
license_file: LICENSE
tags:
- infrastructure
- deployment
- automation
dependencies:
community.general: ">=5.0.0"
ansible.posix: ">=1.3.0"
repository: https://github.com/my_namespace/ansible-collection-my_collection
documentation: https://docs.example.com/my_collection
homepage: https://example.com/my_collection
issues: https://github.com/my_namespace/ansible-collection-my_collection/issues
build_ignore:
- .git
- .gitignore
- .travis.yml
- "*.tar.gz"
Building and Publishing Collection
# Build collection
ansible-galaxy collection build
# Install locally for testing
ansible-galaxy collection install my_namespace-my_collection-1.0.0.tar.gz
# Publish to Galaxy
ansible-galaxy collection publish my_namespace-my_collection-1.0.0.tar.gz \
--api-key=YOUR_API_KEY
# Install published collection
ansible-galaxy collection install my_namespace.my_collection
Advanced Role Optimization
Role Caching and Performance
---
# roles/webserver/tasks/main.yml
# Cache expensive operations
- name: Check if nginx is already configured
stat:
path: /etc/nginx/.ansible_configured
register: nginx_configured
- name: Complex configuration tasks
block:
- name: Download and install custom modules
get_url:
url: "{{ item }}"
dest: /etc/nginx/modules/
loop: "{{ nginx_modules }}"
- name: Compile custom configurations
shell: /usr/local/bin/generate_config.sh
- name: Mark as configured
file:
path: /etc/nginx/.ansible_configured
state: touch
when: not nginx_configured.stat.exists
# Use facts caching
- name: Set role facts
set_fact:
webserver_installed: true
webserver_version: "{{ nginx_version.stdout }}"
cacheable: yes
Parallel Role Execution
---
# Deploy to multiple environments in parallel
- name: Deploy to all environments
hosts: localhost
gather_facts: no
tasks:
- name: Deploy to development (async)
include_role:
name: application
vars:
environment: development
app_servers: "{{ groups['dev_servers'] }}"
async: 1800
poll: 0
register: dev_deploy
- name: Deploy to staging (async)
include_role:
name: application
vars:
environment: staging
app_servers: "{{ groups['staging_servers'] }}"
async: 1800
poll: 0
register: staging_deploy
- name: Wait for development deployment
async_status:
jid: "{{ dev_deploy.ansible_job_id }}"
register: dev_result
until: dev_result.finished
retries: 180
delay: 10
- name: Wait for staging deployment
async_status:
jid: "{{ staging_deploy.ansible_job_id }}"
register: staging_result
until: staging_result.finished
retries: 180
delay: 10
Role CI/CD Integration
Complete GitHub Actions Workflow
# .github/workflows/ci.yml
---
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
pip install ansible ansible-lint yamllint
- name: Run ansible-lint
run: ansible-lint .
- name: Run yamllint
run: yamllint .
molecule:
runs-on: ubuntu-latest
strategy:
matrix:
distro:
- ubuntu2004
- ubuntu2204
- centos8
- debian11
ansible:
- '2.12'
- '2.13'
- '2.14'
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.10'
- name: Install dependencies
run: |
pip install molecule molecule-docker pytest-testinfra
pip install ansible==${{ matrix.ansible }}.*
- name: Run Molecule tests
run: molecule test
env:
PY_COLORS: '1'
ANSIBLE_FORCE_COLOR: '1'
MOLECULE_DISTRO: ${{ matrix.distro }}
security:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Run security scan
uses: ansible/ansible-scan-action@main
with:
path: .
documentation:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Generate documentation
run: |
pip install ansible-doc-extractor
ansible-doc-extractor --output docs/ roles/
- name: Deploy documentation
if: github.ref == 'refs/heads/main'
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./docs
GitLab CI Example
# .gitlab-ci.yml
---
stages:
- lint
- test
- deploy
variables:
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
cache:
paths:
- .cache/pip
lint:
stage: lint
image: python:3.10
before_script:
- pip install ansible ansible-lint yamllint
script:
- ansible-lint .
- yamllint .
molecule:ubuntu:
stage: test
image: python:3.10
services:
- docker:dind
variables:
DOCKER_HOST: tcp://docker:2375
MOLECULE_DISTRO: ubuntu2004
before_script:
- pip install molecule molecule-docker pytest-testinfra
script:
- molecule test
only:
- branches
deploy:galaxy:
stage: deploy
image: python:3.10
before_script:
- pip install ansible
script:
- |
ansible-galaxy role import \
--api-key $GALAXY_API_KEY \
$CI_PROJECT_NAMESPACE \
$CI_PROJECT_NAME
only:
- tags
Best Practices
- Single purpose: Each role should do one thing well
- Default values: Use defaults/main.yml for all configurable parameters
- Documentation: Comprehensive README.md with examples
- Idempotency: Ensure roles can be run multiple times safely
- Testing: Use Molecule for multi-platform testing
- Dependencies: Declare all dependencies explicitly in meta/main.yml
- Variable prefixes: Use role-specific prefixes to avoid conflicts
- Handlers: Use handlers for service restarts and reloads
- Version control: Keep roles in separate git repositories
- Galaxy: Leverage and contribute to Ansible Galaxy
- Collections: Group related roles in collections
- CI/CD: Automate testing and publishing
- Security: Never hardcode secrets, use Ansible Vault
- Platform support: Test on all supported platforms
- Semantic versioning: Use proper version tags
Next Steps
- Learn about Ansible Vault for securing role variables
- Explore Advanced Topics for custom plugins
- Try role examples in the Playground
- Browse Ansible Galaxy for community roles