Roles & Modules
Roles provide a framework for organizing playbooks into reusable, portable units of automation. Modules are the building blocks — pre-built functions that perform specific tasks on managed nodes.
Ansible Roles
Role Directory Structure
roles/
└── nginx/ # Role name
├── defaults/
│ └── main.yml # Default variables (lowest priority)
├── vars/
│ └── main.yml # Role variables (higher priority)
├── tasks/
│ ├── main.yml # Main task file (entry point)
│ ├── install.yml # Sub-task file
│ └── configure.yml # Sub-task file
├── handlers/
│ └── main.yml # Handlers for this role
├── templates/
│ └── nginx.conf.j2 # Jinja2 templates
├── files/
│ └── nginx.conf.d/ # Static files to copy
├── meta/
│ └── main.yml # Role metadata and dependencies
└── README.md # Role documentation
Create a Role
# Generate role skeleton
ansible-galaxy role init nginx
# Output:
# - nginx/
# - nginx/defaults/main.yml
# - nginx/handlers/main.yml
# - nginx/tasks/main.yml
# - nginx/templates/
# - nginx/files/
# - nginx/vars/main.yml
# - nginx/meta/main.yml
# - nginx/README.md
Role Files
defaults/main.yml
---
# Lowest priority — easily overridden by playbook or inventory vars
nginx_port: 80
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_server_name: "_"
nginx_root: /var/www/html
ssl_enabled: false
nginx_extra_configs: []
tasks/main.yml
---
- name: Import installation tasks
import_tasks: install.yml
tags: install
- name: Import configuration tasks
import_tasks: configure.yml
tags: configure
- name: Import SSL tasks
import_tasks: ssl.yml
when: ssl_enabled | bool
tags: ssl
tasks/install.yml
---
- name: Install Nginx (Debian/Ubuntu)
apt:
name: nginx
state: present
update_cache: true
when: ansible_os_family == "Debian"
- name: Install Nginx (RHEL/CentOS)
dnf:
name: nginx
state: present
when: ansible_os_family == "RedHat"
- name: Ensure Nginx is started and enabled
service:
name: nginx
state: started
enabled: true
tasks/configure.yml
---
- name: Deploy main Nginx configuration
template:
src: nginx.conf.j2
dest: /etc/nginx/nginx.conf
owner: root
group: root
mode: '0644'
validate: nginx -t -c %s
notify: Reload Nginx
- name: Remove default site
file:
path: /etc/nginx/sites-enabled/default
state: absent
notify: Reload Nginx
- name: Deploy virtual host configs
template:
src: vhost.conf.j2
dest: /etc/nginx/sites-available/{{ item.name }}.conf
loop: "{{ nginx_vhosts | default([]) }}"
notify: Reload Nginx
- name: Enable virtual hosts
file:
src: /etc/nginx/sites-available/{{ item.name }}.conf
dest: /etc/nginx/sites-enabled/{{ item.name }}.conf
state: link
loop: "{{ nginx_vhosts | default([]) }}"
notify: Reload Nginx
handlers/main.yml
---
- name: Start Nginx
service:
name: nginx
state: started
- name: Reload Nginx
service:
name: nginx
state: reloaded
- name: Restart Nginx
service:
name: nginx
state: restarted
meta/main.yml
---
galaxy_info:
author: lebinhphuong
description: Nginx web server role
license: MIT
min_ansible_version: "2.12"
platforms:
- name: Ubuntu
versions:
- "20.04"
- "22.04"
- name: EL
versions:
- "8"
- "9"
galaxy_tags:
- nginx
- web
- proxy
dependencies:
- role: common # This role depends on 'common' role
- role: firewall
vars:
firewall_allowed_ports: [80, 443]
Using Roles in a Playbook
---
- name: Configure web servers
hosts: webservers
become: true
roles:
# Simple role usage
- common
# Role with variables override
- role: nginx
vars:
nginx_port: 8080
ssl_enabled: true
nginx_vhosts:
- name: myapp
server_name: myapp.example.com
root: /var/www/myapp
# Conditional role
- role: datadog
when: monitoring_enabled | default(false)
# Mix tasks and roles
tasks:
- name: Final health check
uri:
url: "http://localhost:{{ nginx_port }}"
status_code: 200
Import vs Include
# import_tasks — static, processed at parse time
# Use when: tasks are always needed, supports tags properly
- import_tasks: configure.yml
# include_tasks — dynamic, processed at runtime
# Use when: tasks are conditional or in a loop
- include_tasks: "{{ ansible_os_family | lower }}.yml"
- include_tasks: install-pkg.yml
loop: "{{ packages }}"
loop_control:
loop_var: pkg_name
# import_role vs include_role — same principle
- import_role:
name: nginx
- include_role:
name: "{{ role_name }}"
when: install_role | bool
Common Built-in Modules
File and Directory Management
# file — manage files, directories, symlinks
- name: Create directory
file:
path: /opt/myapp
state: directory # file | directory | link | absent | touch
owner: ubuntu
group: ubuntu
mode: '0755'
recurse: true # Apply permissions recursively
# copy — copy local files to remote
- name: Copy config file
copy:
src: files/app.conf
dest: /etc/myapp/app.conf
owner: root
group: root
mode: '0644'
backup: true # Backup existing file
# template — render Jinja2 template and copy
- name: Deploy config template
template:
src: app.conf.j2
dest: /etc/myapp/app.conf
# fetch — copy files from remote to control node
- name: Fetch log file
fetch:
src: /var/log/app/error.log
dest: /tmp/logs/{{ inventory_hostname }}/
# lineinfile — manage a line in a file
- name: Set max open files in limits.conf
lineinfile:
path: /etc/security/limits.conf
line: "ubuntu soft nofile 65536"
state: present
# blockinfile — insert/update a block of lines
- name: Add SSH config block
blockinfile:
path: /etc/ssh/sshd_config
marker: "# {mark} MANAGED BY ANSIBLE"
block: |
PasswordAuthentication no
PermitRootLogin no
MaxAuthTries 3
# stat — get file/directory status
- name: Check if file exists
stat:
path: /opt/myapp/current
register: app_link
- name: Run only if file exists
debug:
msg: "App is deployed"
when: app_link.stat.exists
Package Management
# apt — Debian/Ubuntu package management
- name: Install packages
apt:
name:
- nginx
- python3-pip
- git
state: present # present | latest | absent
update_cache: true
cache_valid_time: 3600 # Only update cache if older than 1 hour
# dnf — RHEL/CentOS/Fedora package management
- name: Install packages
dnf:
name:
- nginx
- python3
state: present
enablerepo: epel
# pip — Python package management
- name: Install Python packages
pip:
name:
- flask
- gunicorn
- psycopg2-binary
state: present
virtualenv: /opt/myapp/venv
virtualenv_python: python3
# package — generic package module (detects OS)
- name: Install curl (any OS)
package:
name: curl
state: present
Service Management
# service — manage services (SysV / systemd)
- name: Manage Nginx service
service:
name: nginx
state: started # started | stopped | restarted | reloaded
enabled: true # Start on boot
# systemd — manage systemd units
- name: Configure and start custom service
systemd:
name: myapp
state: started
enabled: true
daemon_reload: true # Reload systemd after unit file change
# Deploy a systemd unit file first
- name: Deploy systemd service file
template:
src: myapp.service.j2
dest: /etc/systemd/system/myapp.service
notify:
- Reload systemd
- Restart MyApp
User and Group Management
# user — manage users
- name: Create application user
user:
name: appuser
comment: "Application Service Account"
shell: /bin/bash
groups:
- www-data
- docker
home: /home/appuser
create_home: true
system: false
state: present
# group — manage groups
- name: Create application group
group:
name: appteam
gid: 5000
state: present
# authorized_key — manage SSH authorized keys
- name: Add SSH public key
authorized_key:
user: ubuntu
state: present
key: "{{ lookup('file', 'files/id_rsa.pub') }}"
Command and Shell Execution
# command — run commands without shell interpretation (safer)
- name: Run application command
command: /opt/myapp/bin/myapp --migrate
args:
chdir: /opt/myapp
creates: /opt/myapp/.migrated # Skip if file exists
# shell — run commands through shell (supports pipes, redirects)
- name: Get running containers
shell: docker ps --format '{% raw %}{{.Names}}{% endraw %}' | grep myapp
register: running_containers
changed_when: false
# script — run a local script on remote hosts
- name: Run initialization script
script: scripts/init.sh {{ app_version }}
args:
creates: /opt/myapp/.initialized
# raw — run raw SSH command (no Python needed)
- name: Bootstrap Python on minimal host
raw: apt-get install -y python3
changed_when: true
Network and URI Modules
# uri — interact with HTTP/HTTPS endpoints
- name: Check application health
uri:
url: "http://localhost:8080/health"
method: GET
status_code: 200
return_content: true
register: health_check
retries: 5
delay: 10
until: health_check.status == 200
- name: POST data to API
uri:
url: "https://api.example.com/deploy"
method: POST
headers:
Authorization: "Bearer {{ api_token }}"
Content-Type: "application/json"
body_format: json
body:
version: "{{ app_version }}"
environment: "{{ env }}"
status_code: [200, 201]
# get_url — download files from URL
- name: Download application binary
get_url:
url: "https://releases.example.com/myapp-{{ app_version }}.tar.gz"
dest: /tmp/myapp.tar.gz
checksum: "sha256:{{ app_checksum }}"
mode: '0644'
Cloud Modules
# AWS — EC2 instance management
- name: Launch EC2 instance
amazon.aws.ec2_instance:
name: my-web-server
key_name: my-key-pair
instance_type: t3.medium
image_id: ami-0c55b159cbfafe1f0
security_group: web-sg
vpc_subnet_id: subnet-12345678
region: ap-southeast-1
tags:
Environment: production
Role: webserver
state: running
# AWS — S3 operations
- name: Upload file to S3
amazon.aws.s3_object:
bucket: my-app-artifacts
object: "releases/myapp-{{ app_version }}.tar.gz"
src: /tmp/myapp.tar.gz
mode: put
region: ap-southeast-1
# GCP — Compute Engine
- name: Create GCP instance
google.cloud.gcp_compute_instance:
name: my-instance
machine_type: n1-standard-2
zone: asia-southeast1-a
project: my-gcp-project
auth_kind: serviceaccount
service_account_file: /etc/gcp-sa.json
state: present
Database Modules
# postgresql_db — manage PostgreSQL databases
- name: Create application database
community.postgresql.postgresql_db:
name: myapp_db
owner: myapp_user
encoding: UTF8
lc_collate: en_US.UTF-8
lc_ctype: en_US.UTF-8
state: present
# postgresql_user — manage PostgreSQL users
- name: Create database user
community.postgresql.postgresql_user:
name: myapp_user
password: "{{ db_password }}"
role_attr_flags: NOSUPERUSER,CREATEDB
state: present
# mysql_db — manage MySQL databases
- name: Create MySQL database
community.mysql.mysql_db:
name: myapp
encoding: utf8mb4
collation: utf8mb4_unicode_ci
state: present
login_host: "{{ db_host }}"
login_user: root
login_password: "{{ mysql_root_password }}"
Ansible Galaxy
Install Roles and Collections
# Install a role from Galaxy
ansible-galaxy role install geerlingguy.nginx
# Install a specific version
ansible-galaxy role install geerlingguy.nginx,3.1.0
# Install from requirements file
ansible-galaxy role install -r requirements.yml
# Install a collection
ansible-galaxy collection install community.general
ansible-galaxy collection install amazon.aws
# Install collections from requirements
ansible-galaxy collection install -r requirements.yml
# List installed roles
ansible-galaxy role list
# Remove a role
ansible-galaxy role remove geerlingguy.nginx
requirements.yml
---
# requirements.yml
roles:
- name: geerlingguy.nginx
version: "3.1.0"
- name: geerlingguy.postgresql
version: "3.3.0"
# From GitHub
- name: my-custom-role
src: https://github.com/myorg/ansible-role-custom
version: main
collections:
- name: community.general
version: ">=7.0.0"
- name: amazon.aws
version: "6.5.0"
- name: community.postgresql
version: "3.2.0"
Publish Your Own Role
# Login to Galaxy
ansible-galaxy login
# Import your role (from GitHub)
ansible-galaxy import myusername my-role-repo
# Or use the Galaxy API token
ansible-galaxy role import --token <token> myusername my-role-repo
Best Practices for Roles
✅ Do:
- Use
defaults/main.ymlfor all configurable variables with sane defaults - Keep roles focused on a single technology (nginx, postgresql, redis, etc.)
- Prefix role-specific variables with the role name (e.g.
nginx_port,nginx_user) - Write a clear
README.mdwith example usage and all available variables - Use
moleculefor testing roles locally before sharing - Tag tasks for selective execution (
install,configure,ssl)
⚠️ Avoid:
- Hardcoding values — use variables with defaults instead
- Using
shellorcommandwhen a dedicated module exists - Including sensitive data directly in role files — use Vault
- Making roles depend on host-specific facts without providing fallbacks
- Duplicating logic across roles — extract shared tasks into a
commonrole
Testing Roles with Molecule
# Install Molecule
pip3 install molecule molecule-docker
# Initialize Molecule in existing role
cd roles/nginx
molecule init scenario --driver-name docker
# Run tests
molecule test # Full test cycle (create, converge, verify, destroy)
molecule converge # Apply role on test container
molecule verify # Run tests against converged container
molecule lint # Lint playbooks and tasks
# molecule/default/molecule.yml
---
driver:
name: docker
platforms:
- name: ubuntu-22.04
image: ubuntu:22.04
pre_build_image: true
- name: rocky-9
image: rockylinux:9
pre_build_image: true
provisioner:
name: ansible
playbooks:
converge: converge.yml
verifier:
name: ansible
💡 Tip: Use
ansible-lint to catch common issues and enforce best practices before committing your playbooks and roles:
pip3 install ansible-lint
ansible-lint site.yml
ansible-lint roles/nginx/
Next Steps
- Playbooks - Go deeper into playbook features
- Introduction - Back to Ansible basics