How to do one time setup with Chef
Some applications require running special commands when you’re first configuring
them. For example, PostgreSQL requires you run initdb
to create the
initial database. When you wrap things up in configuration management software
like Chef, it can be tricky to get one time setup stuff correct. You want it
to run the first time the application’s installed, but never again after that.
Before you start writing recipe code, it’s helpful to step back and ask yourself “How would I solve this without Chef?” There’s a whole lot of accumulated sysadmin lore built out of bash scripts and best practices. Solve your problem without Chef first, and you’ll end up with a better solution when you translate it into a Chef recipe.
The canonical bash solution for “Run this command once and only once” is to test for the existence of a lock file, and if it doesn’t exist create it and run the command. That code usually looks something like this:
#!/bin/bash
if [ ! -f lockfile ];
then
touch lockfile
initdb -D data
fi;
Looking at that code, it’s obvious the end goal is to run the initdb
command.
Chef provides an execute resource that’s a nice abstraction on top of the
idea of running a command. The usual way to guard against an execute resource
running more than once is with the not_if
and only_if
attributes.
Translating that bash script into a Chef recipe makes it look like this:
execute 'initdb' do
command 'touch lockfile && initdb -D data'
not_if 'test -f lockfile'
end
Unfortunately, there’s one potential flaw with that Chef recipe. What if the
touch
and test
executables don’t exist? “But this is Linux, of course they
exist!” you cry. Well, it’s Linux right now. What if it’s Windows later? Chef
works best when you do as much as possible in Chef. Don’t worry about OS
specific details. Let Chef handle those for you.
The touch
and test
commands are really just a way to create a file if it
doesn’t already exist. Chef has a file resource that can do that. You also
want to trigger the execution of the initdb
command if the file gets created.
Chef has notification events that can handle that. Incorporating those ideas
into the recipe makes it look like this:
file 'lockfile' do
action :create_if_missing
notifies :run, 'execute[initdb]', :immediately
end
execute 'initdb' do
command 'initdb -D data'
action :nothing
end
Setting the action attribute to :nothing
on the execute resource ensures it
only runs when the notification triggers it. Setting the action attribute to
:create_if_missing
on the file resource ensures it only runs if the file
doesn’t exist. The end result is a one time setup command in Chef.
If you find yourself using this pattern a lot, especially if you’re triggering multiple things off the same lock file, you may find that switching away from notification events makes your code easier to read and reason about.