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 group Team
- Create a user Bob and add him to the group Team
- 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 user Alice fails, which means that Alice is never given escalated privileges. However, the group Team is added and the user called Bob 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 user Alice is now created and she is also given escalated privileges. As the group Team and user Bob 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.