In part 1, we learned about Chef Solo and used it to create a standard Chef directory structure, along with our own cookbook. Now it’s time to start writing the recipes we will run to provision our Rails server and install Node.js, PostgreSQL, rbenv, Ruby, Redis, and Nginx.
Defining Default Values
The first thing we’ll do is define some default values for our recipes. Go ahead and create a new file called
default.rb inside the
/attributes directory of the cookbook you created in part 1, and add the following code into it:
1 2 3 4 5 6 7
These are called attributes, and they’re just variables that are later used in recipes. We’re defining simple things here like the app name, the directory where Node.js will be installed, and the versions of the software we’ll be installing. Storing such things in a single file makes it easy to modify them later on.
This file is great for storing attributes that will be shared with more than one server, but attributes that are server-specific, like ports, usernames, and passwords, will be stored in another JSON file outside our cookbook. If you followed part 1, your cookbook is stored in your app’s
/config/chef/site-cookbooks directory, but this JSON file will reside inside the
/config/chef/nodes directory, and it’ll be named after the IP address of the server you’ll be provisioning (for example,
Go ahead and create the file now, and add the following code into it (be sure to replace the attributes with your own):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
Due to the sensitive nature of this file, it’s best to add it to your
.gitignore file so it doesn’t get uploaded to GitHub. Here’s the line you’ll need to add:
One thing I’d like to point out is Chef doesn’t use plain text passwords when creating new users. Instead, it uses a shadow hash of the plain text password, so the
user.password attribute must be a shadow hash. If you have the
openssl command installed on your local computer, you can create a password shadow hash by running the following:
This just uses the
passwd command provided by
openssl to create an MD5-based hash of the password (specified by the
-1 flag). You can then copy and paste the string that the command returns into the JSON file above. Once the user is set up on our node, you’ll still use the plain text password to log in, just like always. I wasn’t able to get shadow hash passwords working with PostgreSQL though, which is why I’m just using the plain text password there, but feel free to leave a comment if you know how to make it work.
One last thing I want to mention is that if you have the same attribute defined in both
default.rb and the JSON file, the latter will always override the former.
Now that the attributes are defined, we’re ready to start writing the recipes themselves. These recipes do the exact same server setup as the one covered in the “Deploying Rails Apps” series, so I won’t be explaining the whys behind the things I do here since that’s already covered in the series itself. If you’ve never provisioned a server from scratch before, it’s best to read that series first before continuing.
The First One
Our first recipe will install various packages and set the correct time zone. Create a new file called
default.rb inside the
/recipes directory of your cookbook, and add the following code into it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
Chef uses a domain-specific language (DSL) for writing recipes, and the code above is what it looks like. A recipe is made up of resources, and a resource is simply an abstraction of some shell code you would run to provision your server that Chef already implemented for you and wrapped it in a resource. Chef provides most of the resources you will need, but you can also write your own.
In the file above, we’re using three resources:
execute resource executes a command, the
package resource manages packages, and the
bash resource executes scripts using the Bash interpreter. So in the code above, we’re first using
execute to run
apt-get update to fetch the latest updates for the packages on our node. (A server is known as a node in Chef terminology.) Next, we use
package to install the various packages our node will need, and finally, we use
bash to execute two lines of code that will set the correct time zone (be sure to modify the
echo command so it sets your own time zone).
Each resource has various attributes that you can optionally specify inside a
do...end block. For example, with the
bash resource, we’re using the
code attribute to specify the code that will run to set the timezone (see the documentation to learn about the other attributes it supports). This resource is similar to the
execute resource, which also runs commands, but
execute is used to run a single command, while
code attribute is used to run more than one.
We could’ve specified attributes for the
package resources as well, but there is no need in this case. For example, this
1 2 3
is equivalent to
execute "apt-get update". The only difference between the two is the longer version gives the resource block a name—“update packages” in this case, but it could’ve been anything—while the shorter version just specifies the command to execute. When the
command attribute is missing, the resource name is the command that is executed, and that’s why the shorter version works just as well.
One last interesting thing about the
bash resource is the
not_if line. This is called a guard attribute, and it can be applied to any resource, not just
bash. It’s used to prevent a resource from running if certain conditions are met. In this case, I’m specifying a command that will output the current date and search for either the word “PDT” (Pacific Daylight Time) or “PST” (Pacific Standard Time) to see if the correct time zone is set (be sure to modify this for your own time zone). The guard will be applied and the resource won’t run if the command returns
0, which is the case with
grep when it finds a match.
Working with Users
This next recipe will create a new user and group. Add a new file called
users.rb to the directory containing the previous recipe, and fill it with the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
Here we’re first using the
group resource to create a new group whose name will come from the
group attribute defined in the JSON file we created earlier. Note the syntax to access that attribute; it stays the same whether you’re accessing attributes from the JSON file or the
Next, we use the
user resource to create a new user whose name is also defined in the JSON file. But now it gets interesting because there are a handful of additional attributes we’re using:
gidassigns this user to the group we created right before
homespecifies the location of the user’s home directory
passwordaccepts a shadow hash of the users password
shellspecifies the login shell that the user will log in with (user won’t have login access without this attribute)
trueto tell Chef to create the home directory when the user is created
bash resource adds a line to the
sudoers file that gives the group
sudo privileges, but it does this only if the line isn’t already there.
Restricting SSH Access
The next thing on our list is restricting SSH access to our node. Add the following code into a new file called
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
Chef has a
service resource designed for managing services like SSH. We tell it to manage SSH by specifying “ssh” as the resource name, and since we don’t have a
service_name attribute listed, Chef automatically assumes that the resource name is also the name of the service we want to manage.
We then use the
provider attribute to tell Chef to use upstart to manage the service. This is necessary whenever there are two or more ways available for managing a service, which is the case with SSH on the node I’m running. My node has an upstart job (located in
/etc/init) and a traditional init script (located in
/etc/init.d) for managing SSH, and I decided to go with upstart since it’s superior, but either will work for our purposes. If there is only one service provider available, Chef will detect it automatically, and there is no need for the
By default, Chef inspects the process table to see if a service is running, but it’s also possible to use the
service ssh status command (which is more reliable) to do the same thing. That’s why we use the
supports attribute to tell Chef to use the
status command instead. And while we’re at it, we also give Chef permission to use the
restart command to restart SSH.
Once the service is defined, we use the
bash resource to modify the SSH port, disable
root login, restrict login access just to our created user, and disable DNS lookup. Each
bash block also uses the
notifies attribute to tell the SSH
service resource we just defined to restart itself if the code inside the
code attribute actually runs. But we use the
delayed timer to tell Chef to queue up the notification and run in at the very end. The other option is to use the
immediate timer to do the restart immediately, but we don’t want to restart the service four separate times when we can just wait till the end and do it just once.
And just like in the two previous recipes, we use the
not_if guard to make sure the
bash resources don’t run if the necessary changes are already made.
Next order of business is installing Node.js. Add the following code into a new file called
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
The very first line makes use of Ohai, a tool Chef uses to detect attributes on a node and make them available for use in recipes. We’re extracting the type of architecture our node is running on to make sure we download the correct tar file for installing Node.js.
We download the tar file using a resource called
remote_file, and we then use the
source attribute to specify the source of the tar file,
mode to specify its mode, and
action to tell Chef to create the file only if it’s not already there.
One interesting thing about the
action attribute is it’s actually present in each resource we write, whether we explicitly assign one or not. Not every resource has the same actions though.
remote_file, for example, has
touch actions, while
bash only has
nothing actions. But every resource is assigned a default action, if we don’t assign one ourselves (the resource’s documentation will specify which action is assigned by default).
After the file is downloaded, we use
execute to do the install. One interesting thing about this resource is the
not_if guard, which executes some Ruby code, whereas the previous one only executed shell commands. Guards can run a shell command specified inside a string, or Ruby code specified inside a block. Ruby code must return either
false, while shell commands can return any value, but the guard runs only if a zero is returned (see the documentation).
So in the code above, we’re first executing some Ruby code to determine if a file exists, and then we execute a shell command to output the Node.js version we installed, but this output is processed by Ruby and gets compared with the version we’ve specified in our attributes file. As a result, Node.js won’t be installed if there is a Node.js executable already present on the node that returns the correct version number. (Note that shell commands will be processed by Ruby only if they’re specified using backquotes).
Our next recipe will install PostgreSQL. Create a new file called
postgres.rb, and add the following code into it:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
This recipe is fairly straightforward. We install Postgres using the
package resource and use
execute to modify the
postgres user’s password. We also create new user, along with a new database, which is assigned to the newly created user. (Note that we’re using the
user attribute to execute these commands as the
postgres user, which is necessary because otherwise Postgres would run these commands as
root, and such a user does not exist in Postgres by default.)
Installing rbenv and Ruby
It’s now time to install rbenv and Ruby. Add the following code into a new file called
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
We’re using a new resource here called
cookbook_file, which takes a file in our recipe and copies it to a specific location on our node. The file were creating is called
bash_profile, and it contains some code that allows rbenv to initialize itself properly (store it in your cookbook’s
1 2 3 4 5 6
If you go back to the
cookbook_file resource, you’ll see that we’re copying the file to the user’s home directory (since we did not specify a
path attribute, the resource’s name is also the path to the location where it will be stored), and we’re using the
group attributes to set the file’s mode, owner, and group, respectively.
Afterwards, we’re using
bash to install rbenv. Because we’re not doing a system-wide install, we run the rbnev installer under the user we created earlier, and we use the
cwd attribute to run the install inside the user’s home directory. Once that’s done, the last
bash block then uses rbenv to install Ruby itself.
Next in line is Redis, so go ahead and create a new file called
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
This recipe doesn’t contain any new Chef concepts. It simply downloads Redis using the
remote_file resource, installs it using the
bash resource, and installs the Redis server with the
The last thing left to install is Nginx, and here’s the code that will go in a new
nginx.rb file to do just that:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
There is only one new Chef concept here, and that’s the
template resource. It’s similar to the
cookbook_file resource in that it copies a file from a cookbook to a location on a node, but it also does much more than that; it allows you to modify the contents of the file by embedding Ruby code into it using ERB (Embedded Ruby) templates, just like you would if you wrote Ruby on Rails views in ERB. All the attributes that are accessible in your recipes are also accessible in template files, and when you combine this with the usual ERB features like conditional statements and blocks, you’ll be able to customize your files in any way you want.
Templates are stored in your cookbook’s
/templates directory, so go ahead and create a new file there called
nginx.conf.erb with the following code (note that the syntax for accessing attributes doesn’t change):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
This is the file we’re referencing inside the
template resource in
nginx.rb. You’ll notice the attributes we specify there are very similar to those specified in the
cookbook_file resources we wrote earlier. But one thing that’s different is we’re also using the
notifies attribute to call
restart on the previously defined Nginx service, which allows the new configuration file to be loaded in.
Our final recipe does some setup for our Rails application. Here’s the code you’ll need to add to a new file called
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
The only new resource here is
directory, which we use to create a new
/var/www directory for our Rails app. One other new thing is the
creates attribute inside the second
execute resource, which is used to prevent the resource from creating the
/var/www/phindee/shared/config directory if it already exists. If you’re wondering why we’re using
execute and not
directory, it’s because it’s pretty messy to create recursive directories using
directory, and this just seems cleaner to me.
And finally, here are the two template files we’re referencing inside the
template resources above:
1 2 3 4 5 6 7 8
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78
And with that, our recipes are complete! We’re now ready to use them to provision our node, and that’s exactly what we’ll cover in part 3.