Getting started with Ansible
Tags: ansible, tutorial
Published: 2020-12-02 14:00:55

**WORDS BY** Tadej Borovšak

In our [previous blog post] (https://steampunk.si/blog/introduction-to-configuration-management-tools/), we looked at what configuration management tools bring to the table. And in this post, we will take a closer look at Ansible, our configuration management tool of choice. So if you want to learn what Ansible is, what it does, and how it works, this is the post for you.

We will start by introducing [Ansible] (https://www.ansible.com/) and some of the benefits we gain when using it. Next, we will describe the differences between three different Ansible packages and give some advice on picking the right one for our use case. We will continue with the installation instructions and finally wrap up with an interactive walkthrough of Ansible’s basic functionality like running Ansible playbooks and installing extra Ansible content.

#**What is Ansible**
We all probably heard about automation systems that suffer from *managing the management* and *automating your automation* issues. Ansible not only brings order to our infrastructure, but it also does it sustainably.

Because Ansible is **agentless**, dealing with daemons being down, upgrading agents, and patching agent security vulnerabilities are a thing of the past. All we need to do is keep our control node (the computer where the Ansible runs) updated, and we should be safe.

But what does Ansible do exactly? **Ansible’s primary responsibility is to reliably enforce the desired state on a set of targets.**

We usually keep the state description in a relatively human-friendly and machine-executable file called an Ansible playbook. We have a bit more flexibility when it comes to storing information about targets. We can use:

1. **Static inventory files** that contain login information for our targets.
2. **Inventory plugins** that fetch connection information from some canonical sources like IaaS API.

**Enforcing the desired state** in the Ansible world can mean almost anything: writing a configuration file, creating a virtual machine in the cloud, adding a VLAN to a network switch, etc. All we need is an Ansible module for performing the task at hand. And there are a lot of them available out there. We will talk more about how to obtain them in the last part of this post.

**Target** (or **host**) is also quite a broad term in the Ansible world. Historically, the primary target type was a virtual machine that Ansible managed via the Secure Shell (SSH) protocol. But today, Ansible can control many other things like containers, networking equipment, and various Infrastructure as a Service (IaaS) platforms. And because Ansible is agentless, getting it to work with a specific piece of software or hardware is usually much easier compared to other configuration management tools.

Now we are ready to start talking about Ansible versions.

#**What Ansible package to use**
Today, there are three Ansible packages that we can use for our day-to-day automatization tasks: `ansible 2-9` , `ansible-2.10`, and `ansible-base-2.10`. Why? Because the monolithic Ansible package was split into a core engine and a few bundles of Ansible content called Ansible Collections for the 2.10 release.

But fortunately for us, selecting the correct package is relatively simple. Just answer the following questions in order and stop when you answer “yes” for the first time:

1. Are you a Red Hat subscriber or want to purchase support? Use **ansible-2.9.**
2. Do you have existing playbooks and no time to learn about Ansible Collections? Use **ansible-2.10.**
3. Use **ansible-base-2.10.**

We made sure examples from this blog post work with all of the versions mentioned above, so you should have no trouble following along.

#**How to install Ansible**
We only need to install Ansible onto the control node (the computer or virtual machine you will run Ansible commands on). The most flexible way of installing Ansible is using `pip`:

```
$ pip install --user "ansible==2.9.*"  # For ansible 2.9
$ pip install --user ansible           # For ansible 2.10 and   newer
$ pip install --user ansible-base      # For ansible-base
```

This will install Ansible only for our user account and not mess up the system if anything goes wrong.

We can also install Ansible using our distribution’s package manager (for example, `dnf` on RHEL 8 or `apt` on Debian-derived distributions).

```
$ yum install ansible  # RHEL and CentOS 7
$ dnf install ansible  # RHEL and CentOS 8
$ apt install ansible  # Debian and Ubuntu
```

But bear in mind that most distributions do not package `ansible-base` and `ansible` 2.10 yet, so you might need to resort to the `pip` method for the time being. Official [installation instructions] (https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html) contain even more installation-related details.

Unfortunately, we cannot use a Windows machine as an Ansible control node. Matt Davis, one of the core Ansible developers, wrote a [blog post] (http://blog.rolpdog.com/2020/03/why-no-ansible-controller-for-windows.html) about the reasons. Matt also gives some hints about what Windows users can do to remedy the situation, so make sure you check his post out.

Once the installation is behind us, we should be able to run the following command without errors:

```
$ ansible --version
ansible 2.9.14
  config file = /etc/ansible/ansible.cfg
  configured module search path = ['/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python3.8/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 3.8.6 (default, Sep 25 2020, 00:00:00) [GCC 10.2.1 20200723]
```

If nothing went awry, we are ready to start playing with Ansible.

#**Taking Ansible for a spin**
In this last section, we will play around a bit with Ansible. We will start by analyzing a small Ansible playbook and explaining the purpose of its components, continue with discussing the Ansible inventory, and conclude our escapade with some custom Ansible content installation.

If you would like to follow along with us, you will need a Linux computer with Ansible and Docker installed.

##Ansible playbook
Let us start with something relatively simple:

```
---
- name: Configure local computer
  hosts: localhost

  tasks:
    - name: Create config file
      ansible.builtin.copy:
        dest: /tmp/my.cfg
        content: Hello, World!
```

Even if you never heard about Ansible playbook or YAML before, you probably guessed that we want to create the `/tmp/my.cfg` file with the `Hello, World!` inside it on our local computer. Let us test this. When we run the `ansible-playbook` command (we assumed that the playbook is stored in the `play.yaml` file), we will see something like this:

```
$ ansible-playbook play.yaml
[WARNING]: provided hosts list is empty, only localhost is available. Note
  that the implicit localhost does not match 'all'

PLAY [Configure local computer] ******************************************

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

TASK [Create config file] ************************************************
changed: [localhost]

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

And sure enough, if we open the `/tmp/my-cfg.txt` file, the hello is there ;) But how did Ansible know what to do? Explanation time ;)

Ansible playbook is nothing more than a list of plays that Ansible executes one after another. For the sake of simplicity, our example contains a single Configure local computer play. The `hosts: localhost` line informed Ansible that we will be messing with our local computer. This is the target that we talked about previously.

All real action happened in the `tasks` section of the play. The `ansible.builtin.copy:` line told Ansible that we want to use the built-in copy module to create a file. And the `dest: /tmp/my-cfg.txt` and `content: Hello` parameter lines provided information to the copy module about our file’s location and what to place inside it, respectively. The line that starts with `- name` serves as documentation for us and is printed to the output when Ansible starts executing the task at hand.

Technically speaking, Ansible did not create a file. It would be more accurate to say that Ansible made sure our text file exists and has the correct content. We can see this technical nitpick in action if we run the same `ansible-playbook` command again:

```
$ ansible-playbook play.yaml
[WARNING]: provided hosts list is empty, only localhost is available. Note
  that the implicit localhost does not match 'all'

PLAY [Configure local computer] ******************************************

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

TASK [Create config file] ************************************************
ok: [localhost]

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

Notice how in the previous run, Ansible reported our file creation task as changed, and in this last run, the output changed to ok. This happened because Ansible determined that the file’s current and desired state are precisely the same, and there is nothing to do.

But our configuration files are rarely entirely static, meaning that we often need to customize their content based on some external information. And this is where variables and `ansible.builtin.template` module come into play - pun intended ;).

Using variables in templates is reasonably straightforward: we just need to place the variable name inside double curly braces, and the template module will replace it with the variable’s value. For example, this is how our parameterized configuration file could look:

```
{{ greeting }} from {{ ansible_distribution }}, World!
```

But before we can use the variable, we need to define it somewhere. There are [many places] (https://docs.ansible.com/ansible/latest/user_guide/playbooks_variables.html) where Ansible will look for variable definitions, but we will keep it simple and provide them as part of the playbook (the `vars` section in the listing below).

```
---
- name: Configure local computer
  hosts: localhost

  vars:
    greeting: Welcome

  tasks:
    - name: Create config file
      ansible.builtin.template:
        src: my.cfg.j2
        dest: /tmp/my.cfg
```

We assumed that we saved our template in a file called `my.cfg.j2` next to our playbook. If we now re-run the `ansible-playbook` command and open the `/tmp/my.cfg` file, it will look something like this:

```
Welcome from Fedora, World!
```

Did you notice that we did not supply the value for the `ansible_distribution` variable, and yet nothing failed? And that we did not talk at all about that *gathering facts* task that just materializes out of nowhere? Looks like we have some explaining to do ;)

If not instructed otherwise, Ansible will collect some information about the target before running playbook tasks. And this gives us access to quite a few magic variables such as `ansible_distribution`. The [full list] (https://docs.ansible.com/ansible/devel/user_guide/playbooks_vars_facts.html) is available in the official Ansible documentation.

Most often than not, when we update the configuration for a service, we also need to restart the service itself. And Ansible has us covered here too. We already saw how Ansible reports back if the task execution changed anything. And we can use that information to notify a handler that makes sure changes are correctly propagated.

This is how our demonstration playbook looks like if we add a dummy handler:

```
---
- name: Configure local computer
  hosts: localhost

  vars:
    greeting: Welcome

  tasks:
    - name: Create config file
      ansible.builtin.template:
        src: my.cfg.j2
        dest: /tmp/my.cfg
      notify: Restart stuff

  handlers:
    - name: Restart stuff
      ansible.builtin.debug:
        msg: Restarting stuff
```

We can use any module in the handler section. In our case, we used the `ansible.builtin.debug` module that just prints a message, but in production-ready scenarios, most of the time we use modules like `ansible.builtin.service` to restart the processes.

If we were to re-run our playbook now, the handler would not be executed because the configuration file did not change. But if we update the `greetings` variable and re-run Ansible, we should see a handler running:

```
$ ansible-playbook play.yaml
[WARNING]: provided hosts list is empty, only localhost is available. Note
  that the implicit localhost does not match 'all'

PLAY [Configure local computer] ******************************************

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

TASK [Create config file] ************************************************
changed: [localhost]

RUNNING HANDLER [Restart stuff] ******************************************
ok: [localhost] => {
    "changed": false,
    "msg": "Restarting stuff"
}

PLAY RECAP ***************************************************************
localhost              : ok=3    changed=1    unreachable=0    failed=0
                         skipped=0    rescued=0    ignored=0
```

And although we can use the same modules in both tasks and handlers sections, there is one crucial difference. Tasks enforce the desired state while handlers execute an action. For example, when we use the `ansible.builtin.service` module in the tasks section, we set the state parameter to `started` or `stopped` while in the handlers section, `restarted` or `reloaded` make more sense.

##Ansible inventory
We did not need an [inventory] (https://docs.ansible.com/ansible/latest/user_guide/intro_inventory.html) until now because we were playing on our Ansible control node. But once we start managing remote targets, we will need a place to store access information about our targets.

But inventory does not only store our targets. It can also arrange them into different groups. We can then run our Ansible playbook against a group of targets instead of a single host and manage them in bulk.

We can write our static inventory files in quite a few different formats. We will use the YAML format in this tutorial (we already use that to write Ansible playbooks, so using it for inventory files keeps things consistent). For example, this is how a typical inventory file looks like:

```
---
all:
  children:
    webservers:
      hosts:
        web1:
          ansible_host: ws1.example.com
          ansible_user: ws1_user
          greeting: Ciao
        web2:
          ansible_host: 10.3.2.14
          ansible_user: ws2_user
          greeting: Hola
    containers:
      hosts:
        demo:
          ansible_connection: community.docker.docker
          greeting: Bonjour
```

In the example above, we defined three groups of hosts: *all, webservers,* and *containers*. The *webservers* group contains two hosts (*web1* and *web2*), and the containers group has a single target (*demo*). The top-level group all includes hosts from both children groups.

The key-value pairs listed under each host are variable definitions available to Ansible playbook tasks when they execute on the selected host. Do note that variables with the `ansible_` prefix have a special meaning. They control things like what address and user to use when connecting to the remote target (when connecting using SSH) or what kind of connection to establish if the default SSH is not what we want.

We do not have access to the fictional web servers from our inventory, but we can create our own Docker container called demo and play with that. The command that will bring our Docker container to life is:

```
$ docker run --rm -d --name demo centos:8 \
    bash -c 'while true; do sleep 10000; done'
```

Now that we have our container up and running, we can replace the `localhost` with `containers` in our Ansible playbook. This small change will allow us to manage our newly created Docker container.

```
---
- name: Configure docker containers
  hosts: containers

  # Rest of the playbook omitted for brevity.
```

On our next run, we also need to update our `ansible-playbook` command a bit and provide an inventory file using the `-i` command-line switch:

```
$ ansible-playbook -i inventory.yaml play.yaml

PLAY [Configure docker containers] ***************************************

TASK [Gathering Facts] ***************************************************
fatal: [demo]: FAILED! => {
  "msg": "the connection plugin 'community.docker.docker' was not found"
}

PLAY RECAP ***************************************************************
demo                   : ok=0    changed=0    unreachable=0    failed=1
                         skipped=0    rescued=0    ignored=0
```

If everything went according to our plans, things failed in a rather spectacular way because we are missing a piece of Ansible functionality that is not part of the base package.

**Note:** If you are reading this tutorial far enough in the future and using the `ansible` package, you might not see the error above. Uninstall the `ansible` package and install `ansible-base` if you are missing some excitement in your life ;)

##Ansible Collections
You probably noticed that all Ansible module names that we used this far were composed of three parts, separated by a dot. This triplet is called a **fully-qualified collection name** (FQCN). The last part is the bare module name. The middle part is the collection name that the module lives in. The first part is a namespace that the collection belongs to.

The `ansible.builtin` collection that we used most of the time comes bundled with all Ansible versions from 2.8 forward. And this is why we had no trouble when we played with our configuration file on the localhost. But when we wanted to connect to our Docker container using the plugin from the `community.docker` Ansible Collection, things failed.

Ansible comes bundled with the `ansible-galaxy` tool that we can use to install additional content. In our case, we want to install the Docker Ansible Collection, which we can do by running the following command:

```
$ ansible-galaxy collection install community.docker
Process install dependency map
Starting collection install process
Installing 'community.docker:0.1.0' to
  '/home/tadej/.ansible/collections/ansible_collections/community/docker'
```

If we re-run our `ansible-playbook` command that failed previously, we should see no error:

```
$ ansible-playbook -i inventory.yaml play.yaml

PLAY [Configure docker containers] ***************************************

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

TASK [Create config file] ************************************************
changed: [demo]

RUNNING HANDLER [Restart stuff] ******************************************
ok: [demo] => {
    "changed": false,
    "msg": "Restarting stuff"
}

PLAY RECAP ***************************************************************
demo                   : ok=3    changed=1    unreachable=0    failed=0
                         skipped=0    rescued=0    ignored=0
```

And we can now also confirm that the configuration file is indeed present in our Docker container:

```
$ docker exec demo cat /tmp/my.cfg
Wellcome from CentOS, World!
```

But how did we know that we must use `community.docker.docker` as the `ansible_connection` value? And where did the Docker Ansible Collection come from?

Well, to be completely honest, we knew the exact FQCN because some of us spend our days lurking in various Ansible channels on IRC ;) But you do not have to.

What you should do instead is visit [Ansible Galaxy] (https://galaxy.ansible.com/), a community portal for the distribution of Ansible content. You can search for the relevant content there, obtain installation instructions for the collection you are interested in, and follow the documentation links to learn more about it.

For example, if we search for docker on Ansible Galaxy and click on the first returned Ansible collection, we will end up on the [Docker Ansible Collection] (https://galaxy.ansible.com/community/docker) overview page.

Ansible Galaxy is also a default source of collections for the `ansible-galaxy` tool. But you probably already guessed that by now, right? ;)

And this concludes our tutorial. But before we head off to the parting words, we should probably clean up the mess we made during the experimentation:

```
$ docker kill demo
$ rm /tmp/my.cfg
```

#**Conclusion**
We hope our short introductory tutorial demonstrated well enough why Ansible is one of the big players in the automation space. Designed to be minimal yet extensible, Ansible offers the lowest learning curve for administrators, developers, and other IT specialists. And with its agentless design, Ansible lets us get up and running faster than any other configuration management tool.

With Ansible’s split into the core engine and other content, we can tightly control what modules are available to our Ansible playbooks. But with great power also comes great responsibility, and now we are solely responsible for the quality of Ansible content we use. Feel free to visit our post about [high-quality Ansible Collections] (https://steampunk.si/blog/attract-new-users-with-high-quality-ansible-collections/), where we give some hints on how to assess the quality of Ansible content.

Cheers!