Ansible Galaxy, Roles, And Collections

Last updated: July 1, 2025

My Journey from Monolithic Playbooks to Modular Magic

I still remember my first "successful" Ansible playbook – a monstrous 500-line YAML file that installed everything from scratch: web servers, databases, monitoring tools, backup systems, and security configurations. I was so proud that it worked, but maintaining it became a nightmare. Every time I needed to deploy a similar setup for a different environment, I had to copy the entire playbook and modify dozens of variables and tasks.

The real wake-up call came when a teammate asked if they could use my "web server setup" for their project. I realized I couldn't easily extract just the web server parts without breaking dependencies or copying irrelevant tasks. That's when my mentor introduced me to Ansible Roles – the concept that would revolutionize how I thought about automation architecture.

Then came Collections – Ansible's modern packaging system that took collaboration to the next level. What started as a frustrating inability to share my work became a journey into understanding how to build modular, reusable, and shareable automation components.

In this post, I'll share my evolution from writing monolithic playbooks to creating sophisticated, modular automation using Ansible Galaxy, Roles, and Collections across both Linux and Windows environments.

Understanding the Ecosystem: Galaxy, Roles, and Collections

Before diving into the technical details, let me explain how these three concepts work together:

Ansible Galaxy: The Marketplace

Ansible Galaxy is the community hub where automation content is shared and discovered. Think of it as the "GitHub" for Ansible content – a central repository where developers share roles, collections, and playbooks.

Ansible Roles: The Building Blocks

Ansible Roles are reusable automation components with a standardized directory structure. They package related tasks, variables, files, templates, and handlers into a portable unit that can be shared across projects.

Ansible Collections: The Modern Packages

Ansible Collections are the evolved packaging format that can contain multiple roles, modules, plugins, and playbooks in a single distributable unit. They're the modern way to package and share complex automation solutions.

Deep Dive into Ansible Roles

Role Directory Structure

Ansible roles follow a standardized directory structure that makes them predictable and easy to understand:

webserver/
├── tasks/
│   ├── main.yml
│   ├── redhat.yml
│   ├── debian.yml
│   └── windows.yml
├── handlers/
│   └── main.yml
├── templates/
│   ├── httpd.conf.j2
│   ├── nginx.conf.j2
│   └── iis.config.j2
├── files/
│   ├── ssl-cert.pem
│   └── security-policy.xml
├── vars/
│   ├── main.yml
│   └── secrets.yml
├── defaults/
│   └── main.yml
├── meta/
│   └── main.yml
└── README.md

Creating Your First Role

Let's create a comprehensive web server role that works across platforms:

# Create role directory structure
mkdir -p roles
cd roles
ansible-galaxy role init webserver

# This creates the complete directory structure
tree webserver/

Building a Cross-Platform Web Server Role

1. Main Tasks (tasks/main.yml)

---
# tasks/main.yml - Entry point for all tasks
- name: Include platform-specific variables
  include_vars: "{{ ansible_os_family | lower }}.yml"
  when: ansible_os_family is defined

- name: Install web server (Linux)
  include_tasks: "{{ ansible_os_family | lower }}.yml"
  when: ansible_os_family != 'Windows'

- name: Install web server (Windows)
  include_tasks: windows.yml
  when: ansible_os_family == 'Windows'

- name: Configure firewall
  include_tasks: firewall.yml

- name: Deploy application files
  include_tasks: deploy.yml

- name: Start and enable web server
  include_tasks: service.yml

2. Linux-Specific Tasks (tasks/redhat.yml)

---
# tasks/redhat.yml - Red Hat family specific tasks
- name: Install EPEL repository
  ansible.builtin.yum:
    name: epel-release
    state: present
  become: yes

- name: Install web server packages (RHEL/CentOS)
  ansible.builtin.yum:
    name: "{{ webserver_packages_redhat }}"
    state: present
  become: yes

- name: Install PHP and additional modules
  ansible.builtin.yum:
    name: "{{ php_packages_redhat }}"
    state: present
  become: yes
  when: enable_php | default(false)

- name: Configure SELinux for web server
  ansible.posix.seboolean:
    name: "{{ item }}"
    state: yes
    persistent: yes
  loop:
    - httpd_can_network_connect
    - httpd_can_network_relay
  become: yes
  when: ansible_selinux.status == "enabled"

3. Debian-Specific Tasks (tasks/debian.yml)

---
# tasks/debian.yml - Debian family specific tasks
- name: Update package cache
  ansible.builtin.apt:
    update_cache: yes
    cache_valid_time: 3600
  become: yes

- name: Install web server packages (Debian/Ubuntu)
  ansible.builtin.apt:
    name: "{{ webserver_packages_debian }}"
    state: present
  become: yes

- name: Install PHP and additional modules
  ansible.builtin.apt:
    name: "{{ php_packages_debian }}"
    state: present
  become: yes
  when: enable_php | default(false)

- name: Enable required Apache modules
  ansible.builtin.apache2_module:
    name: "{{ item }}"
    state: present
  loop:
    - rewrite
    - ssl
    - headers
  become: yes
  notify: restart apache

4. Windows-Specific Tasks (tasks/windows.yml)

---
# tasks/windows.yml - Windows specific tasks
- name: Install IIS feature
  ansible.windows.win_feature:
    name: "{{ iis_features }}"
    state: present
  register: iis_install

- name: Install additional IIS components
  ansible.windows.win_feature:
    name: "{{ item }}"
    state: present
  loop:
    - IIS-HttpRedirect
    - IIS-HttpLogging
    - IIS-LoggingLibraries
    - IIS-RequestMonitor
    - IIS-HttpTracing
    - IIS-Security
    - IIS-RequestFiltering
    - IIS-Performance
    - IIS-WebServerManagementTools
    - IIS-ManagementConsole
    - IIS-IIS6ManagementCompatibility

- name: Install ASP.NET features
  ansible.windows.win_feature:
    name: "{{ item }}"
    state: present
  loop:
    - IIS-NetFxExtensibility45
    - IIS-ISAPIExtensions
    - IIS-ISAPIFilter
    - IIS-ASPNET45
  when: enable_aspnet | default(false)

- name: Reboot if required
  ansible.windows.win_reboot:
  when: iis_install.reboot_required

5. Firewall Configuration (tasks/firewall.yml)

---
# tasks/firewall.yml - Cross-platform firewall configuration
- name: Configure firewall (Linux - firewalld)
  ansible.posix.firewalld:
    service: "{{ item }}"
    permanent: true
    state: enabled
    immediate: true
  loop:
    - http
    - https
  become: yes
  when: 
    - ansible_os_family != 'Windows'
    - ansible_facts['service_mgr'] == 'systemd'
  ignore_errors: yes

- name: Configure firewall (Linux - ufw)
  community.general.ufw:
    rule: allow
    port: "{{ item }}"
    proto: tcp
  loop:
    - "80"
    - "443"
  become: yes
  when: 
    - ansible_os_family == 'Debian'
    - ansible_facts.packages.ufw is defined
  ignore_errors: yes

- name: Configure Windows Firewall
  ansible.windows.win_firewall_rule:
    name: "{{ item.name }}"
    localport: "{{ item.port }}"
    action: allow
    direction: in
    protocol: tcp
    state: present
  loop:
    - { name: "HTTP Inbound", port: "80" }
    - { name: "HTTPS Inbound", port: "443" }
  when: ansible_os_family == 'Windows'

6. Application Deployment (tasks/deploy.yml)

---
# tasks/deploy.yml - Deploy application files and configuration
- name: Create web directories (Linux)
  ansible.builtin.file:
    path: "{{ item }}"
    state: directory
    owner: "{{ webserver_user }}"
    group: "{{ webserver_group }}"
    mode: '0755'
  loop:
    - "{{ document_root }}"
    - "{{ document_root }}/assets"
    - "{{ log_directory }}"
  become: yes
  when: ansible_os_family != 'Windows'

- name: Create web directories (Windows)
  ansible.windows.win_file:
    path: "{{ item }}"
    state: directory
  loop:
    - "{{ document_root }}"
    - "{{ document_root }}\\assets"
    - "{{ log_directory }}"
  when: ansible_os_family == 'Windows'

- name: Deploy index page template
  ansible.builtin.template:
    src: index.html.j2
    dest: "{{ document_root }}/index.html"
    owner: "{{ webserver_user }}"
    group: "{{ webserver_group }}"
    mode: '0644'
  become: yes
  when: ansible_os_family != 'Windows'

- name: Deploy index page template (Windows)
  ansible.windows.win_template:
    src: index.html.j2
    dest: "{{ document_root }}\\index.html"
  when: ansible_os_family == 'Windows'

- name: Deploy web server configuration
  ansible.builtin.template:
    src: "{{ webserver_config_template }}"
    dest: "{{ webserver_config_path }}"
    owner: root
    group: root
    mode: '0644'
    backup: yes
  become: yes
  when: ansible_os_family != 'Windows'
  notify: "restart {{ webserver_service }}"

- name: Deploy web server configuration (Windows)
  ansible.windows.win_template:
    src: web.config.j2
    dest: "{{ document_root }}\\web.config"
  when: ansible_os_family == 'Windows'
  notify: restart iis

- name: Copy static assets
  ansible.builtin.copy:
    src: "{{ item }}"
    dest: "{{ document_root }}/assets/"
    owner: "{{ webserver_user }}"
    group: "{{ webserver_group }}"
    mode: '0644'
  loop: "{{ static_assets | default([]) }}"
  become: yes
  when: 
    - ansible_os_family != 'Windows'
    - static_assets is defined

7. Service Management (tasks/service.yml)

---
# tasks/service.yml - Start and manage web server services
- name: Start and enable web server (Linux)
  ansible.builtin.systemd:
    name: "{{ webserver_service }}"
    state: started
    enabled: yes
    daemon_reload: yes
  become: yes
  when: ansible_os_family != 'Windows'

- name: Start and enable web server (Windows)
  ansible.windows.win_service:
    name: W3SVC
    state: started
    start_mode: auto
  when: ansible_os_family == 'Windows'

- name: Verify web server is responding
  ansible.builtin.uri:
    url: "http://{{ ansible_default_ipv4.address | default(ansible_all_ipv4_addresses[0]) }}:{{ webserver_port | default(80) }}/"
    method: GET
    status_code: 200
  register: web_check
  retries: 5
  delay: 10
  until: web_check is succeeded
  when: ansible_os_family != 'Windows'

- name: Verify web server is responding (Windows)
  ansible.builtin.uri:
    url: "http://{{ ansible_ip_addresses[0] }}:{{ webserver_port | default(80) }}/"
    method: GET
    status_code: 200
  register: web_check_win
  retries: 5
  delay: 10
  until: web_check_win is succeeded
  when: ansible_os_family == 'Windows'

8. Variable Definitions

defaults/main.yml - Default values that can be easily overridden:

---
# defaults/main.yml - Default variables for the webserver role
webserver_port: 80
webserver_ssl_port: 443
enable_ssl: false
enable_php: false
enable_aspnet: false

# Linux specific defaults
webserver_packages_redhat:
  - httpd
  - httpd-tools
  - mod_ssl

webserver_packages_debian:
  - apache2
  - apache2-utils

php_packages_redhat:
  - php
  - php-mysql
  - php-gd
  - php-curl

php_packages_debian:
  - php
  - php-mysql
  - php-gd
  - php-curl
  - libapache2-mod-php

# Windows specific defaults
iis_features:
  - IIS-WebServerRole
  - IIS-WebServer
  - IIS-CommonHttpFeatures
  - IIS-HttpRedirect
  - IIS-ApplicationDevelopment
  - IIS-NetFxExtensibility45
  - IIS-HealthAndDiagnostics
  - IIS-HttpLogging
  - IIS-Security
  - IIS-RequestFiltering
  - IIS-Performance
  - IIS-StaticContent
  - IIS-DefaultDocument
  - IIS-DirectoryBrowsing

# Cross-platform defaults
document_root: "{{ '/var/www/html' if ansible_os_family != 'Windows' else 'C:\\inetpub\\wwwroot' }}"
log_directory: "{{ '/var/log/httpd' if ansible_os_family == 'RedHat' else '/var/log/apache2' if ansible_os_family == 'Debian' else 'C:\\inetpub\\logs\\LogFiles' }}"

# Service and user defaults
webserver_service: "{{ 'httpd' if ansible_os_family == 'RedHat' else 'apache2' if ansible_os_family == 'Debian' else 'W3SVC' }}"
webserver_user: "{{ 'apache' if ansible_os_family == 'RedHat' else 'www-data' if ansible_os_family == 'Debian' else 'IIS_IUSRS' }}"
webserver_group: "{{ 'apache' if ansible_os_family == 'RedHat' else 'www-data' if ansible_os_family == 'Debian' else 'IIS_IUSRS' }}"

# Configuration templates
webserver_config_template: "{{ 'httpd.conf.j2' if ansible_os_family == 'RedHat' else 'apache2.conf.j2' if ansible_os_family == 'Debian' else 'web.config.j2' }}"
webserver_config_path: "{{ '/etc/httpd/conf/httpd.conf' if ansible_os_family == 'RedHat' else '/etc/apache2/apache2.conf' if ansible_os_family == 'Debian' else 'C:\\inetpub\\wwwroot\\web.config' }}"

vars/redhat.yml - Red Hat specific variables:

---
# vars/redhat.yml - Red Hat family specific variables
webserver_service: httpd
webserver_user: apache
webserver_group: apache
webserver_config_path: /etc/httpd/conf/httpd.conf
webserver_config_template: httpd.conf.j2
document_root: /var/www/html
log_directory: /var/log/httpd

vars/debian.yml - Debian specific variables:

---
# vars/debian.yml - Debian family specific variables  
webserver_service: apache2
webserver_user: www-data
webserver_group: www-data
webserver_config_path: /etc/apache2/apache2.conf
webserver_config_template: apache2.conf.j2
document_root: /var/www/html
log_directory: /var/log/apache2

vars/windows.yml - Windows specific variables:

---
# vars/windows.yml - Windows specific variables
webserver_service: W3SVC
webserver_user: IIS_IUSRS
webserver_group: IIS_IUSRS
document_root: C:\inetpub\wwwroot
log_directory: C:\inetpub\logs\LogFiles
webserver_config_template: web.config.j2

9. Handlers (handlers/main.yml)

---
# handlers/main.yml - Event-driven tasks
- name: restart httpd
  ansible.builtin.systemd:
    name: httpd
    state: restarted
  become: yes

- name: restart apache
  ansible.builtin.systemd:
    name: apache2
    state: restarted
  become: yes

- name: restart apache2
  ansible.builtin.systemd:
    name: apache2
    state: restarted
  become: yes

- name: restart iis
  ansible.windows.win_service:
    name: W3SVC
    state: restarted

- name: reload apache
  ansible.builtin.systemd:
    name: apache2
    state: reloaded
  become: yes

- name: reload httpd
  ansible.builtin.systemd:
    name: httpd
    state: reloaded
  become: yes

10. Role Metadata (meta/main.yml)

---
# meta/main.yml - Role metadata and dependencies
galaxy_info:
  author: "Your Name"
  description: "Cross-platform web server role supporting Apache and IIS"
  company: "Your Company"
  license: MIT
  min_ansible_version: "2.10"
  
  platforms:
    - name: EL
      versions:
        - 7
        - 8
        - 9
    - name: Ubuntu
      versions:
        - 18.04
        - 20.04
        - 22.04
    - name: Debian
      versions:
        - 10
        - 11
    - name: Windows
      versions:
        - 2016
        - 2019
        - 2022

  galaxy_tags:
    - web
    - apache
    - iis
    - webserver
    - crossplatform

dependencies:
  - role: common
    when: install_common_packages | default(true)

11. Templates

templates/index.html.j2:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>{{ site_title | default('Welcome to ' + inventory_hostname) }}</title>
    <style>
        body { 
            font-family: Arial, sans-serif; 
            margin: 40px; 
            background-color: #f4f4f4; 
        }
        .container { 
            background: white; 
            padding: 20px; 
            border-radius: 8px; 
            box-shadow: 0 2px 10px rgba(0,0,0,0.1); 
        }
        .server-info { 
            background: #e8f4fd; 
            padding: 15px; 
            border-radius: 5px; 
            margin-top: 20px; 
        }
    </style>
</head>
<body>
    <div class="container">
        <h1>🚀 {{ site_title | default('Web Server Successfully Deployed!') }}</h1>
        <p>Congratulations! Your web server is running successfully.</p>
        
        <div class="server-info">
            <h3>Server Information:</h3>
            <ul>
                <li><strong>Hostname:</strong> {{ inventory_hostname }}</li>
                <li><strong>Operating System:</strong> {{ ansible_os_family }} {{ ansible_distribution_version | default('') }}</li>
                <li><strong>Web Server:</strong> {{ 'Apache HTTP Server' if ansible_os_family != 'Windows' else 'Microsoft IIS' }}</li>
                <li><strong>Architecture:</strong> {{ ansible_architecture }}</li>
                <li><strong>Deployment Time:</strong> {{ ansible_date_time.iso8601 }}</li>
                {% if enable_php %}
                <li><strong>PHP:</strong> Enabled</li>
                {% endif %}
                {% if enable_ssl %}
                <li><strong>SSL:</strong> Enabled</li>
                {% endif %}
            </ul>
        </div>
        
        <p><em>Deployed with ❤️ using Ansible</em></p>
    </div>
</body>
</html>

templates/httpd.conf.j2 (simplified version):

# Apache HTTP Server Configuration
ServerRoot "{{ '/etc/httpd' if ansible_os_family == 'RedHat' else '/etc/apache2' }}"
PidFile {{ '/var/run/httpd/httpd.pid' if ansible_os_family == 'RedHat' else '/var/run/apache2/apache2.pid' }}

Listen {{ webserver_port | default(80) }}
{% if enable_ssl %}
Listen {{ webserver_ssl_port | default(443) }} ssl
{% endif %}

# Load essential modules
LoadModule dir_module modules/mod_dir.so
LoadModule mime_module modules/mod_mime.so
LoadModule rewrite_module modules/mod_rewrite.so
{% if enable_ssl %}
LoadModule ssl_module modules/mod_ssl.so
{% endif %}
{% if enable_php %}
LoadModule php7_module modules/libphp7.so
{% endif %}

# User and Group
User {{ webserver_user }}
Group {{ webserver_group }}

# Document Root
DocumentRoot "{{ document_root }}"

<Directory "{{ document_root }}">
    Options Indexes FollowSymLinks
    AllowOverride All
    Require all granted
</Directory>

# Logging
ErrorLog "{{ log_directory }}/error_log"
CustomLog "{{ log_directory }}/access_log" combined

# MIME Types
TypesConfig /etc/mime.types

# Default Index Files
DirectoryIndex index.html index.php

{% if enable_ssl %}
# SSL Configuration
<VirtualHost *:{{ webserver_ssl_port }}>
    DocumentRoot "{{ document_root }}"
    SSLEngine on
    SSLCertificateFile {{ ssl_cert_path | default('/etc/ssl/certs/server.crt') }}
    SSLCertificateKeyFile {{ ssl_key_path | default('/etc/ssl/private/server.key') }}
</VirtualHost>
{% endif %}

Using Roles in Playbooks

Basic Role Usage

---
# deploy-webserver.yml
- name: Deploy web servers across multiple platforms
  hosts: webservers
  become: yes
  roles:
    - webserver

Role with Parameters

---
# deploy-webserver-custom.yml
- name: Deploy customized web servers
  hosts: webservers
  become: yes
  roles:
    - role: webserver
      vars:
        enable_ssl: true
        enable_php: true
        site_title: "My Awesome Application"
        webserver_port: 8080
        static_assets:
          - logo.png
          - styles.css
          - script.js

Conditional Role Execution

---
# conditional-deployment.yml
- name: Deploy services based on environment
  hosts: all
  become: yes
  roles:
    - role: common
      tags: [always]
    
    - role: webserver
      when: "'webservers' in group_names"
      tags: [web]
    
    - role: database
      when: "'databases' in group_names"
      tags: [db]
    
    - role: monitoring
      when: environment == 'production'
      tags: [monitoring]

Introduction to Ansible Collections

Collections represent the modern evolution of Ansible content packaging. While roles package related automation components, collections can package entire ecosystems.

Collection Structure

my_namespace.my_collection/
├── galaxy.yml
├── README.md
├── plugins/
│   ├── modules/
│   │   ├── my_module.py
│   │   └── another_module.py
│   ├── inventory/
│   ├── lookup/
│   ├── filter/
│   └── connection/
├── roles/
│   ├── role1/
│   ├── role2/
│   └── role3/
├── playbooks/
│   ├── site.yml
│   └── deploy.yml
├── docs/
└── tests/
    ├── integration/
    └── unit/

Creating a Custom Collection

# Create collection structure
ansible-galaxy collection init local.infrastructure --init-path ./collections/ansible_collections

# Navigate to collection directory
cd collections/ansible_collections/local/infrastructure

Collection Galaxy.yml

# galaxy.yml - Collection metadata
namespace: local
name: infrastructure
version: 1.0.0
readme: README.md
authors:
  - "Your Name <[email protected]>"
description: >
  Complete infrastructure automation collection with cross-platform support
  for web servers, databases, and monitoring systems.
license:
  - MIT
license_file: 'LICENSE'
tags:
  - infrastructure
  - webserver
  - database
  - monitoring
  - crossplatform
dependencies: {}
repository: https://github.com/yourusername/ansible-collection-infrastructure
documentation: https://your-docs-site.com
homepage: https://your-homepage.com
issues: https://github.com/yourusername/ansible-collection-infrastructure/issues
build_ignore:
  - '*.tar.gz'
  - 'tests/'

Collection with Multiple Roles

# playbooks/site.yml - Main collection playbook
---
- name: Complete infrastructure deployment
  hosts: all
  gather_facts: yes
  
  pre_tasks:
    - name: Ensure system is updated
      package:
        name: "*"
        state: latest
      when: update_system | default(false)
      become: yes

  roles:
    # Common role for all systems
    - role: local.infrastructure.common
      tags: [common, base]
    
    # Web server role for web servers
    - role: local.infrastructure.webserver
      when: "'webservers' in group_names"
      tags: [web, webserver]
    
    # Database role for database servers
    - role: local.infrastructure.database
      when: "'databases' in group_names"
      tags: [db, database]
    
    # Load balancer role
    - role: local.infrastructure.loadbalancer
      when: "'loadbalancers' in group_names"
      tags: [lb, loadbalancer]
    
    # Monitoring role for all systems
    - role: local.infrastructure.monitoring
      tags: [monitoring, observability]

  post_tasks:
    - name: Generate deployment report
      template:
        src: deployment-report.j2
        dest: "/tmp/deployment-report-{{ inventory_hostname }}.txt"
      delegate_to: localhost
      tags: [report]

Working with Ansible Galaxy

Discovering Content

# Search for roles
ansible-galaxy search webserver

# Search for collections
ansible-galaxy collection list

# Get information about a specific role
ansible-galaxy info geerlingguy.apache

# Get information about a collection
ansible-galaxy collection info community.general

Installing from Galaxy

Installing Roles

# Install a specific role
ansible-galaxy install geerlingguy.apache

# Install a specific version
ansible-galaxy install geerlingguy.apache,2.0.0

# Install to a specific directory
ansible-galaxy install --roles-path ./roles geerlingguy.apache

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

Installing Collections

# Install a collection
ansible-galaxy collection install community.general

# Install specific version
ansible-galaxy collection install community.general:==4.0.0

# Install to specific path
ansible-galaxy collection install community.general -p ./collections

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

Requirements File Examples

requirements.yml for Roles and Collections

---
# requirements.yml - Mixed requirements file
roles:
  # Install from Galaxy
  - name: geerlingguy.apache
    version: "2.0.0"
  
  # Install from GitHub
  - src: https://github.com/your-org/ansible-role-custom
    name: custom_role
    version: main
  
  # Install from Git with specific commit
  - src: git+https://github.com/another-org/role.git
    version: "a1b2c3d4"
    name: another_role

collections:
  # Core collections
  - name: ansible.posix
    version: ">=1.3.0"
  
  # Community collections
  - name: community.general
    version: ">=4.0.0"
  
  # Cloud provider collections
  - name: amazon.aws
    version: ">=3.0.0"
  
  - name: azure.azcollection
    version: ">=1.10.0"
  
  # Windows collections
  - name: ansible.windows
    version: ">=1.7.0"
  
  # Custom collection from Git
  - name: https://github.com/your-org/ansible-collection-custom.git
    type: git
    version: main

Environment-Specific Requirements

requirements-dev.yml:

---
collections:
  - name: community.general
  - name: ansible.posix
  - name: community.crypto
roles:
  - name: geerlingguy.apache
  - name: geerlingguy.mysql

requirements-prod.yml:

---
collections:
  - name: community.general
    version: "==4.8.0"
  - name: ansible.posix  
    version: "==1.4.0"
  - name: community.crypto
    version: "==2.5.0"
  - name: amazon.aws
    version: "==3.4.0"
roles:
  - name: geerlingguy.apache
    version: "2.0.0"
  - name: geerlingguy.mysql
    version: "3.3.0"

Real-World Integration Example

Let's create a comprehensive deployment that showcases roles and collections working together:

Project Structure

infrastructure-project/
├── ansible.cfg
├── inventory/
│   ├── hosts.yml
│   ├── group_vars/
│   │   ├── all.yml
│   │   ├── webservers.yml
│   │   ├── databases.yml
│   │   └── windows.yml
│   └── host_vars/
├── playbooks/
│   ├── site.yml
│   ├── webservers.yml
│   ├── databases.yml
│   └── monitoring.yml
├── roles/
│   ├── common/
│   ├── webserver/
│   └── database/
├── collections/
│   └── ansible_collections/
├── requirements.yml
├── vars/
│   ├── secrets.yml
│   └── environments/
│       ├── dev.yml
│       ├── staging.yml
│       └── production.yml
└── scripts/
    ├── deploy.sh
    └── setup.sh

Main Deployment Playbook

# playbooks/site.yml
---
- name: Infrastructure Deployment Pipeline
  hosts: localhost
  gather_facts: no
  vars:
    deployment_timestamp: "{{ ansible_date_time.epoch }}"
  
  tasks:
    - name: Start deployment tracking
      debug:
        msg: |
          Starting infrastructure deployment...
          Environment: {{ environment | default('development') }}
          Timestamp: {{ ansible_date_time.iso8601 }}
      tags: [always]

# Common configuration for all hosts
- name: Base system configuration
  hosts: all
  become: yes
  
  pre_tasks:
    - name: Gather comprehensive facts
      setup:
        gather_subset:
          - all
      tags: [always]
    
    - name: Load environment-specific variables
      include_vars: "environments/{{ environment | default('dev') }}.yml"
      tags: [always]

  roles:
    - role: common
      tags: [common, base]

# Web server deployment
- name: Deploy web servers
  hosts: webservers
  become: yes
  serial: "{{ rolling_update_serial | default('100%') }}"
  
  pre_tasks:
    - name: Check if web server is already running
      uri:
        url: "http://{{ ansible_default_ipv4.address }}:{{ webserver_port | default(80) }}/"
        method: GET
        status_code: [200, 404, -1]
      register: web_check
      ignore_errors: yes
      tags: [web, health-check]

  roles:
    - role: webserver
      tags: [web, webserver]

  post_tasks:
    - name: Verify web server deployment
      uri:
        url: "http://{{ ansible_default_ipv4.address }}:{{ webserver_port | default(80) }}/"
        method: GET
        status_code: 200
        return_content: yes
      register: deployment_check
      retries: 5
      delay: 10
      tags: [web, verification]
    
    - name: Log successful deployment
      debug:
        msg: "Web server successfully deployed on {{ inventory_hostname }}"
      when: deployment_check is succeeded
      tags: [web, verification]

# Database deployment  
- name: Deploy database servers
  hosts: databases
  become: yes
  serial: 1  # Deploy databases one at a time
  
  roles:
    - role: database
      tags: [db, database]

# Load balancer configuration
- name: Configure load balancers
  hosts: loadbalancers
  become: yes
  
  vars:
    backend_servers: "{{ groups['webservers'] | map('extract', hostvars, 'ansible_default_ipv4') | map(attribute='address') | list }}"
  
  roles:
    - role: loadbalancer
      tags: [lb, loadbalancer]

# Monitoring setup
- name: Deploy monitoring infrastructure
  hosts: all
  become: yes
  
  roles:
    - role: monitoring
      tags: [monitoring, observability]

# Final verification and reporting
- name: Deployment verification and reporting
  hosts: localhost
  
  tasks:
    - name: Perform end-to-end health checks
      uri:
        url: "http://{{ item }}:{{ webserver_port | default(80) }}/"
        method: GET
        status_code: 200
      loop: "{{ groups['webservers'] }}"
      delegate_to: localhost
      tags: [verification, health-check]
    
    - name: Generate deployment report
      template:
        src: deployment-report.j2
        dest: "./reports/deployment-{{ deployment_timestamp }}.html"
      delegate_to: localhost
      tags: [report]
    
    - name: Send notification
      mail:
        to: "{{ notification_email }}"
        subject: "Infrastructure Deployment Complete"
        body: |
          Infrastructure deployment completed successfully.
          
          Environment: {{ environment | default('development') }}
          Deployed hosts: {{ ansible_play_hosts | join(', ') }}
          Timestamp: {{ ansible_date_time.iso8601 }}
          
          Report available at: ./reports/deployment-{{ deployment_timestamp }}.html
      when: 
        - notification_email is defined
        - send_notifications | default(false)
      tags: [notification]

Environment-Specific Variables

vars/environments/production.yml:

---
# Production environment configuration
environment: production
rolling_update_serial: 2

# Security settings
enable_ssl: true
enable_firewall: true
strict_security: true

# Performance settings
webserver_port: 80
enable_caching: true
cache_size: "512m"

# Monitoring
enable_monitoring: true
monitoring_retention_days: 90
alert_email: "[email protected]"

# Backup settings
enable_backups: true
backup_schedule: "0 2 * * *"
backup_retention_days: 30

# Database settings
database_max_connections: 200
database_buffer_size: "256M"
enable_database_ssl: true

# Load balancer settings
lb_algorithm: "round_robin"
health_check_interval: 30
max_fails: 3

# Notification settings
send_notifications: true
notification_email: "[email protected]"

vars/environments/dev.yml:

---
# Development environment configuration
environment: development
rolling_update_serial: "100%"

# Security settings (relaxed for development)
enable_ssl: false
enable_firewall: false
strict_security: false

# Performance settings
webserver_port: 8080
enable_caching: false

# Monitoring (lightweight)
enable_monitoring: true
monitoring_retention_days: 7

# Backup settings (disabled for dev)
enable_backups: false

# Database settings
database_max_connections: 50
database_buffer_size: "128M"
enable_database_ssl: false

# Load balancer settings
lb_algorithm: "round_robin"
health_check_interval: 60

# Notification settings
send_notifications: false

Deployment Automation Script

#!/bin/bash
# scripts/deploy.sh - Automated deployment script

set -euo pipefail

# Configuration
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
ENVIRONMENT="${1:-dev}"
TAGS="${2:-all}"
LIMIT="${3:-all}"

# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color

# Logging function
log() {
    echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')]${NC} $1"
}

warn() {
    echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARNING:${NC} $1"
}

error() {
    echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR:${NC} $1"
    exit 1
}

# Validate environment
validate_environment() {
    if [[ ! -f "$PROJECT_DIR/vars/environments/$ENVIRONMENT.yml" ]]; then
        error "Environment file not found: $PROJECT_DIR/vars/environments/$ENVIRONMENT.yml"
    fi
    log "Using environment: $ENVIRONMENT"
}

# Install dependencies
install_dependencies() {
    log "Installing Ansible dependencies..."
    
    if [[ -f "$PROJECT_DIR/requirements.yml" ]]; then
        ansible-galaxy install -r "$PROJECT_DIR/requirements.yml" --force
        ansible-galaxy collection install -r "$PROJECT_DIR/requirements.yml" --force
    else
        warn "No requirements.yml found, skipping dependency installation"
    fi
}

# Run syntax check
syntax_check() {
    log "Running syntax check..."
    ansible-playbook \
        -i "$PROJECT_DIR/inventory/hosts.yml" \
        "$PROJECT_DIR/playbooks/site.yml" \
        --syntax-check \
        -e "environment=$ENVIRONMENT"
}

# Run deployment
deploy() {
    log "Starting deployment with tags: $TAGS, limit: $LIMIT"
    
    local ansible_args=(
        -i "$PROJECT_DIR/inventory/hosts.yml"
        "$PROJECT_DIR/playbooks/site.yml"
        -e "environment=$ENVIRONMENT"
    )
    
    # Add tags if specified
    if [[ "$TAGS" != "all" ]]; then
        ansible_args+=("--tags" "$TAGS")
    fi
    
    # Add limit if specified
    if [[ "$LIMIT" != "all" ]]; then
        ansible_args+=("--limit" "$LIMIT")
    fi
    
    # Add verbose output for non-production
    if [[ "$ENVIRONMENT" != "production" ]]; then
        ansible_args+=("-v")
    fi
    
    ansible-playbook "${ansible_args[@]}"
}

# Main execution
main() {
    log "Starting deployment script"
    log "Project directory: $PROJECT_DIR"
    
    cd "$PROJECT_DIR"
    
    validate_environment
    install_dependencies
    syntax_check
    deploy
    
    log "Deployment completed successfully!"
}

# Handle script arguments
case "${1:-}" in
    --help|-h)
        echo "Usage: $0 [ENVIRONMENT] [TAGS] [LIMIT]"
        echo ""
        echo "Arguments:"
        echo "  ENVIRONMENT  Target environment (dev, staging, production) [default: dev]"
        echo "  TAGS         Ansible tags to run [default: all]"
        echo "  LIMIT        Limit execution to specific hosts [default: all]"
        echo ""
        echo "Examples:"
        echo "  $0 dev"
        echo "  $0 production web"
        echo "  $0 staging db webserver-01"
        exit 0
        ;;
esac

main "$@"

Advanced Patterns and Best Practices

Role Dependencies and Layering

# meta/main.yml - Complex dependency example
dependencies:
  # Always install common role first
  - role: common
    tags: [always]
  
  # Install security hardening
  - role: security
    when: enable_security | default(true)
  
  # Install monitoring agent
  - role: monitoring-agent
    when: enable_monitoring | default(true)
  
  # Platform-specific dependencies
  - role: selinux-config
    when: 
      - ansible_os_family == 'RedHat'
      - ansible_selinux.status == 'enabled'

Dynamic Role Loading

---
# Dynamic role execution based on facts
- name: Dynamic infrastructure deployment
  hosts: all
  vars:
    role_mapping:
      webservers: 
        - webserver
        - ssl-termination
      databases:
        - database
        - backup-agent
      monitoring:
        - monitoring-server
        - alerting
  
  tasks:
    - name: Determine roles to execute
      set_fact:
        roles_to_run: "{{ role_mapping[group_names | intersect(role_mapping.keys()) | first] | default([]) }}"
    
    - name: Execute determined roles
      include_role:
        name: "{{ item }}"
      loop: "{{ roles_to_run }}"
      tags: [dynamic]

Collection Development Workflow

# .github/workflows/collection-ci.yml
name: Collection CI/CD
on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Setup Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.9'
      
      - name: Install dependencies
        run: |
          pip install ansible ansible-lint yamllint
      
      - name: Lint YAML files
        run: yamllint .
      
      - name: Lint Ansible content
        run: ansible-lint

  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        ansible-version: ['4.10', '5.9', '6.0']
    
    steps:
      - uses: actions/checkout@v3
      - name: Setup Python
        uses: actions/setup-python@v4
        with:
          python-version: '3.9'
      
      - name: Install Ansible
        run: pip install ansible==${{ matrix.ansible-version }}
      
      - name: Run tests
        run: ansible-test units --docker default

  build:
    needs: [lint, test]
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
      - uses: actions/checkout@v3
      - name: Build collection
        run: ansible-galaxy collection build
      
      - name: Publish to Galaxy
        run: ansible-galaxy collection publish *.tar.gz --api-key ${{ secrets.GALAXY_API_KEY }}

Testing Strategies

Molecule Testing for Roles

# molecule/default/molecule.yml
---
dependency:
  name: galaxy
driver:
  name: docker
platforms:
  - name: instance-centos8
    image: centos:8
    pre_build_image: true
  - name: instance-ubuntu20
    image: ubuntu:20.04
    pre_build_image: true
  - name: instance-windows2019
    image: mcr.microsoft.com/windows/servercore:ltsc2019
    pre_build_image: true
    platform: windows
provisioner:
  name: ansible
  playbooks:
    converge: converge.yml
    verify: verify.yml
verifier:
  name: ansible

Integration Tests

# tests/integration/test-webserver.yml
---
- name: Test web server deployment
  hosts: all
  gather_facts: yes
  
  tasks:
    - name: Deploy web server role
      include_role:
        name: webserver
      vars:
        enable_ssl: true
        webserver_port: 8080
    
    - name: Verify web server is running
      uri:
        url: "http://{{ ansible_default_ipv4.address }}:8080/"
        method: GET
        status_code: 200
      register: web_response
    
    - name: Verify SSL certificate
      openssl_certificate:
        path: "{{ ssl_cert_path }}"
        provider: assertonly
        valid_in: 3600
      when: enable_ssl
    
    - name: Check service status
      service_facts:
    
    - name: Assert web server service is running
      assert:
        that:
          - ansible_facts.services[webserver_service].state == 'running'
        fail_msg: "Web server service is not running"

Performance Optimization and Scaling

Parallel Execution Strategies

---
# High-performance deployment with parallel execution
- name: Optimized infrastructure deployment
  hosts: all
  strategy: free  # Allow hosts to run independently
  gather_facts: yes
  
  vars:
    ansible_python_interpreter: /usr/bin/python3
    ansible_pipelining: true  # Reduce SSH overhead
  
  pre_tasks:
    # Use async for time-consuming operations
    - name: Update package cache (async)
      package:
        update_cache: yes
      async: 300
      poll: 0
      register: update_cache_job
      become: yes
    
    - name: Wait for package cache update
      async_status:
        jid: "{{ update_cache_job.ansible_job_id }}"
      register: update_result
      until: update_result.finished
      retries: 30
      delay: 10
      become: yes

  roles:
    - role: webserver
      tags: [web]
  
  post_tasks:
    - name: Parallel service verification
      uri:
        url: "http://{{ ansible_default_ipv4.address }}:{{ webserver_port | default(80) }}/"
        method: GET
        status_code: 200
      register: health_check
      async: 60
      poll: 0
    
    - name: Collect verification results
      async_status:
        jid: "{{ health_check.ansible_job_id }}"
      register: verification_result
      until: verification_result.finished
      retries: 12
      delay: 5

Caching and Optimization

# ansible.cfg - Performance optimizations
[defaults]
# Use persistent SSH connections
host_key_checking = False
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o ControlPath=/tmp/ansible-ssh-%h-%p-%r

# Enable pipelining for faster execution
pipelining = True

# Use efficient fact gathering
gathering = smart
fact_caching = jsonfile
fact_caching_connection = /tmp/ansible_facts_cache
fact_caching_timeout = 86400

# Optimize callback plugins
stdout_callback = yaml
bin_ansible_callbacks = True

# Connection settings
timeout = 30
host_key_checking = False

[ssh_connection]
# SSH optimization
ssh_args = -o ControlMaster=auto -o ControlPersist=60s
pipelining = True
control_path = /tmp/ansible-ssh-%%h-%%p-%%r

Troubleshooting and Debugging

Debugging Role Execution

---
# Debug playbook for role troubleshooting
- name: Debug role execution
  hosts: all
  vars:
    debug_mode: true
  
  pre_tasks:
    - name: Display execution environment
      debug:
        var: ansible_env
      when: debug_mode | default(false)
    
    - name: Display gathered facts
      debug:
        var: ansible_facts
      when: debug_mode | default(false)

  roles:
    - role: webserver
      tags: [web]

  post_tasks:
    - name: Display role variables
      debug:
        msg: |
          Role execution completed with the following variables:
          - webserver_service: {{ webserver_service | default('undefined') }}
          - document_root: {{ document_root | default('undefined') }}
          - webserver_port: {{ webserver_port | default('undefined') }}
      when: debug_mode | default(false)
    
    - name: Check for common issues
      debug:
        msg: "WARNING: {{ item }}"
      loop:
        - "SSL enabled but no certificates configured"
        - "PHP enabled but no PHP packages installed"
        - "Firewall disabled in production environment"
      when: 
        - debug_mode | default(false)
        - (enable_ssl and ssl_cert_path is not defined) or
          (enable_php and php_packages is not defined) or
          (environment == 'production' and not enable_firewall)

Common Issues and Solutions

1. Role Not Found Error

# Error: role 'webserver' not found
# Solution: Check role path configuration

# Check current role paths
ansible-config dump | grep ROLES_PATH

# Set role path in ansible.cfg
echo "roles_path = ./roles" >> ansible.cfg

# Or use environment variable
export ANSIBLE_ROLES_PATH=./roles

2. Collection Import Issues

# Error: collection not found
# Solution: Ensure collection is properly installed and referenced

- name: Fix collection import issues
  hosts: localhost
  tasks:
    - name: List installed collections
      command: ansible-galaxy collection list
      register: collections_list
    
    - name: Display collections
      debug:
        var: collections_list.stdout_lines
    
    - name: Install missing collection
      command: ansible-galaxy collection install community.general
      when: "'community.general' not in collections_list.stdout"

3. Variable Precedence Issues

---
# Debug variable precedence
- name: Debug variable precedence
  hosts: all
  vars:
    test_var: "playbook_vars"
  
  tasks:
    - name: Display variable sources
      debug:
        msg: |
          Variable precedence check:
          - Current value: {{ test_var }}
          - From defaults: {{ hostvars[inventory_hostname].get('test_var', 'not_defined') }}
          - From group_vars: {{ group_vars.get('test_var', 'not_defined') }}
          - From host_vars: {{ host_vars.get('test_var', 'not_defined') }}
    
    - name: Set fact to override
      set_fact:
        test_var: "set_fact_override"
    
    - name: Display final value
      debug:
        var: test_var

Conclusion

My journey from monolithic playbooks to modular, reusable automation components taught me that the real power of Ansible lies not just in its ability to automate tasks, but in its ecosystem that enables sharing, collaboration, and building upon others' work.

Key insights from this exploration:

  1. Start with Roles - Break down your automation into logical, reusable components

  2. Embrace Collections - Use the modern packaging system for complex automation solutions

  3. Leverage Galaxy - Don't reinvent the wheel; build upon community contributions

  4. Plan for Reusability - Design your automation with sharing and portability in mind

  5. Test Thoroughly - Use proper testing strategies to ensure reliability across environments

  6. Document Everything - Good documentation makes your automation accessible to others

Whether you're building infrastructure automation for a small startup or enterprise-scale solutions, understanding Ansible Galaxy, Roles, and Collections will transform how you approach automation architecture. The shift from "scripts that work" to "automation that scales" happens when you embrace these collaborative and modular patterns.

The beauty of this ecosystem is that every role you create, every collection you build, and every playbook you write can potentially help someone else solve similar challenges. Start building, start sharing, and become part of the automation community that's making infrastructure management more efficient for everyone.

Additional Resources


Have questions about Ansible Galaxy, Roles, or Collections? Want to share your own automation components? Feel free to reach out through the comments below. Let's build better automation together!

Last updated