My Take on Ansible Collections
Tue Jan 30, 2024 · 1019 words · 6 min
TagsΒ :  ansible

Prelude

It's 2024, and I don't know about you, but I've still been hesitant when it comes to Ansible collections. What is this? Do I need to care at all, besides using community.general here and there? Why can't I just use roles like I was doing for the last 1000 years?

So, what happened? Ansible Galaxy stopped working, basically. (Ok, that's a bit unfair maybe – the change was announced long ago, and it makes sense, but still it bit me and quite some other people.) I maintain some Ansible roles on Github, and I've noticed that the automated release process broke some time ago.

These days I had some time to investigate, and it turned out that Ansible Galaxy now treats collections as first-class citizens, and roles are 'unofficially deprecated'. πŸͺ¦ At least that's the feeling I get when using the new Ansible Galaxy. It's just become more complicated to deal with roles than with collections there, hasn't it?

Once you start wrapping your head around collections, sooner or later you might notice that this is a nice idea though. There is a cool ecosystem around it (ansible-test and antsibull-changelog, to name just two) and RedHat also had some reasonable motives to introduce them. I think the greatest benefit is that you can share common plugins between roles of the same collection, whereas before you had to duplicate the code. It's not a use case that I personally have, and I still am hesitant with collections, but since this is the way to go (and because I got a lot of annoying errors when I tried to publish a role), I decided to give it a shot.

And so I set out to migrate my first role to a collection: bellackn.homelab. There is some documentation about how to do this, but I personally think it falls a bit short. It doesn't explain much (like, for example do I still need the metadata inside a role folder? How do I properly test my collections and roles?), and so I had to hop between doc pages, GitHub issues, and blog posts to stitch together something that works. So let's try to make it better.

Setting Up a Collection

To get started, there is a command that gives us the correct directory structure already:

$ ansible-galaxy collection init your_namespace.collection_name
- Collection your_namespace.collection_name was created successfully

That leaves us with the following:

your_namespace
└── collection_name
    β”œβ”€β”€ README.md
    β”œβ”€β”€ docs
    β”œβ”€β”€ galaxy.yml
    β”œβ”€β”€ meta
    β”‚   └── runtime.yml
    β”œβ”€β”€ plugins
    β”‚   └── README.md
    └── roles

2 crucial files stand out here: galaxy.yml and meta/runtime.yml. The first is kind of what you would expect to be in a role's meta/main.yml. You need to set a namespace, a title, tags, and other things for your collection in there. It's well documented inside the file, just check it. The latter is a bit more technical, but for basic usage, just note that you have to define the minimum Ansible version for your collection here. And, what's that – a roles folder?!

Converting a Standalone Role Into a Collection Role

A single Ansible collection can hold multiple roles. They can later be accessed like this:

- name: run generic role
  ansible.builtin.import_role:
    name: your_namespace.collection_name.the_role

Let's check if that's true. Create a generic role in this folder:

$ ansible-galaxy role init the_role
- Role the_role was created successfully

# that's what we get:
your_namespace
└── collection_name
    β”œβ”€β”€ README.md
    β”œβ”€β”€ docs
    β”œβ”€β”€ galaxy.yml
    β”œβ”€β”€ meta
    β”‚   └── runtime.yml
    β”œβ”€β”€ plugins
    β”‚   └── README.md
    └── roles
        └── the_role
            β”œβ”€β”€ README.md
            β”œβ”€β”€ defaults
            β”‚   └── main.yml
            β”œβ”€β”€ files
            β”œβ”€β”€ handlers
            β”‚   └── main.yml
            β”œβ”€β”€ meta
            β”‚   └── main.yml
            β”œβ”€β”€ tasks
            β”‚   └── main.yml
            β”œβ”€β”€ templates
            β”œβ”€β”€ tests
            β”‚   β”œβ”€β”€ inventory
            β”‚   └── test.yml
            └── vars
                └── main.yml

That's what we're used to. Nice. Let's make that whole construct 'testable' by adding some dummy task, so we can see something later. Add the following to roles/the_role/tasks/main.yml:

---
- name: debug
  ansible.builtin.debug:
    msg: hello from the_role!

Alright, now we should be able to build the collection. That happens by issuing the following command that will give us a tarball:

$ ansible-galaxy collection build
Created collection for your_namespace.collection_name at /Users/nico/dev/your_namespace/collection_name/your_namespace-collection_name-1.0.0.tar.gz

Time to test if that works as expected. Let's set up a small project that uses our collection. On the same level as the your_namespace folder, create a folder myproject and add the following files:

requirements.yml:

---
collections:
  - name: your_namespace.collection_name
    source: ../your_namespace/collection_name/your_namespace-collection_name-1.0.0.tar.gz
    type: file

This is the way we tell Ansible Galaxy to install a collection from the local filesystem.

playbook_test.yml:

---
- name: test
  hosts: localhost
  tasks:
    - name: hello role
      ansible.builtin.import_role:
        name: your_namespace.collection_name.the_role

Other valid ways to achieve the same without the tasks dict would be:

# [...]
roles:
  - your_namespace.collection_name.the_role

or:

# [...]
collections:
  - your_namespace.collection_name
roles:
  - the_role

This will call the role from our collection. that's all we need for now. Next step is installing the collection from the tarball we've created. Run this command from within the myproject folder.

$ ansible-galaxy install -r requirements.yml
Starting galaxy collection install process
Process install dependency map
Starting collection install process
Installing 'your_namespace.collection_name:1.0.0' to '/Users/nico/.ansible/collections/ansible_collections/your_namespace/collection_name'
your_namespace.collection_name:1.0.0 was installed successfully

Great, so now we should be able to run our playbook. Let's see...

$ ansible-playbook playbook_test.yml
[WARNING]: No inventory was parsed, only implicit localhost is available
[WARNING]: provided hosts list is empty, only localhost is available. Note that the implicit localhost does not match 'all'

PLAY [test] **********************************************************************************************************************************************

TASK [Gathering Facts] ***********************************************************************************************************************************
ok: [localhost]

TASK [your_namespace.collection_name.the_role : debug] ***************************************************************************************************
ok: [localhost] => {
    "msg": "hello from the_role!"
}

PLAY RECAP ***********************************************************************************************************************************************
localhost                  : ok=2    changed=0    unreachable=0    failed=0    skipped=0    rescued=0    ignored=0

Aaand it works!

Summary

When you know what to look for, moving from roles to collections isn't hard. It's basically just pushing some files from A to B. But that's only the case for the most basic scenarios. There's more to it; we didn't touch linting, unit tests, integration tests, or publishing a collection. I'm might cover this a bit more in a follow-up post.

I hope you could take something with you from this. Cheers!


about · blog · contact · legal notice · home