If you’ve read my post on framework scalability you’ll know that whenever I start a new project, I like to try and do it the right way to ensure the project will scale up. This post is intended as an introductory resource on how to start out a Puppet codebase in the right way.
If you’re writing Puppet code to manage your infrastructure, you’re a software developer – even if you don’t have “developer” in your job title – which means that in order to ensure the reusability and maintainability of your Puppet code, you should look at employing some software development best practices.
Here’s what we’ll cover:
Note that while this post is deliberately opinionated, it is mostly based on the tools and best practices coming out of the Puppet community.
Style guide
If there’s a few of you hacking on your Puppet codebase, or you’re looking at releasing a Puppet module to the community, it’s worth following the Puppet style guide as much as possible. Even if it’s just you and you’ll never open source your modules, there are benefits to maintaining a consistent style at least.
To help you, there’s a tool called puppet-lint which will check your code for violations of the style guide. Not only that, but you can tell it to try and fix them too!
{% highlight bash %} $ puppet-lint –fix puppet/ {% endhighlight %}
Dependency management
As the saying goes, a good programmer is a lazy programmer, so you’ll likely be using third-party modules from the Puppet Forge. If you’ve used puppet module install
on your puppetmaster (or masterless nodes), how do you ensure that you’re developing against the same version of the module you have installed on your nodes, or that you install the correct dependencies for a new node type when you come to deploy them?
If you commit entire third-party modules into your modules/ directory, then how do you update them when the maintainers add that feature you need? If you modify them this becomes even more painful.
Maybe you use git submodules to solve this, but its problems are well documented.
That’s where librarian-puppet comes in. You specify your dependencies in a Puppetfile, similar to Ruby’s Gemfile or Composer’s composer.json and use the librarian-puppet tool to fetch your dependencies. You can get them from the Puppet Forge, a custom Forge, a source control repository or any mixture of the above.
Because of this you can split your own modules out into separate repositories, even if you don’t open source them, and in fact you should as librarian-puppet likes to manage your whole modules/ directory.
Automated testing
Automated testing should be part of any software developer’s arsenal – if used correctly it helps you catch bugs early and prevent regressions.
There are two approaches to testing your Puppet code, and both should be employed. The first uses rspec-puppet to ‘unit test’ your code. It essentially ensures that the graph produced by your code matches the graph you expect.
However, this does not ensure it applies correctly to the systems it targets so to test that (‘integration testing’) you should use beaker. Beaker sets up a test environment in a virtual machine or container and applies your manifests to it. You can then assert that the system is in the desired state.
Gareth Rushgrove has created a module skeleton that includes the tools and files necessary to run both tools as well as running puppet-syntax and puppet-lint. The setup would apply equally well to a main puppet codebase (though as you’ll see below in design patterns, you should try and house most of your manifests in modules.)
Separation of concerns
One of the tenets of Object Orientation is the principle of separation of concerns, where the program is modular and addresses a separate concern.
From my perspective, this has two potential effects on a Puppet codebase. The first is on the modules you create: they should be self-contained and reusable, with a well-defined interface.
Design patterns
Design patterns are important in any piece of software; their use encourages best practices and provides a standard approach to solving common problems.
A few idiomatic patterns have emerged from the Puppet community in response to different problems:
- The simple Puppet module structure proposed by R.I.Pienaar attempts to address common use-cases when creating modules (including providing default values for class parameters) and ensuring their ability to couple safely with other modules.
- The roles/profiles pattern was introduced as a way to specify node functionality in a modular, abstract way that would be robust enough to withstand configuration changes to the way services work without changing the definition for each node which is responsible for that service.