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:
Start with Roles - Break down your automation into logical, reusable components
Embrace Collections - Use the modern packaging system for complex automation solutions
Leverage Galaxy - Don't reinvent the wheel; build upon community contributions
Plan for Reusability - Design your automation with sharing and portability in mind
Test Thoroughly - Use proper testing strategies to ensure reliability across environments
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