Chapter 4. Write Your First Chef Recipe

Create a Directory Structure for Your Code

Since we will be writing a lot of code over the remainder of this book, let’s create a simple directory structure—organizing the code by chapter—like the one below (There is no need to use this exact directory structure to organize your files. It is only a suggestion. Use a system that makes sense to you):

learningchef
|_ chap04
|_ chap05
...
|_ chap16

In your home directory, create a subdirectory named learningchef, making it the current directory:

$ cd
$ mkdir learningchef
$ cd learningchef

Then create a chap04 subdirectory for the code examples you will be writing in this chapter. Make chap04 the current directory:

$ mkdir chap04
$ cd chap04

Follow a similar pattern for each new chapter, creating a new subdirectory underneath learningchef to contain each chapter’s examples. The code examples for this book follow this convention. When a specific directory structure is required for an example, we’ll let you know; otherwise, assume you can put the files anywhere you find convenient.

Write Your First Chef Recipe

To show you the basics, let’s write the simplest form of Chef code to make a “Hello World” recipe. A recipe is a file that contains Chef code.

Using your favorite text editor, create the recipe file hello.rb to match Example 4-1. This file can be anywhere—no specific directory structure is required. By convention, files that contain Chef code have the extension .rb to show they are written in Ruby.

The Chef coding language is a Ruby Domain Specific Language (DSL). It contains additional Ruby-like statements specialized for expressing Chef system administration concepts.

Example 4-1. hello.rb
file 'hello.txt' do
  content 'Welcome to Chef'
end

Note

It’s not necessary to place hello.rb or any of the other *.rb example files in this chapter in a special directory. To find the hello.rb file containing the code from the preceding example, look among the source code examples for the book in the chap04/ directory. Other examples in this and subsequent chapters can be found in similarly titled chapter directories.

We’ll go over what all the statements in this file mean in more detail in Examine hello.rb. Enter the code using a text editor, making sure you match the capitalization, spacing, and syntax exactly.

The file statement code you entered in hello.rb. is a resource. Resources are the building blocks for assembling Chef code. A resource is a statement within a recipe that helps define actions for Chef to perform. This particular file resource in hello.rb tells Chef to:

  • Create the file hello.txt.
  • Write the content Welcome to Chef to hello.txt.

Use the chef-apply command to get Chef to perform the actions indicated in your newly created hello.rb file.

Note

Chef requires administrator privileges to run. If you are running User Account Control (UAC) on Windows, make sure you Run As Administrator. On Linux/Mac OS X, run chef-apply with sudo privileges if you are not running as root.

When you run chef-apply hello.rb, the output should resemble, for Linux/Mac OS X:

$ sudo chef-apply hello.rb
Recipe: (chef-apply cookbook)::(chef-apply recipe)
  * file[hello.txt] action create
    - create new file hello.txt
    - update content in file hello.txt from none to 40a30c
    --- hello.txt    2014-08-10 22:27:44.000000000 -0700
    +++ /tmp/.hello.txt20140810-14225-6e7qc7    2014-08-10 22:27:44.000000000
    -0700 @@ -1 +1,2 @@
    +Welcome to Chef

For Windows (Run As Administrator):

> chef-apply hello.rb
Recipe: (chef-apply cookbook)::(chef-apply recipe)
  * file[hello.txt] action create
    - create new file hello.txt
    - update content in file hello.txt from none to 40a30c
        --- hello.txt   2014-07-11 12:38:30.000000000 -0700
        +++ C:/Users/misheska/AppData/Local/Temp/hello.txt20140711-2344-mf17rh
        @@ -1 +1,2 @@
        +Welcome to Chef

Verify Your First Chef Recipe

Congratulations, you just automated the creation of the hello.txt file using Chef!

Verify that your hello.rb recipe performed the correct action. Look to see if a hello.txt file exists in the current directory alongside your hello.rb file and that it has the correct content:

$ more hello.txt
Welcome to Chef

Examine hello.rb

Let’s go over each line in hello.rb from Example 4-1 in more detail, exploring the purpose of each component. As mentioned earlier, Chef code uses a domain-specific language (DSL) built on top of the Ruby programming language. Having expressions tailored for system administration makes Chef code more accessible to beginners. The DSL is also designed to make you focus more on describing what the desired configuration of a machine should be, rather than how it should be accomplished. Desired configuration is a concept we’ll cover in more detail in Recipes Specify Desired Configuration.

Note

Because Chef recipes are code, we recommend that you use some form of source control to manage your Chef source. It is beyond the scope of this book to show you how to use version control to manage source. However, use version control for everything you do with Chef. Any version control system will do: Git, Subversion, Mercurial, Team Foundation Server, and so on.

The first line of hello.rb contains a file resource referring to the file hello.txt:

file 'hello.txt' do

Remember that resources are building blocks that Chef uses to configure things on a system. The file resource is used to manage a file on a computer. The file resource takes a string parameter specifying the path to the file. In a Chef recipe, this is denoted by enclosing the string in double quotes (“”) or single quotes (''). For example, use the following file resource syntax, to specify the filename /usr/local/hello.txt:

file "/usr/local/hello.txt"

It doesn’t matter whether you use double or single quotes for string literals; either choice is valid:

file '/usr/local/hello.txt'

Now, what is that do clause at the end of the first line? The do statement at the end of the first line denotes the start of a block. To specify extra parameters in a resource statement, it must span multiple lines. When resource statements span multiple lines, everything but the first line must be enclosed by a do..end pair. The do…end pair containing the extra lines is referred to as a block.

Line two contains a reference to a content attribute, specifying a string that should be written to the file:

  content 'Welcome to Chef'

For now, just think of an attribute as yet another variable maintained by Chef that can be used as a parameter to a resource. We’ll delve more deeply into attributes in Chapter 8. By convention, statements in Chef recipes are indented with spaces when they are inside a block. Thus, the content attribute is indented two spaces, following Ruby convention.

The string Welcome to Chef is passed as a content attribute to the file resource. The file resource writes out the specified content string attribute to the hello.txt file.

Finally, line three completes the block for the file resource with an end statement, finishing off the do..end pair:

end

This example should give you an idea of what Chef code looks like, building on the introduction to Ruby, the core of Chef, which we covered in Chapter 3.

Recipes Specify Desired Configuration

Let’s explore the concept that you only need to tell Chef what the desired configuration should be, not how to achieve it, that we touched on earlier. Figure 4-1 illustrates this concept. Before Chef performs actions, it refers to the resources and attributes in a recipe to answer the question "What do I care about?” Then Chef decides how to put the system in the desired configuration by reasoning about the current state of the system. As a result, Chef code tends to be more succinct than equivalent bash or PowerShell scripting code as you only need to specify the desired configuration in your code, not how this configuration must be achieved. Chef determines how automatically and autonomously.

Recipes specify desired configuration
Figure 4-1. Recipes specify desired configuration

Let’s make this concept more concrete by writing some Chef code. Create a new recipe alongside hello.rb called stone.rb following Example 4-2. Similar to hello.rb, stone.rb does not need to be in any specially named directory structure.

Example 4-2. stone.rb
file "#{ENV['HOME']}/stone.txt" do
  content 'Written in stone'
end

stone.rb is just a slight modification of the earlier hello.rb recipe in Example 4-1. The string Written in stone will be written to stone.txt. However, in this example, the stone.txt file will be written to your home directory instead of the directory where you run chef-apply.

Why did we make this change? It is not safe to use implied relative paths like stone.txt with Chef resources. On some platforms, behind the scenes, Chef could be running in a different place than you expect. We constrained the things you did in the first hello.rb example, so it was safe, but in general you must use absolute paths when specifying a filename to a resource. Plus, keep in mind that Chef recipes are intended to be run on different machines and even different operating systems producing the same configuration. Chef steers you toward using absolute paths in recipes because it would be difficult to specify consistent file locations using relative paths.

Note

On Windows, Chef will convert absolute paths with forward slashes (/) to use the Windows-style backslash character (\). You can use the backslash character, but Chef code uses the backslash (\) as an escape character in strings. So, you have to specify a double backslash (\\) to insert a literal single backslash in a string. In this book, we’ll stick with using forward slashes (/) on Windows.

Let’s explain the #{ENV['HOME']} construct and why we changed the file string to use double quotes (“”) instead of single quotes ('\'). In Chef code, it doesn’t matter if you use double quotes or single quotes for string literals. However, it does matter when you want to evaluate the value of a variable in a string (also known as string interpolation).

#{ENV['HOME']} is a variable referring to the current user’s home directory. Variable references within strings are denoted by #{<variable>} (a hash character followed by the variable enclosed by a set of curly braces). A string must be enclosed in double quotes (“”) when it contains a variable reference. Otherwise, Chef will not replace the variable reference with its value when the string is evaluated.

The ENV['HOME'] variable is a reference to a collection of name-value pairs. Chef calls a collection of name-value pairs a Hash. Other languages refer to this construct as dictionary, associative array, or a map—it’s all the same thing. In a Chef recipe, you can retrieve the value for a system environment variable by referring to the variable ENV['<name>']. name refers to a string with an environment value name. This is the code equivalent of:

  • echo $HOME on Linux/Mac OS X/Windows PowerShell
  • echo %USERPROFILE% on Windows Command Prompt

For example, my home directory is /Users/misheska. Chef evaluates the string "#{ENV['HOME']}/stone.txt" as "/Users/misheska/stone.txt".

Note

Sharp-eyed readers might wonder how we could get away with using the $HOME reference if you are using the Windows Command Prompt. If you try to echo %HOME% using the Windows Command Prompt, you’ll discover the environment variable doesn’t exist. By default on Windows, internally Chef uses PowerShell to evaluate command-line references, even when you run Chef on the Windows Command Prompt. PowerShell is more Unix-like than the Windows Command Prompt, so Chef uses PowerShell by default.

Now that we’ve explained the changes to the source, run your Chef code using chef-apply on a command line. The output should resemble, for Linux/Mac OS X:

$ sudo chef-apply stone.rb
Recipe: (chef-apply cookbook)::(chef-apply recipe)
  * file[/Users/misheska/stone.txt] action create
    - create new file /Users/misheska/stone.txt
    - update content in file /Users/misheska/stone.txt from none to ba4fda
    --- /Users/misheska/stone.txt    2014-08-10 22:33:40.000000000 -0700
    +++ /tmp/.stone.txt20140810-14302-1nfmi0r   2014-08-10 22:33:40.000000000
    -0700 @@ -1 +1,2 @@
    +Written in stone

For Windows (Run As Administrator):

> chef-apply stone.rb
Recipe: (chef-apply cookbook)::(chef-apply recipe)
  * file[C:/Users/misheska/stone.txt] action create
    - create new file C:/Users/misheska/stone.txt
    - update content in file C:/Users/misheska/stone.txt from none to ba4fda
        --- C:/Users/misheska/stone.txt 2014-07-11 15:48:46.000000000 -0700
        +++ C:/Users/misheska/AppData/Local/Temp/stone.txt20140711-2232-1wpswfb
        @@ -1 +1,2 @@
        +Written in stone

Now the file stone.txt should be created in your home directory with the content Written in stone. Verify with the following command for Linux/Mac OS X/Windows PowerShell:

$ more $HOME/stone.txt
Written in stone

or, for Windows Command Prompt:

> more %USERPROFILE%\stone.txt
Written in stone

Try running chef-apply using the same stone.rb recipe one more time. You should notice that the output is a little different executing the same recipe for the second time.

Linux/Mac OS X:

$ sudo chef-apply stone.rb
Recipe: (chef-apply cookbook)::(chef-apply recipe)
  * file[/Users/misheska/stone.txt] action create (up to date)

Windows (Run As Administrator):

> chef-apply stone.rb
Recipe: (chef-apply cookbook)::(chef-apply recipe)
  * file[C:/Users/misheska/stone.txt] action create (up to date)

chef-apply reports that file[...stone.txt] action create is up to date and that no action was performed. This is a good example of how chef-apply behaves differently depending on the machine’s state. Chef performs actions autonomously without being explicitly told to do so:

  • If stone.txt does not exist, chef-apply creates the file with the appropriate content.
  • If stone.txt already exists, chef-apply will do nothing.

Do you think that chef-apply is smart enough to detect someone tampering with file content outside of Chef? Let’s try an experiment. Change the contents of stone.txt with the following command for Linux/Mac OS X:

$ sudo sh -c 'echo "Modifying this file written in stone" > $HOME/stone.txt'

For Windows Command Prompt:

> echo Modifying this file written in stone > %USERPRFOILE%\stone.txt

For Windows PowerShell:

$ echo "Modifying this file written in stone" > $HOME\stone.txt

Verify that the file contents were changed by running one of the following for the Linux/Mac OS X/Windows PowerShell platform:

$ more $HOME/stone.txt
Modifying this file written in stone

For Windows Command Prompt:

$ more %USERPROFILE%\stone.txt
Modifying this file written in stone

Run chef-apply again for Linux/Mac OS X:

$ sudo chef-apply stone.rb
Recipe: (chef-apply cookbook)::(chef-apply recipe)
  * file[/Users/misheska/stone.txt] action create
    - update content in file /Users/misheska/stone.txt from 283cb7 to ba4fda
    --- /Users/misheska/stone.txt    2014-08-10 22:35:22.000000000 -0700
    +++ /tmp/.stone.txt20140810-14428-1uxzrvv   2014-08-10 22:35:46.000000000
    -0700 @@ -1,2 +1,2 @@
    -Modifying this file written in stone
    +Written in stone

or, for Windows (Run As Administrator):

> chef-apply stone.rb
Recipe: (chef-apply cookbook)::(chef-apply recipe)
  * file[C:/Users/misheska/stone.txt] action create
    - update content in file C:/Users/misheska/stone.txt from 7400c9 to ba4fda
        (current file is binary, diff output suppressed)

Notice that chef-apply reports that it performed an action. What action was performed? Check the content of stone.txt again on Linux/Mac OS X/Windows PowerShell:

$ more $HOME/stone.txt
Written in stone

or, for Windows Command Prompt:

> more %USERPROFILE%\stone.txt
Written in stone

Notice that chef-apply reverted the content back to Written in stone.

This is how Chef prevents configuration drift. Chef not only decides whether or not files are created, but it also checks file content. When a file is inadvertently modified, Chef makes sure the file reverts back to the content specified in the recipe.

The only way you can change the contents of stone.txt is by specifying different content in the stone.rb recipe. Otherwise, chef-apply reverts the content of stone.txt back to what the recipe specifies.

Chef decides the actions to perform to make the system configuration match what the recipe specifies. As a Chef developer, you only need to tell Chef the desired configuration. Chef takes care of all the rest automatically.

To Uninstall, Specify What Not to Do

You might wonder if it is possible to get Chef to automatically uninstall everything it installs. Not quite, but you can perform the equivalent of an uninstallation by telling Chef explicitly what not to do.

This might seem like Chef falls short in the uninstallation department, but that’s not the case. Remember, Chef tries to be smart. You don’t need to tell Chef how to do something. Instead you define the desired configuration you want in a recipe, and Chef determines what to do. Your recipe tells Chef when to stop reasoning about the configuration of the machine by defining what the desired configuration looks like.

There is no reasonable way for Chef to automatically reverse changes or uninstall and ensure that a system will consistently be in a known good configuration. You probably already know this is an impossible problem to solve in general. Every system administrator has come to the point in troubleshooting an issue caused by unknown changes to a computer where he gives up, wipes the box, and starts over again from scratch.

You might have thought that if you merely had enough time or were more persistent in your troubleshooting, you could solve an issue. Mark Burgess, the computer scientist introduced in Chapter 1 who made significant contributions to the automation theory upon which Chef is based, did the math and proved otherwise, because order matters. Based on the theory supported by this math, Chef restricts itself in trying to reason about the state of the system only to the extent of what is explicitly defined in a recipe. This ensures that your system will always be consistently what the recipe defines as a “good” configuration. Then Chef can be smart and repair the system, as in the example from the previous section when you skirted around Chef and modified the content of stone.txt manually. Chef was able to assess that there was a change in the configuration and reverted stone.txt back to the configuration defined in the recipe.

Thus, if you want Chef to perform an uninstall, you must explicitly define what not to do. All resources support this kind of definition in some fashion. In the case of a file resource, you can tell Chef that a file is no longer supposed to be present on the system. Then Chef will perform the inverse of the reasoning it performed to create the file:

  • If the file exists, chef-apply deletes the file.
  • If the file is verifiably not present, chef-apply will do nothing.

To close out this chapter, let’s write a recipe to clean up the stone.txt file we just created. Create cleanup.rb following Example 4-3.

Example 4-3. cleanup.rb
file "#{ENV['HOME']}/stone.txt" do
  action :delete
end

Let’s review this code before running chef-apply.

The file resource performs the :create action by default, but you can override this default with the :delete action instead. action is an attribute that can be specified in a file resource, to override the default setting. In cleanup.rb we’ve specified that our recipe perform the :delete action.

Note

In Chef code, a string prefaced by a single colon (:) is called a symbol. In other languages this is equivalent to a string constant.

Now let’s perform a Chef run using the cleanup.rb recipe on Linux/Mac OS X:

$ sudo chef-apply cleanup.rb
Recipe: (chef-apply cookbook)::(chef-apply recipe)
  * file[/Users/misheska/stone.txt] action delete
    - delete file /Users/misheska/stone.txt

or, in Windows (Run As Administrator):

> chef-apply cleanup.rb
Recipe: (chef-apply cookbook)::(chef-apply recipe)
  * file[C:/Users/misheska/stone.txt] action delete
    - delete file C:/Users/misheska/stone.txt

Note

In Chef, using relative paths with :delete is problematic on some platforms, so just delete the hello.txt file by hand that you created in the first exercise.

We’ve cleaned up the stone.txt we created in this final hands-on exercise in the chapter. chef-apply deleted stone.txt.

Summary

In this chapter we introduced the chef-apply command, showing you how to run .rb files containing Chef code.

We introduced the following Chef concepts and terminology:

recipe
A set of instructions written in a Ruby DSL that indicate the desired configuration to Chef.
resource
A cross-platform abstraction for something managed by Chef (such as a file). Resources are the building blocks from which you compose Chef code.
attribute
Parameters passed to a resource.

You created recipe files with Chef code, and ran chef-apply to perform the actions specified in the recipe. You learned that in Chef code, you need only tell Chef the desired configuration using resources as building blocks. We showed you how to use the file resource to create a file, and how to use the action :delete attribute to delete a file.

In the next chapter, we will show you how to create a sandbox environment using Test Kitchen, so that you have a safe place to experiment and learn more about Chef.

Get Learning Chef now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.