Versioning #

In the modern infrastructure development and operations lifecycle, change is unavoidable. Your Ansible automation code will keep evolving with operating system updates, security patch additions, and changes to the application requirements you manage. However, in an enterprise environment with dozens of critical production servers, updating automation code without strict version control is an instant recipe for disaster. One small change to a variable name inside a role can break the entire deployment flow across various teams using that role without them knowing. This is where applying a robust versioning system to every Ansible role becomes essential.


The Importance of Versioning in the Infrastructure Lifecycle (IaC) #

When you treat infrastructure as code (Infrastructure as Code), you must apply the same software engineering discipline to your automation code as you do to application code. One of the most crucial disciplines is release management.

Without a versioning system, organizations typically experience the “works on my machine” phenomenon at the infrastructure level. Imagine Team A creates a role to configure PostgreSQL servers and puts it in the central Git repository on the main branch. Team B and Team C use that role for their respective projects by cloning the repository directly without version pinning. One day, Team A needs to add a database encryption feature that requires changing the configuration structure and altering certain default parameters. Team A commits and pushes directly to the main branch. The next day, when Team B runs their routine deployment playbook, their servers suddenly error out because they automatically pulled the latest code from the main branch, which is no longer compatible with their configuration.

By applying versioning:

  1. Protects Production Servers — Every project locks its role usage to a specific version that has been thoroughly tested in the staging environment.
  2. Enables Controlled Releases — Infrastructure teams can release new features and bug fixes without fear of breaking other systems not yet ready to migrate.
  3. Auditability and Compliance — You can know precisely which automation version ran on a particular server at a particular time, which is very important for security forensics analysis and compliance audits.

Applying Semantic Versioning (SemVer) to Ansible Roles #

The versioning system most widely adopted by the technology community and highly recommended for Ansible roles is Semantic Versioning (SemVer). SemVer uses a three-number format separated by dots: MAJOR.MINOR.PATCH (example: v2.1.4). Each number has a specific meaning that communicates the compatibility level of changes inside the role to users:

1. MAJOR (First Number Increase) #

A MAJOR version increase is done when you release changes that are incompatible with previous versions (breaking changes). Role users must modify their playbooks or variables for this new role version to run successfully.

Examples of actions triggering a MAJOR version increase on an Ansible role:

  • Changing mandatory variable names (for example db_port to postgresql_port_number).
  • Changing input variable structures from plain strings to lists or dictionaries.
  • Dropping support for a particular operating system or distribution (for example no longer supporting Ubuntu 18.04).
  • Raising the minimum Ansible version required by the role.
  • Drastically changing default installation directory paths.

2. MINOR (Second Number Increase) #

A MINOR version increase is done when you add backward-compatible new features. Users can directly update the role version without changing their existing playbook configurations.

Examples of actions triggering a MINOR version increase:

  • Adding support for a new operating system distribution (for example adding Rocky Linux 9 to the compatibility list).
  • Adding new optional variables in defaults/main.yml to provide extra customization options.
  • Adding new optional tasks that only run if a certain variable is enabled (conditional).

3. PATCH (Third Number Increase) #

A PATCH version increase is done when you release backward-compatible bug fixes. These changes only focus on correcting mistakes without changing the role’s expected operational behavior.

Examples of actions triggering a PATCH version increase:

  • Fixing typos in Jinja2 configuration templates.
  • Fixing wrong when condition logic that caused tasks to be skipped unintentionally.
  • Fixing file permission issues on directories created by the role.

Managing Versions with Git Tagging #

The most practical, industry-standard way to mark Ansible role versions stored in a Git repository is using Git Tags. Tags act as static pointers to specific commits in your Git repository’s history.

Here’s the CLI workflow to create and release a new role version using Git tags:

# 1. Make sure all changes are committed and clean
git status

# 2. Commit the changes with a clear message
git add .
git commit -m "feat: add support for Rocky Linux 9"

# 3. Create an annotated tag following SemVer rules
# It's strongly recommended to use the letter 'v' as the version prefix
git tag -a v1.2.0 -m "Release v1.2.0: Added Rocky Linux 9 support and SSL port customization options"

# 4. Push the commit to the remote repository (e.g. GitHub/GitLab)
git push origin main

# 5. Push the newly created tag to the remote repository
git push origin v1.2.0

If a critical issue occurs after a release and you want to roll back the local tag numbering for an emergency fix:

# Delete the local tag
git tag -d v1.2.0

# Delete the tag on the remote repository
git push --delete origin v1.2.0

Using Git tags ensures the role code at that version is permanent and can’t be accidentally changed by new commits on the main branch.


Sharing Roles: Ansible Galaxy vs Private Git Repositories #

There are two main methods you can use to distribute and consume versioned Ansible roles:

1. Ansible Galaxy (Public) #

Ansible Galaxy is a public community hub managed by Red Hat for openly sharing Ansible roles. It’s perfect if you want to contribute to the open-source community or use industry-standard roles managed by trusted vendors (like the Docker installation role from Nginx or Geerlingguy). You reference these roles directly using their namespace names (for example geerlingguy.nginx).

2. Private Git Repositories (Internal Enterprise) #

For most companies, infrastructure code is confidential because it contains internal architecture details. Therefore, internal roles are usually stored on the organization’s private Git servers (like GitHub Enterprise, self-hosted GitLab, or Azure DevOps). You can split roles into separate Git repositories (one repository per role, for example https://git.company.com/ansible/role-mysql.git) and secure access using SSH keys or personal access tokens.


Dependency Declarations and Version Pinning via requirements.yml #

To manage role installation from various sources (both Galaxy and private Git) along with their specific versions in your playbook project, you use a file called requirements.yml. This file acts like the package.json file in Node.js or the Gemfile in Ruby.

Here’s an example of a safe dependency declaration file implementation applying version pinning techniques:

# File: requirements.yml
---
roles:
  # ✓ RECOMMENDED: Pin to a specific Git Tag (SemVer)
  - name: company.mysql
    src: "[email protected]:mycompany/ansible-role-mysql.git"
    scm: git
    version: "v2.1.0"

  # ✓ ALTERNATIVE RECOMMENDATION: Pin to a specific Commit Hash (Very Safe)
  - name: company.nginx
    src: "[email protected]:mycompany/ansible-role-nginx.git"
    scm: git
    version: "a1b2c3d4e5f67890abcdef1234567890abcdef12"

  # ✓ GALAXY RECOMMENDATION: Pin to a Community Release Version
  - name: geerlingguy.docker
    src: geerlingguy.docker
    version: "7.1.0"

  # ✗ ANTI-PATTERN: Referring to the main branch (not safe for production)
  - name: company.common
    src: "[email protected]:mycompany/ansible-role-common.git"
    scm: git
    version: main # Always pulls the latest commit, vulnerable to breakage without warning

Why Is Avoiding the main/master Branch a Critical Rule? #

Avoiding branch references like version: main or version: develop in production environments is a mandatory rule. If you lock a dependency to a branch, every time you run the role installation command, Ansible pulls the latest version from that branch. If the role development team is doing unstable internal testing on that branch, your production servers are immediately affected. Use commit hashes or Git tags absolutely for all production releases.


Installation Automation Using the ansible-galaxy CLI #

After defining all role dependencies in the requirements.yml file, you use the ansible-galaxy command-line utility to download and install those roles into your execution environment.

The standard command to install all declared roles:

ansible-galaxy install -r requirements.yml

By default, Ansible installs roles into the system default directory (like ~/.ansible/roles or /usr/share/ansible/roles). However, the best practice in playbook development is storing roles locally inside your own project directory for easy portability. You can specify the destination folder using the --roles-path argument:

# Install roles directly into the local project directory
ansible-galaxy install -r requirements.yml --roles-path ./roles/

To avoid writing this argument repeatedly on the command line, you can permanently configure the installation path inside your project’s ansible.cfg configuration file:

# File: ansible.cfg
[defaults]
roles_path = ./roles:~/.ansible/roles
host_key_checking = False

If one of the roles listed in requirements.yml was already installed previously in your local folder, the ansible-galaxy install command by default skips the download process to speed up execution. If you just updated the tag version in requirements.yml and want to force Ansible to download the new version and overwrite the old code, you must use the --force flag:

# Force role reinstallation to update its version
ansible-galaxy install -r requirements.yml --roles-path ./roles/ --force

Safe Upgrade and Rollback Strategies in Production #

Updating role versions in production requires careful planning and execution. You shouldn’t just update the requirements.yml file on the production main branch and run the playbook right away. Here’s the recommended controlled update protocol:

1. Reading the CHANGELOG.md File #

Every role author must provide a CHANGELOG.md file documenting every change history. Before updating, read the changelog to understand whether the release contains breaking changes requiring adjustments on your playbook side.

Example of a good CHANGELOG.md format:

# Changelog - Company Nginx Role

All important changes to this role will be documented in this file.

## [v2.0.0] - 2026-05-10
### Breaking Changes
- The `nginx_port` parameter has been renamed to `nginx_http_port`.
- The minimum supported Ansible version is now 2.15.

### Migration Guide
1. Update your `group_vars/all.yml` file to change the `nginx_port` key to `nginx_http_port`.
2. Make sure your control node runs Ansible >= 2.15 before running this role.

## [v1.2.0] - 2026-03-04
### Added
- Added automation support for Debian 12.
- New `nginx_client_max_body_size` variable with a `10M` default value.

2. Staging Test Cycle Before Production #

Never skip testing in a non-production environment. The role migration workflow must follow this scheme:

Create a New Feature Branch in the Playbook Repository
                      │
Update the role version in requirements.yml (Staging Branch)
                      │
Run 'ansible-galaxy install -r requirements.yml --force'
                      │
Apply the playbook to Staging/Testing servers
                      │
Did the Staging test succeed without errors?
        ├── NO: Fix the variable configuration or playbook code
        └── YES: Merge the Staging Branch into the Main Branch (Production Ready)

3. Fast Rollback Protocol #

If a failure undetected during the staging phase occurs after updating in production, you must have an emergency recovery plan (rollback plan) ready to execute instantly.

The advantage of using Git tag/commit hash-based version pinning in requirements.yml is the ease of the rollback process:

  1. You just edit the requirements.yml file to return the version parameter value to the previous stable version tag (for example from v2.0.0 back to v1.2.0).
  2. Run the forced installation command: ansible-galaxy install -r requirements.yml --force.
  3. Re-run the Ansible playbook. The system is immediately reconfigured using the old stable version logic, minimizing production service downtime.

Versioning Strategy Decision Flowchart #

To help infrastructure teams determine when to raise a role version and how to reference it in project dependency files, use the following decision flowchart:

flowchart TD
    A["Start Role Code Change"] --> B{"Are there mandatory variable or target OS changes?"}
    B -- "Yes" --> C["Change to a new MAJOR version in the Git tag"]
    C --> F["Write the migration guide in CHANGELOG.md"]
    B -- "No" --> D{"Are there new optional feature additions?"}
    D -- "Yes" --> E["Change to a new MINOR version in the Git tag"]
    D -- "No" --> G["Change to a new PATCH version in the Git tag"]
    E --> H["Push the new Git tag to the remote repository"]
    G --> H
    F --> H
    H --> I{"Where will the role be used?"}
    I -- "Internal Organization" --> J["Pin the version using a Git tag in requirements.yml"]
    I -- "External / Public" --> K["Publish to Ansible Galaxy & pin the release version"]
    J --> L["Run the installation with ansible-galaxy install --force"]
    K --> L
    L --> M["Do comprehensive testing on staging servers"]

Summary #

  • Semantic Versioning Is Mandatory — Apply SemVer rules (MAJOR.MINOR.PATCH) strictly to all role development so users understand the implications of version updates.
  • Dynamic Git Tagging — Use annotated Git tags to mark stable role release versions in the Git repository; avoid using static branch references.
  • requirements.yml Dependency Pinning — Declare all external role dependencies in the requirements.yml file and pin their versions using specific tags or commit hashes.
  • Branch Anti-Pattern Strategy — Never refer to main branches (main/master) in production environments because they’re vulnerable to unexpected unstable changes.
  • Local Path Configuration — Set the roles_path option in the ansible.cfg file to ensure roles install inside your local project directory for easy portability.
  • Controlled Release Lifecycle — Evaluate the CHANGELOG.md file and do comprehensive staging testing before raising role versions on production servers.
  • Fast Recovery Scheme — Prepare an instant rollback protocol by returning the version tag in requirements.yml to the old version and running reinstallation with the --force option.
  • Repository Encapsulation — Separate each Ansible role into an isolated Git repository for easier version history tracking and security access sharing.

← Previous: Parameterized Next: User & Permission →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact