Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Ansible Roles and Galaxy: Reusable Automation

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.

Role Directory Structuretasks/main.ymlhandlers/main.ymldefaults/main.ymlvars/main.ymlmeta/main.ymltemplates/*.j2files/static.conftests/test.ymlEntry point for executionTriggered by notifyUser-overridable valuesHigh-priority constantsDependencies & metadataJinja2 config filesCopied as-isMolecule / integrationVariable Precedence (Low → High)role defaults → inventory → playbook vars → role vars → extra vars (-e)
Standard Ansible Role directory layout with variable precedence hierarchy for Ansible Roles and Galaxy: Reusable Automation

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.

Galaxy Dependency Resolution Pipelinerequirements.ymlDeclared dependenciesansible-galaxy installResolves & downloads~/.ansible/roles/Local role cachemeta/main.yml depsImplicit role includesPlaybook roles:Explicit invocationExecution OrderDeps → Role → HandlersVersion Pinning StrategyAlways pin: src, version (tag/commit), scm. Never rely on 'latest' in production.
Dependency resolution pipeline for Ansible Roles and Galaxy: Reusable Automation showing explicit vs implicit dependency handling

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., a php-fpm role that your laravel-app role 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.

CriteriaStandalone RoleCollection
ScopeSingle component (Nginx, Redis, PHP-FPM)Technology stack or org standard (LAMP, Nepal Legal-Tech Base)
DistributionGalaxy role namespace.role_nameGalaxy collection namespace.collection_name
Custom ModulesNot supported (use library/ hack)Native plugins/modules/ directory
DocumentationREADME.md onlydocs/, integrated with ansible-doc
TestingMolecule per roleMolecule scenarios + integration tests
VersioningSemver per roleSemver for entire bundle
Best ForReusable atomic configurationsInternal 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.

Molecule Test LifecyclecreateSpin up container/VMconvergeApply role first timeidempotenceRe-run, expect 0 changesverifyAssert state (Testinfra)destroyTear down environmentFailure Feedback LoopAny stage fails → halt, preserve instance, debug, fix, re-runIdempotence Is the Critical CheckIf second run reports changes, role is NOT safe for production re-runs
Molecule testing lifecycle ensuring idempotence and correctness for Ansible Roles and Galaxy: Reusable Automation

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.

Frequently Asked Questions

An Ansible role is a standardized directory structure that bundles tasks, handlers, templates, files, and variables into a reusable automation unit. It enforces consistency across projects and teams by encapsulating specific functionality like web server setup or database configuration into portable, testable components that can be shared via Galaxy or private repositories without duplicating code.

Run ansible-galaxy init my_role_name in your roles directory to generate the standard folder structure including tasks, handlers, defaults, vars, meta, and templates directories. This command creates all necessary boilerplate files so you can immediately start defining automation logic without manually creating each subdirectory and YAML file, ensuring compatibility with Galaxy publishing requirements and team conventions.

Ansible Galaxy is the free public repository for community-contributed roles and collections, while Automation Hub is Red Hat's certified, enterprise-grade content source with support and security validation. Galaxy suits open-source projects and learning; Automation Hub targets organizations requiring vetted, supported automation content with SLAs. Both use similar CLI commands but differ in content assurance, licensing, and integration with Red Hat Ansible Platform subscriptions.

Ansible Galaxy is completely free for downloading and publishing community roles. Red Hat Automation Hub requires an Ansible Platform subscription starting around USD 5,000 annually (approximately NPR 665,000) for certified content access. Most Nepal-based projects I work on use free Galaxy roles successfully, reserving paid Automation Hub only when enterprise compliance or vendor support mandates certified automation content.

Yes, Ansible roles excel at provisioning Laravel infrastructure including PHP-FPM, Nginx, MySQL, Redis, and queue workers. In my experience deploying Laravel applications on Ubuntu servers, roles handle consistent environment setup across staging and production. You define PHP extensions, opcache settings, deployer dependencies, and cron schedules once in a role, then apply identical configurations everywhere, eliminating drift between environments that causes deployment failures.

Run ansible-galaxy install author.role_name to download directly into your default roles path, typically /etc/ansible/roles or ~/.ansible/roles. For project-specific installations, use ansible-galaxy install -r requirements.yml with a version-pinned dependency file. Always specify exact versions in production rather than latest tags to prevent unexpected breaking changes during future installs, especially when maintaining multiple client environments with different upgrade cycles.

Defaults contain low-precedence values users should override, like port numbers or package names. Vars hold high-precedence internal values rarely changed, such as hardcoded paths or computed parameters. In practice, I put configurable options in defaults so playbook authors customize behavior without editing role internals, while vars protects critical implementation details. Mixing these causes frustrating precedence bugs where overrides silently fail because vars always wins over defaults.

Use Molecule with Docker or Podman drivers to spin up isolated containers matching your target OS, run the role, and verify idempotency through automated test sequences. Define converge.yml for role execution and verify.yml for assertions checking service status, file contents, or command output. On real client projects, this catches permission errors, missing dependencies, and template rendering failures before they break live servers during maintenance windows.

Verify relative paths use include_tasks or import_tasks correctly within the tasks directory. Absolute paths break portability; always reference files relative to the role root. Check that included files exist in the expected location and have proper YAML syntax. A common issue I encounter is stale cached roles from previous installs conflicting with updated versions. Run ansible-galaxy remove role_name then reinstall to ensure clean state matches your current playbook expectations.

List required roles under dependencies in meta/main.yml with optional version constraints and conditional parameters. Ansible automatically installs and executes dependencies before your role runs. Specify galaxy_info with author, description, license, and supported platforms for discoverability. In production systems, pin exact dependency versions rather than ranges to prevent upstream breaking changes from cascading into your infrastructure during routine updates or new environment provisioning.

Community roles vary widely in quality and security posture. Always audit source code, check last commit date, review open issues, and test thoroughly in isolated environments before production use. Prefer roles with active maintainers, comprehensive documentation, and Molecule tests. On legal-tech portals handling sensitive documents, I fork trusted community roles into private repositories for full control over updates and security patches rather than trusting upstream changes blindly.

Ensure your role passes ansible-lint checks, includes complete meta/main.yml metadata, has meaningful README documentation, and contains working Molecule tests. Create a GitHub repository following naming conventions, then authenticate via ansible-galaxy login and run ansible-galaxy role import github_user repo_name. Tags and platform filters improve discoverability. Maintain semantic versioning through git tags so consumers can pin stable releases reliably in their requirements files.

Standard structure includes tasks/main.yml for primary logic, handlers/main.yml for service restarts, defaults/main.yml for overridable variables, vars/main.yml for internal constants, meta/main.yml for dependencies and metadata, templates/ for Jinja2 files, files/ for static assets, and library/ for custom modules. Deviating from this layout breaks ansible-galaxy tooling and confuses other developers expecting conventional organization when reading or contributing to your automation code.

Never store passwords or API keys in defaults or vars files committed to version control. Use Ansible Vault to encrypt sensitive variable files, reference them via include_vars with vault password prompts or key files, and decrypt only at runtime. For production deployments, integrate with HashiCorp Vault or AWS Secrets Manager via lookup plugins. On client projects, I keep vault-encrypted secrets separate from role code entirely, injecting them only during playbook execution.

Collections bundle roles, modules, plugins, and documentation into a single distributable namespace, ideal for complex ecosystems requiring coordinated components. Roles remain simpler for single-purpose automation like configuring one service. If your automation spans multiple related functions sharing common libraries or custom modules, migrate to a collection. For straightforward server provisioning tasks I handle regularly, individual roles stay more maintainable and easier to understand than collection overhead.

Share this article

Quick Contact Options
Choose how you want to connect me: