Transition to Ansible collections with the help of git submodule

Transforming your Ansible roles into collections might be easier than you think

Posted by Christian Jung on Wed, Mar 24, 2021
In Ansible
Tags git, collections, ansible, roles, tower, awx

Ansible collections - a new way to package your content

When planning how to organize and distribute Ansible content, we have to get familiar with the (not so new) concept of Ansible collections. Ansible collections can include plugins, modules, filters, roles and more in one gzipped tar archive. Although Ansible collections are created in a very similar way to roles, a recurring question is: How do I efficiently transition to the new content structure?

In this blog post I want to quickly guide you through the process of how to put existing roles into a single Ansible collection by using the git submodule feature, how to use the collection in your playbook and will give you a simple example on how to automatically build Ansible collections on GitLab using their CI/CD capabilities.

In this scenario I’m assuming you already know Ansible and the concept of roles and collections, and will therefore skip the basics. Let’s assume you have a bunch of existing roles and you want to transition over to using Ansible collections with as little effort as possible.

If you want to learn more about Ansible collections and do a full exercise, I suggest to have a look at the Ansible collections workshop.

Prerequisites

To better illustrate how to transition from roles to collections, I setup a bunch of git repositories:

Note: All these roles are only used for learning purposes and should not be used in a production scenario and will probably not even work for you.

A word on git submodules

Git allows you to have git repositories inside of existing git repositories. This is a very powerful feature which makes it easy to embed an existing repository without the need to manually sync the content.

To add an existing git repository to a sub directory, you can use the following example:

git submodule add <repository URL> <local directory>

This will add a git clone of the specified repository into the directory. Check out the man page or the more detailed section on git submodules in the official Git Book.

When running git pull to update your local repository, this will not check for updates in the submodules. Make sure to use git submodule update --remote to also check for new changes in the submodules. You can also update specific sub modules:

git submodule update --remove <local directory of submodule>

Fully Qualified Collection Name

A Fully Qualified collection Name (FQCN) is built from three components:

  1. the namespace, which is typically the name of the author or organization creating and maintaining the collection

  2. the name of the collection

  3. the name of the module, role, plugin etc. inside the collection

To reference a module, plugin, role etc. you should use the full name to avoid name clashes. Here are a few examples:

ansible.posix.selinux # Configures the SELinux mode and policy.
ansible.builtin.debug # This module prints statements during execution and can be useful for debugging variables or expressions without necessarily halting the playbook. (same as the "debug" module in Ansible <= 2.9)
ansible.builtin.dnf # Installs, upgrade, removes, and lists packages and groups with the `dnf' package manager. (same as "dnf" module in Ansible <= 2.9)

Create the Ansible collection

Let’s first initialize a basic Ansible collection structure. ansible-galaxy can do that for us:

ansible-galaxy collection init <namespace>.<collection name>

In the example below the namespace is cjung and the name of the collection is lamp (since the collection will provide a simple LAMP stack):

ansible-galaxy collection init cjung.lamp

Note: You probably want to use your own name as author.

This creates a directory structure which should look like this:

# tree
.
└── cjung
    └── lamp
        ├── docs
        ├── galaxy.yml
        ├── plugins
        │   └── README.md
        ├── README.md
        └── roles

5 directories, 3 files

Working with dependencies

Ansible collections can have dependencies as well, similar to roles. This can be helpful when reorganizing the roles. Let’s say we have three roles:

  • Web Server Role

  • Database Role

  • General System Hardening Role

Both the Web Server and the Database role will be migrated to a collection. Since they both rely on the hardening role, we could add it to both collections - but that wouldn’t be very efficient and introduce additional effort in maintaining the content. Instead we should create a hardening collection which is defined as a dependency. It’s very simple to do that, by just adding the list of dependencies in our galaxy.yml file.

dependencies: {
  - cjung.hardening # this is just an example, this collection does not exist. ansible-galaxy install will try to find a collection with this name automatically
  - https://gitlab.com/cjung/ansible-lamp-collection/-/raw/master/cjung-lamp-1.0.0.tar.gz
}

When installing the collection with ansible-galaxy collection install the dependencies will automatically be installed as well.

Add existing Roles to the Collection

As you can see, the Ansible roles are supposed to go into the directory cjung/lamp/roles/. To make our life easier, we can use Git Submodules to add our existing roles.

git submodule add https://gitlab.com/cjung/ansible-lamp-apache.git cjung/lamp/roles/lamp_apache
git submodule add https://gitlab.com/cjung/ansible-lamp-mariadb.git cjung/lamp/roles/lamp_mariadb
git submodule add https://gitlab.com/cjung/ansible-lamp-php.git cjung/lamp/roles/lamp_php

After adding the submodules, make sure to commit your changes.

Use the Ansible Collection in your Playbook

It’s good practice to specify the roles and collections used in your playbook as requirements. If you’re planning to use Red Hat Ansible Tower or AWX, specifying the requirements is a must. Otherwise they will not be automatically installed before running the playbook.

The syntax for a collection requirements file looks like this:

---
collections:
# point to a collection archive
- https://gitlab.com/cjung/ansible-lamp-collection/-/raw/master/cjung-lamp-1.0.0.tar.gz
# point to a collection on Ansible Galaxy
- awx.awx

You can then easily install the collection automatically:

ansible-galaxy collection install -r collections/requirements.yml

Note: The name of the file has to be collections/requirement.yml relative of your project root directory, to allow Ansible Tower or AWX to automatically install it for you. Also make sure to remove the role from your roles/requirements.yml - or better delete the file entirely, if no longer needed.

Update your Playbooks

Since you’re now using an Ansible collection for your roles, you have to update your playbooks accordingly. This is easier than it might sound.

The shortcut

To make the transition easier, you can use the collections keyword in your roles or playbook, This will tell Ansible to search for the roles (modules, plugins etc.) in the specified Ansible collections.

You simply list all the collections you need, in your playbook:

- hosts: all
  collections:
    - my_namespace.my_collection

Or in your role:

# myrole/meta/main.yml
collections:
  - my_namespace.first_collection
  - my_namespace.second_collection
  - other_namespace.other_collection

This is a shortcut to get you started more quickly, but it is not the recommended solution. Instead of using the collections keyword, you should update your Ansible roles and playbooks to use the FQCN.

If you’re using the collections keyword, you rely on how Ansible internally searches for content. Problems can occur if the name of a role or module is found in several collections and is therefore not unique. This can lead to unpredictable results when running your Ansible playbook.

You can avoid this kind of problem by using the FQCN syntax as described in the following chapter.

The proper way

When adding Ansible collection support, a new naming syntax for Ansible modules, roles, plugins, etc. was introduced. This is called the Fully Qualified Collection Name, or FQCN for short.

You can use the Apache role in your playbook like this:

---
- name: setup a lamp server
  hosts: lamp_server
  become: true
  roles:
  - lamp_apache

After switching to a collection, it becomes:

---
- name: setup a lamp server
  hosts: lamp_server
  become: true
  roles:
  - cjung.lamp.lamp_apache

As you can see, the namespace and collection name is put in front of the role name. This concludes all the necessary steps to migrate from Ansible roles to collections.

CI/CD Pipeline

This section is for advanced readers who want to use CI/CD pipelines to automatically build new Ansible collections every time a change is pushed to the Git repository.

If you’re using many different Ansible roles, rebuilding the Ansible collection manually can become a drag. I’m using GitLab quite heavily and built a simple CI/CD pipeline to automate the process.

# this is a container image with Ansible and the python modules for AWS, Azure and Google Cloud
image: registry.gitlab.com/cjung/podman-examples/ansible-cloud

stages:
  - check
  - build

build:
  stage: build
  before_script:
  # configure git with the variables from the current user profile
  - git config --global user.name "${GITLAB_USER_NAME}"
  - git config --global user.email "${GITLAB_USER_EMAIL}"
  script:
  # build the actual Ansible collection found in the sub directory
  - ansible-galaxy collection build cjung/lamp/
  # add the new file
  - git add .
  # commit the file to the git repository - the "skip ci" keyword will make sure you're not creating an endless CI/CD loop
  - git commit -m "[skip ci] automated build of ansible collection"
  # push the actual changes
  - git push "https://${GITLAB_USER_NAME}:${CI_GIT_TOKEN}@${CI_REPOSITORY_URL#*@}" HEAD:master
  only:
  - master

# Optionally run ansible-lint on each role to find syntax errors or other issues
lint-apache:
  stage: check
  script:
  - ansible-lint -r roles/lamp_apache/

lint-mariadb:
  stage: check
  script:
  - ansible-lint -r roles/lamp_mariadb/

lint-php:
  stage: check
  script:
  - ansible-lint -r roles/lamp_php/

To really make this work, you have to set the following variables in your GitLab CI/CD configuration:

  • CI_GIT_TOKEN: you have to create a personal access token in GitLab and add it as a variable. This is necessary to automatically push a new version of the Ansible collection to the git repository.

  • GIT_SUBMODULE_STRATEGY: recursive - to make sure GitLab does also clone the git submodules repositories, the variable has tobe set to recursive.

Now you can run the GitLab pipeline manually or push a change into the repository to trigger an automated build. You can also use GitLab hooks to trigger a build when changes are pushed to the individual roles.