
August 18, 2026
10 min read
Table of Contents
By Kokil Thapa | Last reviewed: August 2026
Managing infrastructure as code quickly becomes unmanageable when playbooks grow into monolithic YAML files spanning thousands of lines. Ansible Roles and Galaxy: Reusable Automation solves this by enforcing a standardized directory structure that encapsulates tasks, handlers, variables, and templates into portable, testable units. Instead of copying configuration blocks between projects, you define a role once and consume it across legal-tech portals, eCommerce platforms, or client servers with consistent behavior. This guide covers the practical mechanics of building, testing, and sharing roles in 2026, grounded in production deployments rather than theoretical documentation.
How do you structure Ansible Roles and Galaxy: Reusable Automation correctly?
A common mistake I see on client projects is treating roles as mere task folders. A properly structured role is a contract: it declares its interface through defaults/main.yml, validates input via meta/argument_specs.yml, and isolates implementation details. When building reliable DevOps automation, this structure prevents configuration drift between staging and production environments.
Distinguishing defaults from vars
The difference between defaults/main.yml and vars/main.yml causes frequent bugs. Defaults have the lowest precedence; they exist so users can override them in playbooks or inventory without editing the role. Vars have high precedence and should only contain values the role author intends to be fixed (e.g., internal package names, hardcoded paths). On a legal-tech portal I built, putting database credentials in vars/ prevented clients from overriding them per environment — always use defaults/ for configurable values.
Validating role interfaces with argument specs
Since Ansible 2.11, meta/argument_specs.yml enforces type checking and required parameters at runtime. This catches misconfigurations before tasks execute:
# meta/argument_specs.yml
argument_specs:
main:
short_description: Configure Nginx for Laravel application
options:
nginx_server_name:
type: str
required: true
description: Primary domain name for virtual host
nginx_php_version:
type: str
default: "8.4"
choices: ["8.2", "8.3", "8.4"]
description: PHP-FPM socket version to proxy
nginx_ssl_enabled:
type: bool
default: true
description: Whether to configure Let's Encrypt certificates Without this spec, a missing nginx_server_name would fail deep inside a Jinja2 template with an opaque error. With it, Ansible fails fast at role invocation with a clear message. For teams maintaining CI/CD pipelines, this validation step is non-negotiable.
How do you manage dependencies in Ansible Roles and Galaxy: Reusable Automation?
Roles rarely work in isolation. A Laravel deployment role depends on PHP-FPM, Composer, and Nginx roles. Managing these dependencies incorrectly leads to version conflicts, circular imports, or silent failures where outdated roles run against incompatible systems.
Explicit vs implicit dependencies
There are two ways to declare dependencies, and mixing them carelessly creates maintenance nightmares:
- Implicit (meta/main.yml): Listed under
dependencies:. Ansible runs these automatically before the role's tasks. Use only for hard prerequisites that must always run (e.g., aphp-fpmrole that yourlaravel-approle cannot function without). - Explicit (requirements.yml): Declared separately and installed via
ansible-galaxy install -r requirements.yml. These are not auto-executed; they must be listed in your playbook. Prefer this for soft dependencies or optional components.
On production systems, I avoid implicit dependencies entirely. They execute silently, making debugging difficult when a role fails because of an unexpected transitive dependency. Explicit requirements give full visibility in the playbook and allow conditional inclusion based on environment variables or tags.
Pinning versions for reproducible builds
Never use unpinned Galaxy roles in production. A role updated upstream can break your deployment without any change on your side. Pin to exact versions or Git commits:
# requirements.yml
roles:
- name: geerlingguy.php
version: "6.0.0"
src: https://galaxy.ansible.com/api/v2/roles/geerlingguy/php/
- name: custom-laravel-deploy
src: git+https://gitlab.example.com/infra/ansible-laravel.git
version: v2.3.1 # Tag or commit SHA, never branch name
collections:
- name: community.general
version: ">=9.0.0,<10.0.0"
source: https://galaxy.ansible.com For private roles hosted on GitLab (common for Nepal-based clients with proprietary legal-tech logic), use SSH URLs and ensure the deploy runner has read access via deploy keys. The version field accepts tags, branches, or commit SHAs — prefer tags or SHAs for immutability.
When should you use Ansible Collections instead of standalone roles?
Collections bundle multiple roles, modules, plugins, and documentation into a single distributable unit. While roles handle single concerns (configure Nginx, install PHP), collections address entire technology stacks or organizational standards. Understanding when to graduate from roles to collections prevents premature abstraction.
| Criteria | Standalone Role | Collection |
|---|---|---|
| Scope | Single component (Nginx, Redis, PHP-FPM) | Technology stack or org standard (LAMP, Nepal Legal-Tech Base) |
| Distribution | Galaxy role namespace.role_name | Galaxy collection namespace.collection_name |
| Custom Modules | Not supported (use library/ hack) | Native plugins/modules/ directory |
| Documentation | README.md only | docs/, integrated with ansible-doc |
| Testing | Molecule per role | Molecule scenarios + integration tests |
| Versioning | Semver per role | Semver for entire bundle |
| Best For | Reusable atomic configurations | Internal standards, multi-role orchestration |
In my experience shipping Laravel applications across multiple client servers, I start with individual roles. Only when three or more roles are always used together and share common variables do I extract them into a collection. For example, a nepal_legal_tech collection might bundle court_marriage_portal, notary_document_workflow, and ird_vat_compliance roles alongside custom modules for Bikram Sambat date conversion and ConnectIPS payment verification.
Creating a private collection for internal use
Private collections live in your Git repository and are installed directly without publishing to Galaxy:
# Initialize collection skeleton
ansible-galaxy collection init kokil_thapa.nepal_infra
# Install from local path during development
ansible-galaxy collection install ./kokil_thapa-nepal_infra-1.0.0.tar.gz
# Or reference in requirements.yml from private Git
collections:
- name: git+https://gitlab.example.com/infra/nepal-infra-collection.git
version: v1.2.0
type: git This approach keeps proprietary business logic (legal document templates, Nepal-specific tax calculations, local payment gateway integrations) out of public Galaxy while still benefiting from collection tooling, documentation generation, and standardized testing.
How do you test Ansible Roles and Galaxy: Reusable Automation before production?
Untested roles are technical debt. A role that works on your laptop may fail on Ubuntu 24.04 due to changed package names, different systemd paths, or updated PHP-FPM socket locations. Molecule provides isolated testing environments using Docker, Podman, or cloud instances.
Setting up Molecule for a Laravel deployment role
Initialize Molecule inside your role directory and configure it to test against Ubuntu 24.04 (matching your production targets):
# Inside roles/laravel-deploy/
molecule init scenario --driver-name docker
# molecule/default/molecule.yml
dependency:
name: galaxy
driver:
name: docker
platforms:
- name: ubuntu-2404
image: geerlingguy/docker-ubuntu2404-ansible:latest
pre_build_image: true
privileged: true
volumes:
- /sys/fs/cgroup:/sys/fs/cgroup:rw
cgroupns_mode: host
provisioner:
name: ansible
playbooks:
converge: converge.yml
verifier:
name: testinfra The privileged flag and cgroup volume mount are required for systemd services (PHP-FPM, Nginx) to start inside Docker. Without them, systemctl commands fail silently and your idempotence check passes falsely.
Writing meaningful verify tests
Verification must assert actual system state, not just task success. Use Testinfra to confirm the role produced the expected outcome:
# molecule/default/tests/test_default.py
import testinfra
def test_php_fpm_is_running(host):
service = host.service("php8.4-fpm")
assert service.is_running
assert service.is_enabled
def test_nginx_config_syntax(host):
cmd = host.run("nginx -t")
assert cmd.rc == 0
def test_laravel_storage_permissions(host):
storage = host.file("/var/www/app/storage")
assert storage.user == "www-data"
assert storage.group == "www-data"
assert storage.mode == 0o775
def test_env_file_exists_and_secure(host):
env = host.file("/var/www/app/.env")
assert env.exists
assert env.mode == 0o600 These tests catch real problems: wrong ownership breaking file uploads, insecure .env permissions exposing secrets, Nginx config errors that only surface on reload. Running molecule test in CI before every merge ensures security and reliability are baked in, not audited after incidents.
Deploying Ansible Roles and Galaxy: Reusable Automation in Production Workflows
Roles and collections are only useful if they integrate cleanly with your deployment pipeline. On projects using Deployer 7 and GitLab CI (a pattern I've implemented for multiple sister sites on shared EC2 infrastructure), Ansible handles server provisioning while Deployer manages application releases. Keeping these concerns separate prevents coupling infrastructure changes to code deployments.
Integrating roles into GitLab CI
Run Molecule tests in CI and pin role versions in a lockfile-style approach:
# .gitlab-ci.yml
stages:
- lint
- test
- deploy
ansible-lint:
stage: lint
image: cytopia/ansible-lint:latest
script:
- ansible-lint roles/ requirements.yml
molecule-test:
stage: test
image: python:3.12-slim
services:
- docker:dind
script:
- pip install molecule[docker] testinfra
- cd roles/laravel-deploy
- molecule test
provision-staging:
stage: deploy
only: [main]
script:
- ansible-galaxy install -r requirements.yml --force
- ansible-playbook site.yml -i inventory/staging.ini --diff The --force flag ensures pinned versions in requirements.yml override any cached copies. Without it, CI runners reuse stale roles from previous jobs, causing inconsistent builds.
Handling secrets and environment-specific overrides
Never commit secrets to role defaults or vars. Use Ansible Vault for sensitive data and environment-specific variable files for non-secret configuration differences:
# inventory/staging/group_vars/web_servers.yml
nginx_server_name: staging.legalportal.example.com
nginx_ssl_enabled: false
app_debug: true
# inventory/production/group_vars/web_servers.yml
nginx_server_name: legalportal.example.com
nginx_ssl_enabled: true
app_debug: false
# Encrypted secrets (committed safely)
# inventory/production/group_vars/web_servers.vault.yml
db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
...encrypted content... Decrypt at runtime with --ask-vault-pass or a vault password file stored securely in CI secrets. This pattern lets the same role serve staging, production, and disaster recovery environments without modification — only inventory changes.
Practical Next Steps for Ansible Roles and Galaxy: Reusable Automation
Start by extracting your most-repeated playbook sections into roles today. Pick one component (PHP-FPM, Nginx, Redis) and structure it properly with argument specs and Molecule tests before touching anything else. Resist the urge to build a grand unified collection prematurely; let abstraction emerge from repeated use. For teams managing Nepal-focused infrastructure or legal-tech platforms, invest early in private collections for domain-specific logic (Bikram Sambat handling, IRD compliance, local payment gateways) that will never belong on public Galaxy. If you need help structuring automation for production systems or auditing existing Ansible workflows, reach out to discuss your infrastructure needs.

