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, 123.123.123.123.json
).
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:
1
|
|
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:
1
|
|
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.
Writing Recipes
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
, package
, and bash
. The 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 bash
’s code
attribute is used to run more than one.
We could’ve specified attributes for the execute
and 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 default.rb
file.
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:
gid
assigns this user to the group we created right beforehome
specifies the location of the user’s home directorypassword
accepts a shadow hash of the users passwordshell
specifies the login shell that the user will log in with (user won’t have login access without this attribute)supports
setsmanage_home
totrue
to tell Chef to create the home directory when the user is created
The last 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 ssh.rb
:
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 provider
attribute.
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.
Installing Node.js
Next order of business is installing Node.js. Add the following code into a new file called nodejs.rb
:
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 create
, create_if_missing
, delete
, and touch
actions, while bash
only has run
and 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 true
or 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).
Installing PostgreSQL
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 rbenv.rb
:
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 /files/default
directory):
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 mode
, owner
, and 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.
Installing Redis
Next in line is Redis, so go ahead and create a new file called redis.rb
:
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 execute
resource.
Installing Nginx
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.
App Setup
Our final recipe does some setup for our Rails application. Here’s the code you’ll need to add to a new file called app.rb
:
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.