Getting a new computer, whether physical or virtual, up and running for the first time or the 50th is time-consuming and requires a good deal of work. For many years I have used a series of scripts and RPMs that I created to install the packages I needed and to perform many bits of configuration for my favorite tools. This approach has worked well and simplified my work as well as reducing the amount of time I spend actually typing commands.
I am always looking for better ways of doing things and, for several years now, I have been hearing and reading about Ansible which is a powerful tool for automating system configuration and management. Ansible allows the Sysadmin to define a specific state for each host in one or more playbooks and then performs whatever tasks are necessary to bring the host to that state. This includes installing or deleting various resources such as RPM or APT packages, configuration files and other files, users, groups, and much more.
I have delayed learning how to use it for a long time because – stuff. So yesterday I ran into a problem that I thought Ansible could easily solve for me.
Note that this article is not intended to be a complete how-to of getting started with Ansible, it is intended to provide you with some insight into some of the issues that I encountered and to provide some information that I found only in some very obscure places. Much of the information I found on various on-line discussions and Q&A groups about Ansible was incorrect. Errors ranged from information that was really old with no indication of its date or provenance to information that was just wrong. The information in this article is known to work – although there might be other ways of accomplishing the same things – and it works with Ansible 2.9.13 and Python 3.8.5.
All of my best learning experiences start with a problem I need to solve and this was no exception.
I have been working on a little project to modify the configuration files for Midnight Commander (mc) and pushing them out to various systems on my network for testing. Although I have a script to automate that, it still requires a bit of fussing with a command line loop to provide the names of the systems to which I want to push the new code. The large number of changes as I was making to the configuration files made it necessary for me to push the new ones frequently and then, just when I thought I had my new configuration just right, I would find a problem and need to do another push after making the fix.
This environment made it difficult to keep track of which systems had the new files and which did not. I also have a couple hosts that need to be treated differently. And my little bit of understanding of Ansible suggested that it would probably be able to do all or most of what I need.
I had previously read a number of good articles and books about Ansible but not in the, “I have to get this working NOW!” kind of situation. And now was – well, NOW!
In rereading these documents I discovered that the books mostly talk about how to install Ansible from Github using – wait for it – Ansible. That is cool but I really just wanted to get started so I installed it on my Fedora workstation using DNF and the version in the Fedora repository. Easy. But then I started looking for the file locations, and trying to determine which configuration files I needed to modify, where to keep my playbooks, what a playbook even looks like and what it does. I had lots of so far unanswered questions running around in my head.
So without further descriptions of my tribulations, here are the things I discovered and which got me going.
Ansible’s configuration files are kept in /etc/ansible. Makes sense, right – since /etc is where system programs are supposed to keep their configuration files. The two files I needed to work with here are ansible.cfg and hosts.
After getting started with some of the exercises I found in the documents and on-line, I was receiving warning messages about deprecating certain older Python files. So I set the following to false in ansible.cfg and no longer received those angry red warning messages.
deprecation_warnings = False
Those warnings are important so I will revisit them later and figure out what I need to do, but for now they no longer clutter the screen and obfuscate the errors with which I actually need to be concerned.
The hosts file
Not the same as the /etc/hosts file, this file is also known as the inventory file and it lists the hosts on your network. This file allows grouping hosts together in related sets such as servers, workstations, and pretty much any designation you need. This file contains its own help and plenty of examples so I won’t go into boring detail here. However, there are some things to know.
Hosts can be listed outside of any groups but groups can be helpful in identifying hosts with one or more common characteristics. Groups use the INI format so a server group looks like this:
[servers] server1 server2 …etc.
A host name must be present in this file for Ansible to work on it. Even though some sub-commands allow you to specify a host name, the command will fail unless the host name is in the hosts file. A host can also be listed in multiple groups. So server1 might also be a member of the [webservers] group in addition to the [servers] group, and a member of the [ubuntu] group to differentiate it from Fedora servers.
Ansible is smart. If the all argument is used for hostname, Ansible scans the file and performs the defined tasks on all hosts listed in the file. Ansible will only attempt to work on each host once no matter how many groups it appears in. This also means that there does not need to be a defined “all” group because Ansible can determine all host names in the file and create its own list of unique host names.
Another little thing to look out for is multiple entries for a single host. I use CNAME records in my DNS zone file to create aliased names that point to the A records for some of my hosts. That way I can refer to a host as host1 or h1 or myhost. If you use multiple host names for the same host in the hosts file Ansible will try to perform its tasks on all of those host names; it has no way of knowing that they refer to the same host. The good news is that this does not affect the overall result, it just takes a bit more time as Ansible works on the secondary host names and determines that all of the operations have already been performed.
Most of the books I have read talk about Ansible facts. This information is available in other ways such as lshw, dmidecode, the /proc filesystem, and more, but Ansible generates a JSON file to contain this information. Each time Ansible it generates this facts data. There is an amazing amount of information in this data stream all of which is in <“variable-name”: “value”> pairs. All of these variables are available for use within an Ansible playbook. The best way to understand the huge amount of information available is to display it for yourself.
# ansible -m setup <hostname> | less
See what I mean? Everything you ever wanted to know about your host hardware and Linux distribution is there and usable in a playbook. I have not yet gotten to the point where I need those variables but I am certain I will in the next couple days.
The previous ansible command uses the -m option to specify the “setup” module. Ansible has many modules of its own already built in so the -m does not need to be used for those. There are also many downloadable modules that can be installed but for my current projects the built-ins do everything I have needed so far.
Playbooks can be located most anywhere. Since I need to run my playbooks as root I placed mine in /root/ansible. As long as this directory is the PWD when I run Ansible can find my playbook. Of course Ansible also has a runtime option to specify a different playbook and location.
Playbooks can contain comments although I have seen few articles or books that mention this. As a Sysadmin who believes in documenting everything, I find that comments can be very helpful. This is not so much about saying the same things in the comments as I do in the task name but rather it is about identifying the purpose of groups of tasks and ensuring that I record my reasons for doing certain things in a certain way or order. This can help with debugging problems at a later date when I might have forgotten my original thinking.
Playbooks are simply collections of tasks which define the desired state of a host. A host name or inventory group is specified at the beginning of the playbook and defines the hosts on which Ansible will run the playbook.
A sample of my playbook looks like this:
##################################################################################### # This Ansible playbook updates Midnight commander configuration files. # ##################################################################################### - name: Update midnight commander configuration files Hosts: all tasks: - name: ensure midnight commander is the latest version dnf: name: mc state: present - name: create ~/.config/mc directory for root file: path: /root/.config/mc state: directory mode: 0755 owner: root group: root - name: create ~/.config/mc directory for dboth file: path: /home/dboth/.config/mc state: directory mode: 0755 owner: dboth group: dboth - name: copy latest personal skin copy: src: /root/ansible/UpdateMC/files/MidnightCommander/DavidsGoTar.ini dest: /usr/share/mc/skins/DavidsGoTar.ini mode: 0644 owner: root group: root - name: copy latest mc ini file copy: src: /root/ansible/UpdateMC/files/MidnightCommander/ini dest: /root/.config/mc/ini mode: 0644 owner: root group: root - name: copy latest mc panels.ini file copy: src: /root/ansible/UpdateMC/files/MidnightCommander/panels.ini dest: /root/.config/mc/panels.ini mode: 0644 owner: root group: root <SNIP>
The playbook starts with its own name, and the hosts on which it will act, in this case all the hosts listed in my hosts file. The tasks section lists the specific tasks required to bring the host into compliance with the desired state. In this playbook I start with a task in which the Ansible dnf built-in updates Midnight Commander if it is not the most recent release. The next tasks ensure that the required directories are created if they do not already exist, and the remainder of the tasks copy the files to the proper locations. These file and copy tasks can also set the ownership and file modes for the directories and files.
The details of my playbook are beyond the scope of this article but I used a bit of a brute force attack on the problem. There are other methods for determining which users need to have the files updated rather than using a task for each file for each user. My next objective is to simplify this playbook to use some of the more advanced techniques.
Running a playbook is easy. The ansible-playbook command is used to run a playbook. The “yml” extension stands for YAML. I have seen a couple meanings for that but my bet is on “Yet Another Markup Language” despite the fact that I have seen some claims that YAML is not one.
# ansible-playbook -f 10 UpdateMC.yml
This command runs the playbook I created for updating my Midnight Commander files. The -f option specifies that Ansible should fork up to ten threads in order to perform operations in parallel. This can greatly speed overall task completion especially when working on multiple hosts.
The output from a running playbook lists each task and the results. An “ok” means that the machine state managed by the task is already is it is defined in the task stanza. Because the state defined in the task is already true Ansible did not need to perform the actions defined in the task stanza.
A response of “changed” indicates that Ansible performed the task specified in the stanza in order to bring it to the desired state. In this case the machine state defined in the stanza was not true so the actions defined were performed in order to make it true. On a color-capable terminal, the TASK lines are shown in color. On my host with my amber on black terminal color configuration, the TASK lines are shown in amber, changed lines are in brown, and ok lines are shown in green. Error lines are displayed in red.
The following output is from the playbook I will eventually use to perform post install configuration on new hosts.
PLAY [Post-installation updates, package installation, and configuration] TASK [Gathering Facts] ok: [testvm2] TASK [Ensure we have connectivity] ok: [testvm2] TASK [Install all current updates] changed: [testvm2] TASK [Install a few command line tools] changed: [testvm2] TASK [copy latest personal Midnight Commander skin to /usr/share] changed: [testvm2] TASK [create ~/.config/mc directory for root] changed: [testvm2] TASK [Copy the most current Midnight Commander configuration files to /root/.config/mc] changed: [testvm2] => (item=/root/ansible/PostInstallMain/files/MidnightCommander/DavidsGoTar.ini) changed: [testvm2] => (item=/root/ansible/PostInstallMain/files/MidnightCommander/ini) changed: [testvm2] => (item=/root/ansible/PostInstallMain/files/MidnightCommander/panels.ini) TASK [create ~/.config/mc directory in /etc/skel] changed: [testvm2] <SNIP>
If you have the cowsay program installed on your computer, you will notice that the TASK names appear in the speech bubble of the cow.
____________________________________ < TASK [Ensure we have connectivity] > ------------------------------------ \ ^__^ \ (oo)\_______ (__)\ )\/\ ||----w | || ||
If you do not have this fun feature and want it, install the cowsay package using the package manager of your distribution. If you do have this and don’t want it, disable it with by setting “nocows = 1” in the /etc/ansible/ansible.cfg file.
I do like the cow and think it is fun, but it reduces the amount of screen space that can be used to display actual messages. So I disabled it after it started getting in the way.
As with my Midnight Commander task it is frequently necessary to install and maintain files of various types. There are as many “best practices” defined for creating a directory tree for storing files used in playbooks as there are Sysadmins – or at least the number of authors writing books and articles about Ansible.
I chose a simple structure that makes sense to me.
/root/ansible └── UpdateMC ├── files │ └── MidnightCommander │ ├── DavidsGoTar.ini │ ├── ini │ └── panels.ini └── UpdateMC.yml
You should use whatever structure works for you. Just be aware that some other Sysadmin will likely need to work with whatever you set up so there should be some level of logic to it. When I was using an RPM and Bash scripts to perform my post-install tasks my file repository was a bit scattered and definitely not structured with any logic. As I work through creating playbooks for many of my administrative tasks I will be introducing a much more logical structure for managing my files.
Multiple playbook runs
It is safe to run a playbook as many times as needed or desired. Each task will only be executed if the state does not match that specified in the task stanza. This makes it easy to recover from errors encountered during previous playbook runs. The playbook stops running when an eror is encountered.
While testing my first playbook I made many mistakes and would then correct them. Each additional run of the playbook – assuming my fix was a good one – would then skip the tasks whose state already matched the specified one, and would execute those that were not. When my fix worked, that previously failed task would complete successfully and any tasks after that one in my playbook would also execute – until another error was encountered.
This also makes testing easy. I can add new tasks and when I run the playbook only those new tasks are performed because they are the only ones that do not match the desired state of the test host.
Some tasks are not appropriate for Ansible because there are better methods for achieving a specific machine state. The use case that comes to mind is that of returning a VM to an initial state so that it can be used as many times as necessary to perform testing beginning with that known state. It is much easier to get the VM into the desired state, in this case using Ansible is a good method, and then to take a snapshot of the then current machine state. Reverting to that snapshot is usually going to be easier and much faster than using Ansible to return the host to that desired state. This is something I do several times a day when researching articles or testing new code.
After completing the playbook I use for updating Midnight Commander I started a new playbook that I will use to perform post-installation tasks on newly installed Fedora hosts. I have already made good progress and the playbook is a bit more sophisticated and less brute force than my first one.
In my very first day of learning Ansible I have created a playbook that solves a problem. I have started a second playbook that will solve the very big problem of post-install configuration. And I have learned a lot.
Although I really liked using Bash scripts for many of my administrative tasks I am already finding that Ansible can do everything I want and in a way that can maintain the system in the state that I want. After only a single day I am an Ansible fan.
The most complete and useful document I have found is the Ansible User Guide on the Ansible web site. This document is intended as a reference and not as a how-to or getting-started document.
Opensource.com has published many articles about Ansible over the years and I have found most of them very helpful for my needs. The Red Hat Enable Sysadmin web site also has a lot of Ansible articles that I have found to be helpful.