Documentation/Buki/Ansible/ skills /ansible-roles

📖 ansible-roles

Use when structuring and reusing code with Ansible roles for modular, maintainable automation and configuration management.



Overview

Structure and reuse automation code with Ansible roles for modular, maintainable infrastructure.

Role Directory Structure

A well-organized Ansible role follows a standardized directory structure:

roles/
└── webserver/
    ├── README.md
    ├── defaults/
    │   └── main.yml
    ├── files/
    │   ├── nginx.conf
    │   └── ssl/
    │       ├── cert.pem
    │       └── key.pem
    ├── handlers/
    │   └── main.yml
    ├── meta/
    │   └── main.yml
    ├── tasks/
    │   ├── main.yml
    │   ├── install.yml
    │   ├── configure.yml
    │   └── security.yml
    ├── templates/
    │   ├── nginx.conf.j2
    │   └── site.conf.j2
    ├── tests/
    │   ├── inventory
    │   └── test.yml
    └── vars/
        └── main.yml

Basic Role Example

tasks/main.yml

---
# Main task file for webserver role
- name: Include OS-specific variables
  include_vars: "{{ ansible_os_family }}.yml"

- name: Import installation tasks
  import_tasks: install.yml
  tags:
    - install
    - webserver

- name: Import configuration tasks
  import_tasks: configure.yml
  tags:
    - configure
    - webserver

- name: Import security tasks
  import_tasks: security.yml
  tags:
    - security
    - webserver

- name: Ensure nginx is running
  service:
    name: "{{ nginx_service_name }}"
    state: started
    enabled: yes
  tags:
    - service
    - webserver

tasks/install.yml

---
# Installation tasks for webserver role
- name: Install nginx and dependencies (Debian/Ubuntu)
  apt:
    name:
      - nginx
      - nginx-extras
      - python3-passlib
    state: present
    update_cache: yes
    cache_valid_time: 3600
  when: ansible_os_family == "Debian"

- name: Install nginx and dependencies (RedHat/CentOS)
  yum:
    name:
      - nginx
      - nginx-mod-stream
      - python3-passlib
    state: present
    update_cache: yes
  when: ansible_os_family == "RedHat"

- name: Create nginx directories
  file:
    path: "{{ item }}"
    state: directory
    owner: "{{ nginx_user }}"
    group: "{{ nginx_group }}"
    mode: '0755'
  loop:
    - "{{ nginx_conf_dir }}/sites-available"
    - "{{ nginx_conf_dir }}/sites-enabled"
    - "{{ nginx_log_dir }}"
    - "{{ nginx_cache_dir }}"
    - /var/www/html

- name: Install certbot for SSL
  apt:
    name: certbot
    state: present
  when:
    - nginx_ssl_enabled
    - ansible_os_family == "Debian"

tasks/configure.yml

---
# Configuration tasks for webserver role
- name: Deploy main nginx configuration
  template:
    src: nginx.conf.j2
    dest: "{{ nginx_conf_dir }}/nginx.conf"
    owner: root
    group: root
    mode: '0644'
    validate: 'nginx -t -c %s'
    backup: yes
  notify:
    - Reload nginx
  tags:
    - config

- name: Deploy site configurations
  template:
    src: site.conf.j2
    dest: "{{ nginx_conf_dir }}/sites-available/{{ item.name }}.conf"
    owner: root
    group: root
    mode: '0644'
    validate: 'nginx -t -c {{ nginx_conf_dir }}/nginx.conf'
  loop: "{{ nginx_sites }}"
  when: nginx_sites is defined
  notify:
    - Reload nginx

- name: Enable sites
  file:
    src: "{{ nginx_conf_dir }}/sites-available/{{ item.name }}.conf"
    dest: "{{ nginx_conf_dir }}/sites-enabled/{{ item.name }}.conf"
    state: link
  loop: "{{ nginx_sites }}"
  when:
    - nginx_sites is defined
    - item.enabled | default(true)
  notify:
    - Reload nginx

- name: Disable default site
  file:
    path: "{{ nginx_conf_dir }}/sites-enabled/default"
    state: absent
  when: nginx_disable_default_site
  notify:
    - Reload nginx

- name: Configure log rotation
  template:
    src: logrotate.j2
    dest: /etc/logrotate.d/nginx
    owner: root
    group: root
    mode: '0644'

tasks/security.yml

---
# Security tasks for webserver role
- name: Generate dhparam file
  command: openssl dhparam -out {{ nginx_conf_dir }}/dhparam.pem 2048
  args:
    creates: "{{ nginx_conf_dir }}/dhparam.pem"
  when: nginx_ssl_enabled

- name: Set secure permissions on dhparam
  file:
    path: "{{ nginx_conf_dir }}/dhparam.pem"
    owner: root
    group: root
    mode: '0600'
  when: nginx_ssl_enabled

- name: Configure firewall rules (ufw)
  ufw:
    rule: allow
    port: "{{ item }}"
    proto: tcp
  loop:
    - "80"
    - "443"
  when:
    - nginx_configure_firewall
    - ansible_os_family == "Debian"

- name: Configure firewall rules (firewalld)
  firewalld:
    service: "{{ item }}"
    permanent: yes
    state: enabled
    immediate: yes
  loop:
    - http
    - https
  when:
    - nginx_configure_firewall
    - ansible_os_family == "RedHat"

- name: Create basic auth file
  htpasswd:
    path: "{{ nginx_conf_dir }}/.htpasswd"
    name: "{{ item.username }}"
    password: "{{ item.password }}"
    owner: root
    group: "{{ nginx_group }}"
    mode: '0640'
  loop: "{{ nginx_basic_auth_users }}"
  when: nginx_basic_auth_users is defined
  no_log: yes

Role Variables

defaults/main.yml

---
# Default variables for webserver role
# These can be overridden in playbooks or inventory

# Package and service names
nginx_package_name: nginx
nginx_service_name: nginx

# User and group
nginx_user: www-data
nginx_group: www-data

# Directories
nginx_conf_dir: /etc/nginx
nginx_log_dir: /var/log/nginx
nginx_cache_dir: /var/cache/nginx
nginx_pid_file: /var/run/nginx.pid

# Main configuration
nginx_worker_processes: auto
nginx_worker_connections: 1024
nginx_keepalive_timeout: 65
nginx_client_max_body_size: 10m

# Performance tuning
nginx_sendfile: on
nginx_tcp_nopush: on
nginx_tcp_nodelay: on
nginx_gzip: on
nginx_gzip_types:
  - text/plain
  - text/css
  - application/json
  - application/javascript
  - text/xml
  - application/xml

# Security
nginx_ssl_enabled: no
nginx_ssl_protocols: "TLSv1.2 TLSv1.3"
nginx_ssl_ciphers: "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256"
nginx_ssl_prefer_server_ciphers: on
nginx_disable_default_site: yes
nginx_configure_firewall: yes
nginx_server_tokens: off

# Sites configuration
nginx_sites: []
# Example:
# nginx_sites:
#   - name: example.com
#     server_name: example.com www.example.com
#     root: /var/www/example.com
#     enabled: yes
#     ssl: yes

# Basic authentication
# nginx_basic_auth_users:
#   - username: admin
#     password: secretpassword

vars/main.yml

---
# Variables that should not be overridden
nginx_conf_path: "{{ nginx_conf_dir }}/nginx.conf"
nginx_error_log: "{{ nginx_log_dir }}/error.log"
nginx_access_log: "{{ nginx_log_dir }}/access.log"

# OS-specific overrides loaded via include_vars

vars/Debian.yml

---
nginx_user: www-data
nginx_group: www-data
nginx_conf_dir: /etc/nginx
nginx_service_name: nginx

vars/RedHat.yml

---
nginx_user: nginx
nginx_group: nginx
nginx_conf_dir: /etc/nginx
nginx_service_name: nginx

Role Handlers

handlers/main.yml

---
# Handlers for webserver role
- name: Reload nginx
  service:
    name: "{{ nginx_service_name }}"
    state: reloaded
  listen: "reload nginx"

- name: Restart nginx
  service:
    name: "{{ nginx_service_name }}"
    state: restarted
  listen: "restart nginx"

- name: Validate nginx config
  command: nginx -t
  changed_when: false
  listen: "validate nginx"

- name: Reload firewall
  service:
    name: ufw
    state: reloaded
  when: ansible_os_family == "Debian"
  listen: "reload firewall"

Role Templates

templates/nginx.conf.j2

# {{ ansible_managed }}
user {{ nginx_user }};
worker_processes {{ nginx_worker_processes }};
pid {{ nginx_pid_file }};

events {
    worker_connections {{ nginx_worker_connections }};
    use epoll;
    multi_accept on;
}

http {
    ##
    # Basic Settings
    ##
    sendfile {{ nginx_sendfile | ternary('on', 'off') }};
    tcp_nopush {{ nginx_tcp_nopush | ternary('on', 'off') }};
    tcp_nodelay {{ nginx_tcp_nodelay | ternary('on', 'off') }};
    keepalive_timeout {{ nginx_keepalive_timeout }};
    types_hash_max_size 2048;
    server_tokens {{ nginx_server_tokens | ternary('on', 'off') }};
    client_max_body_size {{ nginx_client_max_body_size }};

    include {{ nginx_conf_dir }}/mime.types;
    default_type application/octet-stream;

    ##
    # SSL Settings
    ##
{% if nginx_ssl_enabled %}
    ssl_protocols {{ nginx_ssl_protocols }};
    ssl_ciphers {{ nginx_ssl_ciphers }};
    ssl_prefer_server_ciphers {{ nginx_ssl_prefer_server_ciphers | ternary('on', 'off') }};
    ssl_dhparam {{ nginx_conf_dir }}/dhparam.pem;
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
{% endif %}

    ##
    # Logging Settings
    ##
    access_log {{ nginx_access_log }};
    error_log {{ nginx_error_log }};

    ##
    # Gzip Settings
    ##
    gzip {{ nginx_gzip | ternary('on', 'off') }};
{% if nginx_gzip %}
    gzip_vary on;
    gzip_proxied any;
    gzip_comp_level 6;
    gzip_types {{ nginx_gzip_types | join(' ') }};
{% endif %}

    ##
    # Virtual Host Configs
    ##
    include {{ nginx_conf_dir }}/sites-enabled/*;
}

templates/site.conf.j2

# {{ ansible_managed }}
# Site: {{ item.name }}

{% if item.ssl | default(false) %}
# Redirect HTTP to HTTPS
server {
    listen 80;
    listen [::]:80;
    server_name {{ item.server_name }};
    return 301 https://$server_name$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;
    server_name {{ item.server_name }};

    ssl_certificate {{ item.ssl_cert | default('/etc/letsencrypt/live/' + item.name + '/fullchain.pem') }};
    ssl_certificate_key {{ item.ssl_key | default('/etc/letsencrypt/live/' + item.name + '/privkey.pem') }};
{% else %}
server {
    listen 80;
    listen [::]:80;
    server_name {{ item.server_name }};
{% endif %}

    root {{ item.root | default('/var/www/' + item.name) }};
    index {{ item.index | default('index.html index.htm') }};

{% if item.access_log is defined %}
    access_log {{ item.access_log }};
{% endif %}
{% if item.error_log is defined %}
    error_log {{ item.error_log }};
{% endif %}

{% if item.basic_auth | default(false) %}
    auth_basic "Restricted Access";
    auth_basic_user_file {{ nginx_conf_dir }}/.htpasswd;
{% endif %}

    location / {
{% if item.proxy_pass is defined %}
        proxy_pass {{ item.proxy_pass }};
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
{% else %}
        try_files $uri $uri/ =404;
{% endif %}
    }

{% if item.locations is defined %}
{% for location in item.locations %}
    location {{ location.path }} {
{% if location.proxy_pass is defined %}
        proxy_pass {{ location.proxy_pass }};
{% endif %}
{% if location.alias is defined %}
        alias {{ location.alias }};
{% endif %}
{% if location.return is defined %}
        return {{ location.return }};
{% endif %}
    }
{% endfor %}
{% endif %}

    # Security headers
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-XSS-Protection "1; mode=block" always;
}

Role Dependencies

meta/main.yml

---
galaxy_info:
  role_name: webserver
  author: Your Organization
  description: Install and configure nginx web server
  company: Your Company
  license: MIT
  min_ansible_version: 2.9

  platforms:
    - name: Ubuntu
      versions:
        - focal
        - jammy
    - name: Debian
      versions:
        - buster
        - bullseye
    - name: EL
      versions:
        - 7
        - 8
        - 9

  galaxy_tags:
    - web
    - nginx
    - webserver
    - http

dependencies:
  - role: common
    vars:
      common_packages:
        - curl
        - wget

  - role: firewall
    when: nginx_configure_firewall

allow_duplicates: no

Using Roles in Playbooks

Simple Role Usage

---
- name: Configure web servers
  hosts: webservers
  become: yes

  roles:
    - webserver

Role with Variables

---
- name: Configure web servers with custom settings
  hosts: webservers
  become: yes

  roles:
    - role: webserver
      vars:
        nginx_worker_processes: 4
        nginx_ssl_enabled: yes
        nginx_sites:
          - name: example.com
            server_name: example.com www.example.com
            root: /var/www/example.com
            ssl: yes
            ssl_cert: /etc/ssl/certs/example.com.crt
            ssl_key: /etc/ssl/private/example.com.key

Role with Tags

---
- name: Configure infrastructure
  hosts: all
  become: yes

  roles:
    - role: webserver
      tags:
        - web
        - nginx

    - role: database
      tags:
        - db
        - postgres

Pre and Post Tasks

---
- name: Deploy web application
  hosts: webservers
  become: yes

  pre_tasks:
    - name: Announce deployment
      debug:
        msg: "Starting deployment to {{ inventory_hostname }}"

    - name: Check disk space
      command: df -h /
      register: disk_space
      changed_when: false

  roles:
    - webserver
    - monitoring

  post_tasks:
    - name: Verify nginx is responding
      uri:
        url: http://localhost
        status_code: 200
      retries: 3
      delay: 5

    - name: Notify completion
      debug:
        msg: "Deployment completed successfully"

Role Inclusion Methods

Static Import

---
- name: Configure servers
  hosts: all

  tasks:
    - name: Import webserver role
      import_role:
        name: webserver
      vars:
        nginx_worker_processes: 2
      tags:
        - webserver

Dynamic Include

---
- name: Configure servers based on conditions
  hosts: all

  tasks:
    - name: Include webserver role for web servers
      include_role:
        name: webserver
      when: "'webservers' in group_names"

    - name: Include database role for db servers
      include_role:
        name: database
      when: "'databases' in group_names"

Import with Task Files

---
- name: Custom installation workflow
  hosts: webservers

  tasks:
    - name: Run pre-installation checks
      import_role:
        name: webserver
        tasks_from: preflight

    - name: Install nginx
      import_role:
        name: webserver
        tasks_from: install

    - name: Configure nginx
      import_role:
        name: webserver
        tasks_from: configure

Creating Roles with Ansible Galaxy

Initialize a New Role

# Create role structure
ansible-galaxy init roles/myapp

# Create role with custom template
ansible-galaxy init --init-path roles/ myapp

# List role files
tree roles/myapp

Install Roles from Galaxy

# Install a role
ansible-galaxy install geerlingguy.nginx

# Install specific version
ansible-galaxy install geerlingguy.nginx,2.8.0

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

requirements.yml

---
# Install from Ansible Galaxy
- name: geerlingguy.nginx
  version: 2.8.0

- name: geerlingguy.postgresql
  version: 3.4.0

# Install from Git repository
- name: custom-app
  src: https://github.com/yourorg/ansible-role-custom-app.git
  scm: git
  version: main

# Install from local path
- name: internal-role
  src: /path/to/roles/internal-role

Advanced Role Patterns

Role with Multiple Entry Points

# roles/webserver/tasks/main.yml
---
- name: Default task flow
  import_tasks: "{{ webserver_task_flow | default('standard') }}.yml"
# roles/webserver/tasks/standard.yml
---
- import_tasks: install.yml
- import_tasks: configure.yml
- import_tasks: security.yml
# roles/webserver/tasks/minimal.yml
---
- import_tasks: install.yml
- import_tasks: configure.yml

Conditional Role Execution

---
- name: Configure servers with conditional roles
  hosts: all
  become: yes

  roles:
    - role: webserver
      when:
        - ansible_os_family == "Debian"
        - inventory_hostname in groups['webservers']

    - role: webserver-nginx
      when: webserver_type == "nginx"

    - role: webserver-apache
      when: webserver_type == "apache"

Nested Role Dependencies

# roles/application/meta/main.yml
---
dependencies:
  - role: webserver
    vars:
      nginx_sites:
        - name: "{{ app_name }}"
          server_name: "{{ app_domain }}"
          proxy_pass: "http://localhost:{{ app_port }}"

  - role: database
    vars:
      db_name: "{{ app_db_name }}"
      db_user: "{{ app_db_user }}"

  - role: monitoring
    vars:
      monitor_services:
        - nginx
        - "{{ app_name }}"

When to Use This Skill

Use the ansible-roles skill when you need to:

  • Structure reusable automation code across multiple playbooks and projects
  • Implement modular infrastructure as code with clear separation of concerns
  • Share automation logic between teams or projects
  • Create distributable automation packages for common infrastructure patterns
  • Organize complex playbooks into manageable, testable components
  • Implement role-based configuration management with variable precedence
  • Build layered infrastructure with role dependencies
  • Version control automation logic independently from playbooks
  • Create standardized infrastructure components for consistency
  • Implement security and compliance requirements through reusable roles
  • Build internal automation libraries for your organization
  • Contribute to or consume community automation from Ansible Galaxy
  • Test infrastructure components in isolation before integration
  • Implement different configurations for development, staging, and production
  • Create self-documenting infrastructure through role metadata

Best Practices

  1. Follow standard directory structure - Use ansible-galaxy init to create roles with proper organization
  2. Use defaults wisely - Place overridable variables in defaults/main.yml, non-overridable in vars/main.yml
  3. Document thoroughly - Include comprehensive README.md with usage examples and variable documentation
  4. Keep roles focused - Each role should have a single, well-defined purpose
  5. Use role dependencies - Declare dependencies in meta/main.yml rather than in playbooks
  6. Tag appropriately - Apply meaningful tags to tasks for selective execution
  7. Implement idempotency - Ensure roles can be run multiple times safely
  8. Version your roles - Use semantic versioning for role releases
  9. Test roles independently - Include test playbooks in tests/ directory
  10. Use templates for configuration - Prefer Jinja2 templates over static files for flexibility
  11. Implement OS detection - Use ansible_os_family for cross-platform compatibility
  12. Secure sensitive data - Use ansible-vault for passwords and secrets in role variables
  13. Use handlers correctly - Only notify handlers when configuration changes
  14. Validate configurations - Use validate parameter in template/copy modules
  15. Name tasks clearly - Use descriptive names that explain what each task does

Common Pitfalls

  1. Overly complex roles - Trying to make one role do too many things
  2. Poor variable naming - Using generic names that conflict with other roles
  3. Missing role prefix - Not prefixing role variables with role name
  4. Ignoring variable precedence - Not understanding how Ansible resolves variable conflicts
  5. Hard-coded values - Embedding environment-specific values instead of using variables
  6. Missing dependencies - Not declaring role dependencies in meta/main.yml
  7. No validation - Deploying configurations without validation checks
  8. Skipping tests - Not including test playbooks or scenarios
  9. Poor handler design - Restarting services unnecessarily or not at all
  10. Missing OS support - Assuming all target systems are the same
  11. No backup strategy - Not backing up configurations before changes
  12. Ignoring idempotency - Using command/shell modules without proper guards
  13. Missing tags - Not tagging tasks for selective execution
  14. Poor template practices - Complex logic in templates instead of variables
  15. No version control - Not versioning roles or tracking changes

Resources