IT Automation
Getting started with Ansible
October 7, 2022 - Words by Tadej Borovšak, Anže Luzar - 15 min read
This post was originally published on November 12, 2020, but the content has been updated according to the recent Ansible changes.
In our previous blog post , 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 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:
- Static inventory files that contain login information for our targets.
- 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 two Ansible packages that we can use for our day-to-day
automatization tasks: Ansible community package called
ansible
,
and
ansible-core
. Why? Because some time ago, the monolithic
ansible
package was split into a core engine that now resides in
ansible-core
package, and a few bundles of Ansible content called Ansible
Collections for the 2.10 release.
Now, ansible
package is using new versioning (e.g., 7.0.0
) and still keeps
the logic from 2.9 by including language, runtime, and selected Ansible
Collections. The ansible-core
package uses classic Ansible versioning
(e.g., 2.13
), contains only builtin plugins and is meant for users that want
to install only the collections they need. It is good to know that when
installing ansible
, this installs ansible-core
as well or in other words,
ansible-core
is a dependency of ansible
package.
A package called ansible-base
also exists, with releases in the 2.10
range.
This was the first package rename when Ansible split out its Ansible Collections.
It is an official release, but is not updated any more as it was subsequently
renamed to ansible-core
, the latest official Ansible package. If you use
ansible-core
, you can ignore the existence of ansible-base
altogether.
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:
- Are you using Ansible for the first time? Use ansible.
- Are you a Red Hat subscriber or want to purchase support? Use ansible.
- Do you intent to use modules from Ansible Collections that are maintained by Ansible community or Partner organizations? Use ansible.
- Use ansible-core.
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 # For ansible 2.10 and newer
$ pip install --user ansible-core # For ansible-core
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
Official installation instructions 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 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 [core 2.13.4]
config file = /etc/ansible/ansible.cfg
configured module search path = ['/home/user/.ansible/plugins/modules', '/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python3.10/site-packages/ansible
ansible collection location = /home/user/.ansible/collections:/usr/share/ansible/collections
executable location = /usr/bin/ansible
python version = 3.10.5 (main, Jun 11 2022, 16:53:24) [GCC 9.4.0]
jinja version = 3.1.2
libyaml = True
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
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
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 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 core 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-core
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
Welcome 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 , 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 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.
But good news! To take the pressure off, you can use Steampunk Spotter to check the quality of Ansible content and get help with playbook writing. Spotter helps you spot hard-to-catch and time-consuming errors, make sure you are using correct parameters and quickly identify invalid configurations, identify name changes and redirects, check for fully qualified names, and ensure you are using only certified and approved modules.
Cheers!