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