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 might cover this a bit more in a follow-up post.
I hope you could take something with you from this. Cheers!