Modular Configuration with Chef

As a developer doing systems administration work, you're probably going to be reading a lot of online tutorials to figure out how to setup something for the first time. Once you've got that down, you just automate the process, right?

Not so fast!

Tutorials are great resources, and in my opinion, they're the best first stop on the road to learning any new technology. However, when you start building a recipe around the setup of a service, you need to start thinking about how that's going to interact with other recipes. Better yet, because you can't possibly predict all the future uses you or someone else may have for your recipe, you should focus on how it will not interact with other recipes. In other words, try really hard to keep it from stomping on toes!

logo chefA common problem we've run into is when two different recipes, with very different goals and concerns, need to edit the same configuration file. You can solve this, of course, by merging the two recipes, but do this too often and you end up with monolithic do-everything recipes that have a zillion knobs and buttons. People rightfully become fearful of recipes like this.

It turns out that a lot of 'nix systems have a great way of dealing with this, but you probably won't see it in the tutorial, unless the tutorial's purpose is to show you how to setup the service in a modular fashion. I'm talking about the mysterious ".d" directories you'll see in various places on a system (at least, I found them mysterious..). But they actually represent a pretty straightforward convention. You'll often see them sitting beside a similarly named config file (only without the ".d" at the end). And either that config file directly includes any files found in the ".d" directory, or the service that uses it does this.

Example: modular configuration with /etc/profile

As one of the simpler examples, take /etc/profile. Say you want to set up an environment variable that will be available to all users upon logging in. You might do this in /etc/profile, as it is usually the first configuration file loaded when initializing everyone's shell.

However, if your recipe just templates /etc/profile and turns it into a generated file, it will wipe out any changes any other recipe might make to this file. After all, your recipe probably isn't the only one that will ever want to add an environment variable. You could, of course, solve this by using a clever ruby_block code that actually reads the file on the system and inserts your new lines. You could get even fancier and have it tag those with some indicator that says it's a generated entry, allowing Chef to remove it or change the value on a later run.

Or, you could simply create a brand new file in the /etc/profile.d/ directory.

It may only contain a line or two, and that would seem silly if you were setting things up by hand. But it starts to make a lot of sense when you're looking at many recipes setting up different services/tools that every so often have to add something to a common configuration, without overwriting what previously run recipes have added.

One small warning: make sure to test that the configuration files in these ".d" directories are actually being read and doing what you expected, especially when the inclusion is not being done with some sort of "include" directive in the main configuration. We have run into at least one instance where a ".d" directory does nothing when you throw files inside of it. In that case, it was a planned feature that had not yet been implemented.

What do you do if the common configuration you want to add doesn't have a ".d" directory?

Workaround 1: leverage existing 'include' functionality

If the main configuration file has any sort of "include" functionality, you can simply create your own directory and add the include statement to the common file (since you're doing this from multiple recipes, you'll want to check that it doesn't already exist, of course). For example, if you wanted to make a site-level apache2 conf more modular, you could include the following line in your main config:

Include /etc/apache2/yoursite-custom/*.conf

Then, any .conf file you put in /etc/apache2/yoursite-custom/ will get included within the main configuration. So if you have a recipe to setup an authentication module, it doesn't have to be tied directly into the recipe you used to setup your main apache server.

But what if there is not such 'include' functionality in the service you're setting up?

Workaround 2: simulate non-existent 'include' functionality with Chef

You can still mimic the basic idea with Chef. We've created a LWRP that will do this for you. You use it in pretty much the same way you'd use a regular template resource, except that the path does not default to the resource name variable (this is the string you find immediately following the resource declaration and before the block which defines the resource). Resource names are supposed to be unique in Chef. The regular template resource can specify the path using the resource name because the file referred to by the path is only supposed to be written once.

Here's an example (in this example the resource name is 'gateway_config'):

    cybera_append_template "gateway_config" do
        path "/etc/ssh/ssh_config"
        source "gateway_config.erb"
        owner "root"
        group "root"
        mode 00644

The difference between this and the regular template resource is that we expect that /etc/ssh/ssh_config probably exists already and do not want to simply overwrite it. Here's what the above LWRP instance does at the system level:

  1. Create a directory called: /etc/ssh/ssh_config.d.chef
  2. Create the file /etc/ssh/ssh_config.d.chef/000_ssh_config if it doesn't already exist with the original content of /etc/ssh/ssh_config
  3. Using a regular template resource, create a file at /etc/ssh/ssh_config.d.chef/001_gateway_config (using the basename of the source file above, though you can override that default by specifying an append_name)
  4. Replace /etc/ssh/ssh_config with a sorted concatenation of all files in /etc/ssh/ssh_config.d.chef

The first recipe using chef_append_template that runs will end up setting up the initial directory structure. Other recipes will simply add their configurations to the /etc/ssh/ssh_config.d.chef directory, re-concatenating /etc/ssh/ssh_config every time.

And voila! You can now do modular configuration across recipes even when that sort of mechanism isn't natively supported.

Note that appending ".d.chef" serves two purposes:

  1. It indicates the similar intent of the directory to the classic ".d" directory; and
  2. It makes it clear that Chef is doing something to make this work. So when someone stumbles across the directory, they aren't surprised when a file they throw in it doesn't get automatically picked up.

To go modular or not to go modular…

Of course, applying these modular configuration techniques is not the best idea for every case. The more specialized the service you're setting up is, the more sense it makes to use standard templating functionality to reduce the complexity of your recipes. But if you're starting to consider doing line-level edits of existing configuration files, that's a good sign that one of these techniques might make life easier in the long term.