Ansible or Puppet? A Great Debate?
Should I install Ansible or Puppet? In short, I feel both have their place.
Anyone who has asked me about work in the last few years knows that I have a passion for automation tools. My favorite for configuration automation has always been Puppet. Puppet is a mature infrastructure-as-code tool that describes a desired state and enforces it. Having used it for years, I can say that Puppet handles most of my management needs. However, there are some tasks that Puppet just doesn’t handle as well. After playing a bit with Ansible, I believe it can be the tool to fill many of those gaps.
What Puppet Gets Right
Puppet’s domain-specific language is powerful while being descriptive. Its agents are portable and cross platform. Its server is mature and stable. It handles building a catalog of configuration quite well and provides a lot of descriptive power. In short, Puppet is adept and defining and enforcing a configuration baseline. Your puppet code describes infrastructure configurations and puppet makes sure that they exist and stay consistent.
Beyond that, puppet features many other benefits:
- the Puppet Forge, a community of module developers with a strong following
- a robust ancillary toolset, including r10k configuration manager for advanced deployment of your code
- hiera, a tool used to separate code from configuration (keeping your code clean and reusable)
- a slick enterprise edition, if you need supported deployment in a larger environment
Where Puppet Falls Short
While I’ve derived great benefit from puppet and can sing its praises longer than most are comfortable with, there are a few gaps in puppet’s capabilities. Here are a few of the gaps and drawbacks I most often find when using puppet.
Reliance on an Agent
Puppet’s agent is an asset, enabling many of the benefits I’ve listed above. However, this also adds a slight burden to the configuration. The first puppet code I write is usually a profile class to manage puppet. If something happens and the agent breaks, getting back on track is a manual task.
This isn’t strictly a drawback. As I mentioned above, the agent enables many of puppet’s most powerful features. It is worth noting that the agent isn’t all sunshine and puppies, though. That little bit of pain is part of the fee we pay for well managed infrastructure.
Bootstrapping and Orchestration
Related to the agent is the problem of boostrapping. While puppet is great at taking a server that’s got a base OS installed and configuring it, it’s not as great at kicking off a task to install the OS or create a VM in the first place. Additionally, configuring a new puppet master server for your environment is probably a manual process. You may be required to install an agent, pull down some code, and get things configured properly before you can do your first puppet run.
Lack of Procedural Tools or Tasks
Puppet is designed with desired state in mind- that is, you should build your puppet code to describe your desired outcome and let the tool decide how to do the work. This is awesome. However, there are times when you want to do tasks or time-based procedures.
Perhaps you want to write a task to bootstrap a new puppet server in your environment. Maybe you want to kick off a job that will update and reboot all of your nodes in a particular order. Possibly you want to tell VMWare to build you a new cluster of servers with a given set of IPs.
Puppet by itself cannot solve these problems. I determined Ansible to be a good fill-in for these gaps.
(As an aside, Puppetlabs, the company that develops puppet, provides a tool to solve this problem called mcollective. I won’t go into mcollective vs Ansible here; but, for my own uses, I’ve found Ansible to be a better fit.)
How Ansible Helps
Ansible, while also an infrastructure-as-code tool, doesn’t specifically describe desired state. Instead, it enables the building of playbooks– blocks of code that describe tasks and inter-dependencies to operate on a server and achieve a verified result. Ansible resolves a number of problems left to us by Puppet.
Ansible is Agentless
There are no agents when working with Ansible. Instead, Ansible relies on SSH (PowerShell remote for Windows servers). Since SSH is common to most servers, there isn’t anything to install.
Because there are no agents and the underlying communication is a common component in servers, Ansible is a little less brittle than puppet. A server with an OS installed is ready for Ansible out of the box.
Ansible has Modules for Orchestration
Ansible can build virtual machines. Tasks can be combined to create a cluster. Ansible can configure networking relatively easily. While it cannot provide bare-metal bootstrapping (you still need PXE or some other installer to accomplish that), it can build an environment in the cloud from the ground up.
Ansible Runs Tasks
I’m not going to lie to you- at the heart of it, Ansible is a scripting engine. It uses Python to write code, ships it to your server, and runs it. That’s not a bad thing- Ansible executes powerful tasks based on its language. Because of this and the nature of playbooks, we can write timed tasks in Ansible that couldn’t be written in Puppet alone. I can write a playbook to upgrade my environment. Ansible can reload my webserver process on a set of machines. I can execute a source control pull on all of my nodes at once. I don’t want to enforce this type of action every minute. I want to achieve these goals at times of my choosing.
So, Which Tool is Better? Ansible or Puppet? Both.
Puppet and Ansible compete for market share. They build similar tools and attempt to differentiate themselves. That said, you can use them together easily. In the environments I’ve managed, I chose to employ both of these tools. They complement each other well and can be used in concert without issue. For example, if you have an existing puppet environment, it can create an Ansible configuration for you.
In the rest of this entry, I’ll cover how to start using Ansible by configuring it from Puppet.
Creating an Ansible Configuration In Puppet
To start using Ansible, I leveraged my existing puppet configuration. Notably, the rest of this blog will make heavy use of the roles and profiles pattern. A role is a puppet class describing a type of machine, such as a webserver or database server. A profile, on the other hand, describes a configuration for a specific technology, such as Apache or MySQL. For more information on using roles and profiles, read Gary Larizza’s blog post on the subject. He describes it better than I could.
Creating a Profile to Install Ansible
First of all, start by making a very basic profile that installs Ansible. Below is a good example.
# profile class to install and configure ansible class profiles::ansible { ensure_packages(['ansible']) }
Creating a Role for Your Ansible Control Machine
Next we want to define the role which employs this profile. Where possible, roles should be named in a way that’s technology agnostic.
# role for an orchestration server class roles::orchestrator inherits ::roles::base { include ::profiles::ansible }
Note that we have a “base” role upon which all other roles are built. This provides all of the configuration that every node in our environment should enforce. For now, let’s pretend it’s empty.
# role applied to all nodes class roles::base { }
Applying Your Role to a Server
Finally, we apply the role to a node in our environment. Our role employs the profile that installs Ansible. Therefore, puppet will enforce that the package is installed on the target.
node 'rodrigo.example.com' { include ::roles::orchestrator }
Deploy this code to your puppet server. Now, we’ll run the puppet agent on the orchestrator machine to see it install the Ansible package.
[root@rodrigo~]# puppet agent -t Info: Using configured environment 'production' Info: Retrieving pluginfacts Info: Retrieving plugin Info: Loading facts Info: Caching catalog for rodrigo.example.com Info: Applying configuration version '1508220476' Notice: /Stage[main]/Profiles::Ansible/Package[ansible]/ensure: created Notice: Applied catalog in 32.28 seconds
Using Puppet to Inform Ansible About our Environment
I’ve written 3 code files to do what one command on each server could do. So, why use puppet to do this? Because puppet can also be used to provide context about our environment to Ansible.
Ansible uses a hosts file in /etc/ansible/hosts
to determine what servers are available and how to group them. Follow the below steps to create a puppet configuration to auto-generate this file. As a result, we want to produce a file that looks like this:
[all_servers] server1.example.com server2.example.com server3.example.com rodrigo.example.com
Creating a Defined Type for a Host Entry
Since we’re interested in creating multiple hosts in the host configuration file, we’ll create a defined type in puppet to describe a single entry in that file. Defined types are reusable descriptions of resources that we expect to duplicate in puppet classes. (For the below type, I’m using the concat module available on Puppet Forge, which assembles single files from multiple fragments. See their puppet forge page for more information.)
# defined type to define ansible host entry define profiles::ansible::conf::host_definition ( $host = $title, #the name of the host $group = 'all_hosts', #the group under which the host will fall in the file ) { $hostsfile = '/etc/ansible/hosts' ensure_resource('concat', $hostsfile, { 'owner' => 'root', 'group' => 'root', 'mode' => '0644', }) ensure_resource('concat::fragment', "${hostsfile}_${group}", { 'target' => $hostsfile, 'content' => "\n[${group}]\n", 'order' => "${group}", }) ::concat::fragment { "${hostsfile}_${group}_${host}" : target => $hostsfile, content => "${host}\n", order => "${group}_${host}", } }
First, we’re creating a single hosts file. This is a defined type for every entry in that file. Therefore, we’ll be calling it many times. Hence, we can’t just declare the concat resource for the file or we’ll get duplicate resource entries. The ensure_resource
function allows us to ensure that the resource is in the catalog without erroring if it already exists. Second, we’re going to only want one line that contains the group name for the whole file. This could also create duplicate errors, so we use ensure_resource
again. Finally, we put the hostname under the group for which we’ve defined it.
To employ our defined type to create an entry, we can instantiate it in this way:
::profiles::ansible::conf::host_definition{ 'server1.example.com' : host => 'server1.example.com', group => 'all_servers', }
(Note that the host
and group
params are unnecessary because they default to title and ‘all_groups,’ respectively.)
Exporting Our Resources
While this achieves our goal, it isn’t useful on its own. We’d rather puppet describe our servers than define them each ourselves within puppet using a resource. Therefore, we’ll export these resources from our base role, which is applied by every node.
# role applied to all nodes class roles::base { @@::profiles::ansible::conf::host_definition{ 'server1.example.com' : } }
(Because we don’t need to specify the host or group, I’ve removed them from the example here.) Note the two at signs at the beginning of the declaration. This is an exported resource. Rather than defining hosts in one place, each host can export its own definition to collect later.
Collecting the Resources to Create a Hosts File
Finally, we’ll modify the profile and have it collect the resources from other servers.
# role for an orchestration server class roles::orchestrator inherits ::roles::base { include ::profiles::ansible Profiles::Ansible::Conf::Host_definition <<| |>> }
Since we’ve used the spaceship operator (<<| |>>
), our profile collects the hosts resources. As a result, puppet will create a file with every host in our environment:
/etc/ansible/hosts
. This file is created on our Ansible server.
Puppet has now informed its good friend Ansible of the lay of the land.
More to Come
In my next blog post, I’ll cover more Ansible usage and how Ansible can be used to run and deploy puppet.