Ansible versus other tools
If you look at the design principles in the first commit compared to the current version, you will notice that while there have been some additions and tweaks, the core principles remain pretty much intact:
- Agentless: Everything should be managed by the SSH daemon, the WinRM protocol in the case of Windows machines, or API calls—there should be no reliance on either custom agents or additional ports that need to be opened or interacted with on the target host
- Minimal: You should be able to manage new remote machines without having to install any new software as each Linux host will typically have at least SSH and Python installed as part of a minimal installation
- Descriptive: You should be able to describe your infrastructure, stack, or task in a language that is readable by both machines and humans
- Simple: The setup processes and learning curve should be simple and feel intuitive
- Easy to use: It should be the easiest IT automation system to use, ever
A few of these principles make Ansible quite different to other tools. Let's take a look at the most basic difference between Ansible and other tools, such as Puppet and Chef.
Declarative versus imperative
When I first started using Ansible, I had already implemented Puppet to help manage the stacks on the machines that I was managing. As the configuration became more and more complex, the Puppet code became extremely complicated. This is when I started looking at alternatives, and ones that fixed some of the issues I was facing.
Puppet uses a custom declarative language to describe the configuration. Puppet then packages this configuration as a manifest that the agent running on each server then applies.
The use of declarative language means that Puppet, Chef, and other configuration tools such as CFEngine all operate using the principle of eventual consistency, meaning that eventually, after a few runs of the agent, your desired configuration would be in place.
Ansible, on the other hand, is an imperative language meaning that, rather than just defining the end state of your desired outcome and letting the tool decide how it should get there, you also define the order in which tasks are executed in order to reach the state you have defined.
The example I tend to use is as follows. We have a configuration where the following states need to be applied to a server:
- Create a group called
Team
- Create a user
Alice
and add her to the groupTeam
- Create a user
Bob
and add him to the groupTeam
- Give the user
Alice
escalated privileges
This may seem simple; however, when you execute these tasks using a declarative language, you may, for example, find that the following happens:
- Run 1: The tasks are executed in the following order: 2, 1, 3, and 4. This means that on the first run, as the group called
Team
does not exist, adding the userAlice
fails, which means thatAlice
is never given escalated privileges. However, the groupTeam
is added and the user calledBob
is added. - Run 2: Again, the tasks are executed in the following order: 2, 1, 3, and 4. Because the group
Team
was created during run 1, the userAlice
is now created and she is also given escalated privileges. As the groupTeam
and userBob
already exist, they are left as is. - Run 3: The tasks are executed in the same order as runs 1 and 2; however, as the desired configuration had been reached, no changes were made.
Each subsequent run would continue until there was either a change to the configuration or on the host itself, for example, if Bob
had really annoyed Alice
and she used her escalated privileges to remove the user Bob
from the host. When the agent next runs, Bob
will be recreated as that is still our desired configuration, no matter what access Alice
thinks Bob
should have.
If we were to run the same tasks using an imperative language, then the following should happen:
- Run 1: The tasks are executed in the order we defined them, meaning that the group is created, then the two users, and finally the escalated privileges of
Alice
are applied - Run 2: Again, the tasks are executed in the order and checks are made to ensure that our desired configuration is in place
As you can see, both ways get to our final configuration and they also enforce our desired state. With the tools that use declarative language, it is possible to declare dependencies, meaning that we can simply engineer out the issue we came across when running the tasks.
However, this example only has four steps; what happens when you have a few hundred steps that are launching servers in public cloud platforms and then installing software that needs several prerequisites?
This is the position I found myself in before I started to use Ansible. Puppet was great at enforcing my desired end configuration; however, when it came to getting there, I found myself having to worry about building a lot of logic into my manifests to arrive at my desired state.
What was also annoying is that each successful run would take about 40 minutes to complete. But as I was having dependency issues, I had to start from scratch with each failure and change to ensure that I was actually fixing the problem and not because things were starting to become consistent—not what you want when you are on a deadline.
Configuration versus orchestration
Another key difference between Ansible and the other tools that it is commonly compared to is that the majority of these tools have their origins as systems that are designed to deploy and police a configuration state.
They typically require an agent to be installed on each host, that agent discovers some information about the host it is installed on, and then calls back to a central server basically saying Hi, I am server XYZ, could I please have my configuration? The server then decides what the configuration for the server looks like and sends it across to the agent, which then applies it. Typically, this exchange takes place every 15 to 30 minutes—this is great if you need to enforce a configuration on a server.
However, the way that Ansible has been designed to run allows it to act as an orchestration tool; for example, you can run it to launch a server in your VMware environment, and once the server has been launched, it can then connect to your newly launched machine and install a LAMP stack. Then, it never has to connect to that host again, meaning that all we are left with is the server, the LAMP stack, and nothing else, other than maybe a few comments in files to say that Ansible added some lines of configuration—but that should be the only sign that Ansible was used to configure the host.
Infrastructure as code
Before we finish this chapter and move on to installing Ansible, let's quickly discuss infrastructure as code, first of all by looking at some actual code. The following bash script installs several RPMs using the yum
package manager:
#!/bin/sh LIST_OF_APPS="dstat lsof mailx rsync tree vim-enhanced git whois iptables-services" yum install -y $LIST_OF_APPS
The following is a Puppet class that does the same task as the previous bash script:
class common::apps { package{ [ 'dstat', 'lsof', 'mailx', 'rsync', 'tree', 'vim-enhanced', 'git', 'whois', 'iptables-services', ]: ensure => installed, } }
Next up, we have the same task using SaltStack:
common.packages: pkg.installed: - pkgs: - dstat - lsof - mailx - rsync - tree - vim-enhanced - git - whois - iptables-services
Finally, we have the same task again, this time using Ansible:
- name: install packages we need yum: name: "{{ item }}" state: "latest" with_items: - dstat - lsof - mailx - rsync - tree - vim-enhanced - git - whois - iptables-services
Even without going into any detail, you should be able to get the general gist of what each of the three examples is doing. All three, while not strictly infrastructure, are valid examples of infrastructure as code.
This is where you manage the code that manages your infrastructure in exactly the same way as a developer would manage the source code for their application. You use source control, store it in a centrally available repository where you can collaborate with your peers, you branch and use pull requests to check in your changes, and, where possible, you write and execute unit tests to ensure that changes to your infrastructure are successful and error-free before deploying to production. This should be as automated as possible. Any manual intervention in the tasks mentioned should be seen as potentially a point of failure and you should work to automate the task.
This approach to infrastructure management has a few advantages, one being that you, as system administrators, are using the same processes and tooling as your developer colleagues, meaning that any procedures that apply to them also apply to you. This makes for a more consistent working experience, as well as exposing you to tools that you may not have been exposed to or used before.
Secondly, and more importantly, it allows you to share your work. Before this approach, this type of work seemed to others a dark art performed only by system administrators. Doing this work in the open allows you to have your peers review and comment on your configuration, as well as being able to do the same yourself to theirs. Also, you can share your work so that others can incorporate elements into their own projects.