Ansible - Roles 2/2 (Glances Role)

In the previous part of this article, we had a look at Ansible Roles and how you can use them. Now, let's develop a role on our own. It's really a piece of cake.

Ansible - Roles 2/2 (Glances Role)
Photo by Leone Venter / Unsplash

In the previous part of this article, we had a look at Ansible Roles and how you can use them. Now, let's develop a role on our own. It's really a piece of cake.

Creating Roles

It is not easy to find an example that spoilers too much or is too boring, but I think, this one may be interesting for you. In my Job, it is very common to automate even trivial things. This time, let's automate the deployment of Glances.

Glances (the project)

You don't know Glances? It's basically "top" on steroids. It is a software, written in Python, that gathers and displays data from a host. In contrast to most other tools, glances aggregates different data from sensors, network, CPU, disk and allows the extension through plugins. The below screenshot will demonstrate this a bit better.

Screenshot - Glances console

But, there is even more. You can run Glances in a server mode and connect via Browser or Glances from your local machine to a remote machine.

Screenshot - Glances Web

Let's do exactly this via Ansible and create a role for the Glances web server. This might be handy for a home server, too. For now, I will focus on compatibility with Fedora, only.

Hint
The guide is tested on Fedora 35/36 with Ansible Core 2.12.5.

Directory Layout

The first thing we need to create is a proper directory layout for a new role. You can do this manually or use the ansible-galaxy command to take care of it.

Let's start with the command-way.

# Init a new role
$ ansible-galaxy role init glances

This will create a directory and some content like this.

glances/
├── defaults        # Default variables
│   └── main.yml
├── files           # Files that can be copied
├── handlers        # All the handlers
│   └── main.yml
├── meta            # Meta data for Galaxy and ansible-galaxy
│   └── main.yml
├── README.md       # Document your role here
├── tasks           # Here we will put all the tasks
│   └── main.yml
├── templates       # Templates for the template plugins/modules
├── tests           # some boilerplate for tests
│   ├── inventory
│   └── test.yml
└── vars            # Variables that can be included
    └── main.yml
Default directory layout for Ansible roles

This is a perfect example for the start. I have also added some comments to explain which directory holds which kind of data. Anyway, for our use case, we can strip the directory down to the below.

glances/
├── defaults
│   └── main.yml
├── handlers
│   └── main.yml
├── meta
│   └── main.yml
├── README.md
├── tasks
│   └── main.yml
└── templates
Customized directory layout for Ansible roles

We will have a look at each of the files in the next sections.

Content

Now, we just need to fill the role with our usual content. Everything that was before in the tasks: section of a playbook will go into the tasks/main.yml file, everything that can be tuned through the vars: statement should have a default value in the defaults/main.yml file.

meta/main.yml

The first file, I am always touching, is this one. It is mostly meant for publishing purposes, but also holds dependencies to other roles. Dependencies will be resolved by Ansible automatically and executed once, before the actual role is applied.

In most cases, we can keep it simple:

---
# roles/glances/meta/main.yml

galaxy_info:

  author: "Daniel Schier"
  description: "Install and configure Glances as server."
  license: "BSD-3-Clause"

  min_ansible_version: 2.10

  galaxy_tags:
    - "package"
    - "service"

dependencies: []
...
glances/meta/main.yml

If you intend to include the role in a collection, it's even simpler.

---
# roles/glances/meta/main.yml

dependencies: []
...
glances/meta/main.yml

We will have a look at Ansible collections in the next Ansible article. For now, you can stick to "just" roles.

defaults/main.yml

The defaults are meant to provide sane default values for variables. I am often thinking about the question "What may a user change?" to identify which variables I want to include.

For example, a user may want to change the package names to avoid adding additional tasks for dependencies. Therefore, I am including parametrization for the same. Parametrizing the state of packages and services allows easy manipulation without changing the role.

For the Glances example, it boils down to this.

---
# roles/glances/defaults/main.yml

## Package Management

glances_package_names:
  - "glances"
  - "python3-bottle"
glances_package_state: "present"

## Configuration Management

glances_web_port: "61208"

## Service Management

glances_web_service_name: "glances-web.service"
glances_web_service_state: "started"
glances_web_service_enabled: true

## Firewall Management

glances_web_firewall_port: "{{ glances_web_port }}/tcp"
glances_web_firewall_state: "enabled"
glances_web_firewall_zone: ""
glances_web_firewall_immediate: true
glances_web_firewall_permanent: true
...
glances/defaults/main.yml

This should give us plenty of room for future improvements and configuration options for the user.

tasks/main.yml

I often switch between defaults and tasks to ensure that everything is aligned and working. For this role, it is pretty straight forward. We need to install the packages, provide some configuration, start a service and configure the firewall.

But wait, there are 2 little issues here. First, Glances does not ship a service file and second, we only want to configure the firewall if firewalld is installed.

As a result, we will have a tasks/main.yml file that looks like this:

---
# roles/glances/tasks/main.yml

## Package Management

- name: "Manage glances Packages"
  ansible.builtin.package:
    name: "{{ glances_package_names }}"
    state: "{{ glances_package_state }}"
  become: true
  notify:
    - "Restart glances Service"
  tags:
    - "glances"
    - "package"

## Configuration Management

- name: "Manage glances Service File"
  ansible.builtin.template:
    src: "{{ glances_web_service_name }}.j2"
    dest: "/etc/systemd/system/{{ glances_web_service_name }}"
    owner: "root"
    group: "root"
    mode: 0644
  become: true
  notify:
    - "Restart glances Service"
  tags:
    - "glances"
    - "configuration"

## Service Management

- name: "Manage glances Service"
  ansible.builtin.systemd:
    name: "{{ glances_web_service_name }}"
    state: "{{ glances_web_service_state }}"
    enabled: "{{ glances_web_service_enabled }}"
    daemon_reload: true
  become: true
  when:
    - "glances_package_state != 'absent'"
  tags:
    - "glances"
    - "service"

## Firewall Management

- name: "Gather package Facts"
  ansible.builtin.package_facts:
    manager: "auto"
  tags:
    - "glances"
    - "package"

- name: "Manage glances Firewall Policy"
  ansible.posix.firewalld:
    port: "{{ glances_web_firewall_port }}"
    state: "{{ glances_web_firewall_state }}"
    zone: "{{ glances_web_firewall_zone | default(omit) }}"
    immediate: "{{ glances_web_firewall_immediate }}"
    permanent: "{{ glances_web_firewall_permanent }}"
  become: true
  when:
    - "'firewalld' in ansible_facts.packages"
  tags:
    - "glances"
    - "firewall"
...
glances/tasks/main.yml

As you can see, we have introduced two more topics. We need a handler to restart the glances service" and need a template for the service file.

Furthermore, we are taking care of the firewall situation with the "ansible.builtin.package_facts" module. It is pretty useful for situations like this.

Lastly, we needed to use the systemd module instead of the service module, since we need the daemon_reload flag. This flag is taking care of telling systemd, that there is a new/changed unit file.

handlers/main.yml

Writing the handler for our role is a piece of cake, now that we are rolling.

---
# roles/glances/handlers/main.yml

- name: "Restart glances Service"
  ansible.builtin.systemd:
    name: "{{ glances_web_service_name }}"
    state: "restarted"
    daemon_reload: true
  become: true
  when:
    - "glances_package_state != 'absent'"
  tags:
    - "glances"
    - "package"
    - "configuration"
    - "service"
...
glances/handlers/main.yml

This handler is triggered every time the package is touched (for example updates) or the service file is changed (which is created through Ansible).

templates/glances-web.service.j2

The last functional piece we need to tackle is the Service file for systemd. For now, we know the following:

  • the command to start the glances web server is glances --webserver
  • we can configure the port with --port PORT
  • the service will require network from the kernel (obviously)

Without going into too many details, we can write a simple systemd unit file, that looks like this.

{{ ansible_managed | comment }}

[Unit]
Description=Service to handle the glances web server.

Wants=network-online.target
After=network-online.target

[Service]
Restart=on-failure

ExecStart=/usr/bin/glances --webserver --port {{ glances_web_port }}
Type=simple

[Install]
WantedBy=default.target
glances/templates/glances-web.service.j2

It's pretty minimal, but it will do the trick.

README.md

Since we want to inform users about our role and document how it works, it is also a good idea to provide a README.md. This file should contain some useful examples and information.

I will not provide any example here, but keep in mind that should always provide the amount of documentation that you expect from any other publisher.

What's Next?

So far, the role is written. If you want to push this example a bit more, here are some ideas what can be done:

  • secure the web server with a username and password
  • allow passing more configuration
  • create a second service that will start the gRPC server and connect with glances directly to a glances server
  • change/add/remove plugins
  • add a way to remove Glances and all configuration
  • add a way to gracefully change the port
  • add support for other Linux distributions

For now, I think we should stop writing code, but finally see some results.

Execute the Role

Finally, we can execute the role. Therefore, we need to decide where we want to put our role to make use of it, and we need to write a super minimal playbook, that hooks up the role.

First, we need to make the role available to your playbooks. This can be done by either:

  • put it into "/etc/ansible/roles/"
  • put it into "~/.ansible/roles/"
  • put it in a "roles/" directory, next to your playbook

Next, we need to define our playbook. This was already discussed in the previous part of the article. For a local execution, it boils down to a simple playbook like this.

---
- hosts: "localhost"

  tasks:

    - name: "Import glances Role"
      ansible.builtin.import_role:
        name: "glances"
playbook.yml

And lastly, you can run the playbook.

# Run the playbook
$ ansible-playbook playbook.yml

Now, you can easily write new roles, add them to your playbook and re-use them whenever you need.

Publish the Role

I assume, you also want to know, how you can publish a role, don't you? In general, there are two options: "plain code" or "via Ansible Galaxy".

Just the code

The first option is the easiest. Just create a repository and push your code to it. Others can download the code and use it on their own. It's not very elegant, but still works.

Via Ansible Galaxy (and GitHub)

To make your role more discoverable, it may be a good idea to publish it to Ansible Galaxy. This can be done via CI/CD, but also manually.

Ansible Galaxy is somewhat limited, when it comes to the import of roles, and you need to publish your Ansible Role on GitHub first. You will also need a GitHub Account to create an Account on Ansible Galaxy.

Afterwards, you can use the Ansible Galaxy web interface and "Add Content" to your namespace. Just choose the role from your GitHub organization/personal workspace, and you are good to go.

Another method is, to use the ansible-galaxy command line utility.

To be honest, I am not the biggest fan of the current state of Ansible Galaxy. There is a lot of work done to improve it and provide a better, self-hostable solution, but it seems that still needs some time.

Therefore, I am publishing my roles in an Ansible Collection via GitHub. You can find the Glances role and many others in the while-true-do.io organization in the whiletrueodio.general repository.

Before closing the article, let me provide some useful documentation for further reading.

Glances — Glances 3.2.3.1 documentation
Roles — Ansible Documentation
Creating Roles — Ansible Documentation
Contributing Content — Ansible Documentation
while-true-do.io
Open Source - Development, Infrastructure, Community - while-true-do.io

Conclusion

That's it for now with Ansible Roles. They are a nice way to make code more reusable and manageable. If done right, you don't need to write the same logic over and over again, but enhance your existing code and all your playbooks can benefit from it.

Do you use Ansible Roles already? Do you craft your own? I would love to hear from your experiences. :)