Ansible is a popular IT automation tool. In this article, I would like to share with you some of the useful practices we have discovered while using Ansible over the past four years.
Create a separate account for Ansible
In order to apply configuration changes, Ansible connects to the target machine via SSH. We prefer to create a dedicated
ansible account on each machine managed by Ansible. This account is set up with the required
sudo privileges. Password authentication to the
ansible account is disabled. Instead, access is managed by adding or removing person’s SSH public key to the
SSH daemon logs the SSH key fingerprint that was used for authentication. That allows us to keep track of who made use of the
ansible account. On Red Hat based distros, you can find the access logs in
/var/log/secure. Here is a sample log message after I triggered the execution of an Ansible playbook:
Furthermore, any time Ansible uses
sudo to execute a task, an additional log message is appended to the
/var/log/secure log file on the target machine. We found that for the auditing purposes the generated logs are sufficient.
In the cloud, we either use custom images that already contain the
ansible account or we create the
ansible account on the first boot using a cloud-init script. Using a dedicated
ansible account instead of the
centos and other image accounts streamlines our management efforts.
Report changes made on the target machine accurately
When Ansible executes a playbook, it highlights all configuration changes made to the target machine while bringing it to the desired state described in the playbook. Administrator can review the reported changes to verify that they meet his expectations. If no changes had to be made to the target machine, Ansible should report
changed=0 after the playbook execution completes.
When executing Ansible
command module or
shell module, there’s no good way for Ansible to know whether the executed command or shell script made any changes to the target machine. Here we can help ourselves with a neat trick that I will explain on the following example. Imagine that we want to create a new InfluxDB database:
1 2 3 4 5 6
The above task creates a new InfluxDB database in the case that it doesn’t already exist. The problem with the above code is that Ansible will always mark this task as changed. In order to report the change status accurately, let’s leverage Ansible’s
1 2 3 4 5 6 7 8 9
We modified the shell script to print
CHANGED to the standard output if and only if a new InfluxDB database has been created. Next, we captured the entire standard output of the shell script in the
create_database variable using the Ansible’s
register directive. Lastly, we search the content of the
create_database variable for the
CHANGED keyword. If the
CHANGED keyword is found, Ansible will mark the task as changed.
It would be great if Ansible would come up with a mechanism to determine whether a shell script made changes to the target machine or not. In the meanwhile, the above pattern works very well for us.
Skip completed tasks
Ansible playbook may be executed multiple times against the same target machine. During each run Ansible must be able to determine which tasks should be executed and which tasks should be skipped because they were already completed in the previous run. Think about the scenario where we are configuring a Docker storage. In the following code, we are invoking the
docker-storage-setup command on the target machine:
1 2 3
If you would run the above command the second time, it would fail because the Docker storage has already been configured. How to ensure that the above task will run only once? Ansible’s
command module provides
removes parameters that instruct Ansible to only run the
command task if a specific file does or doesn’t exist on the file system. The idea is that the executed command creates or deletes these files. Could these parameters help us here? While these parameters can save you in many cases, we would like to avoid using them in this situation. Firstly, we would have to go and figure out whether the
docker-storage-setup script creates or removes any file. Secondly, the location of the created or removed file may depend on the chosen storage driver which would demand extending our script with additional logic. And lastly, there is no guarantee that the future versions of the
docker-storage-setup script would manipulate the same file which could cause our Ansible script to break. As an alternative, here is a pattern that a software practitioner can use in such a situation:
1 2 3 4 5 6 7 8 9 10 11 12 13
The gist of the above code is simple. After we have successfully configured the Docker storage we create a stamp file. Because the stamp file exists, Ansible will skip the Docker storage configuration in all the following playbook runs. The stamp file is created in a well defined location. Should we want to re-run the Docker storage configuration in the future, we can just remove the stamp file before executing the playbook.
Backporting Ansible modules
For the task at hand, it is always preferable to leverage a dedicated Ansible module before falling back on generic
shell modules. In our Ansible practice, we came into situations where the Ansible module we would like to use was available only in the newer versions of Ansible or that the module in our version of Ansible was buggy. As the interface between Ansible core and the Ansible modules is pretty stable, here is my advice: just copy the desired module from the newer version of Ansible and drop it into the
library directory next to your playbook. This
library directory is a location where you can put your custom modules. Moreover, any custom module having the same name will override a module distributed with Ansible. This is how our
library directory currently looks like:
1 2 3 4 5 6
We are using Ansible version 18.104.22.168 which doesn’t contain
win_reg_stat modules. Also, we found the
win_get_url module in version 22.214.171.124 to be buggy. Adding a handful of modules to the
library directory avoids the need to upgrade to the next version of Ansible. From past experience we know that porting our Ansible code base to the next version of Ansible requires a considerable effort.
In this article, we shared some of the Ansible practices we found useful. We recommended to create a dedicated
ansible account on each machine that is managed by Ansible. We described how to inform Ansible whether a script did or did not make any changes to the target machine. We showed you how to prevent a repeated execution of Ansible tasks by creating a custom stamp file. Lastly, we demonstrated that backporting Ansible modules is not that complicated as it may sound.
Hope you enjoyed this article. Let me know if you have any comments or suggestions to the presented practices. Feel free to add your comments in the comment section below.