A recipe is the most fundamental configuration element within the Chef environment. A recipe:
An attribute can be defined in a cookbook (or a recipe) and then used to override the default settings on a node. When a cookbook is loaded during a Chef run, these attributes are compared to the attributes that are already present on the node. When the cookbook attributes take precedence over the default attributes, Chef will apply those new settings and values during the Chef run on the node.
Note
Attributes can be configured in cookbooks (attribute files and recipes), roles, and environments. In addition, Ohai collects attribute data about each node at the start of the Chef run. See the overview of attributes for more information about how all of these attributes fit together.
Attribute types can be any of the following:
| Attribute Type | Description |
|---|---|
| default | A default attribute is automatically reset at the start of every Chef run and has the lowest attribute precedence. A cookbook should be authored to use default attributes as often as possible. |
| force_default | A force_default attribute is used to ensure that an attribute defined in a cookbook (by an attribute file or by a recipe) takes precedence over a default attribute set by a role or an environment. |
| normal | A normal attribute is a setting that persists on the target system and is never reset during a Chef run. A normal attribute has a higher attribute precedence than a default attribute. |
| override | An override attribute is automatically reset at the start of every Chef run and has a higher attribute precedence than default, force_default, and normal attributes. An override attribute is most often specified in a recipe, but can be specified in an attribute file, for a role, and/or for an environment. A cookbook should be authored so that it uses override attributes only when required. |
| force_override | A force_override attribute is used to ensure that an attribute defined in a cookbook (by an attribute file or by a recipe) takes precedence over an override attribute set by a role or an environment. |
| automatic | An automatic attribute contains data that is identified by Ohai at the beginning of every Chef run. An automatic attribute cannot be modified and always has the highest attribute precedence. |
At the beginning of a Chef run, all default, override, and automatic attributes are reset. Chef rebuilds them using data collected by Ohai at the beginning of the Chef run and by attributes that are defined in cookbooks, roles, and environments. Normal attributes are never reset. All attributes are then merged and applied to the node according to attribute precedence. At the conclusion of the Chef run, all default, override, and automatic attributes disappear, leaving only a collection of normal attributes that will persist until the next Chef run.
Attributes are always applied to Chef in the following order:
where the last attribute in the list is the one that is applied to the node.
Note
The attribute precedence order for roles and environments is reversed for default and override attributes. The precedence order for default attributes is environment, then role. The precedence order for override attributes is role, then environment. Applying environment override attributes after role override attributes allows a role to exist in multiple environments.
Attribute precedence, viewed from the same perspective as the Chef overview diagram, where the numbers in the diagram match the order of attribute precedence:
Attribute precedence, when viewed as a table:
Use the following methods within the attributes file for a cookbook or within a recipe. These methods correspond to the attribute type of the same name:
Additionally, there are _unless methods available. See the end of this topic for information on how to conditionally set attributes
In Unix, a process’ environment is a set of key-value pairs made available to the process. Often, programs expect their environment to contain information required for the program to run. The details of how these key-value pairs are accessed depends on the API of the language being used. This article explains how environments of child processes interact with their parent process and how you can use Chef to ensure that services and applications are started with the proper environment.
If you are starting a processes by using an execute or script resource, you can use the environment attribute to alter the environment that will be passed to the process.
bash "env_test" do
code<<-EOF
echo $FOO
EOF
environment { 'FOO' => "bar" }
end
Note that the only environment being altered is the environment being passed to the child process being started by the bash resource. This will not affect the environment of chef-client or subsequently started child processes.
The following sections show approaches to working with recipes.
A data bag is a global variable that is stored as JSON data and is accessible from a Chef Server. A data bag is indexed for searching and can be loaded by a recipe or accessed during a search. The contents of a data bag can vary, but they often include sensitive information (such as database passwords).
The contents of a data bag can be loaded into a recipe. For example, a data bag named “apps” and a data bag item named “my_app”:
{
"id": "my_app",
"repository": "git://github.com/company/my_app.git"
}
can be accessed in a recipe, like this:
my_bag = data_bag_item("apps", "my_app")
The data bag item’s keys and values can be accessed with a Ruby hash:
my_bag["repository"] #=> "git://github.com/company/my_app.git"
Encrypting a data bag requires a secret key. A secret key can be created in any number of ways. For example, OpenSSL can be used to generate a random number, which can then be used as the secret key:
$ openssl rand -base64 512 > encrypted_data_bag_secret
where encrypted_data_bag_secret is the name of the file which will contain the secret key. For example, to create a secret key named “my_secret_key”:
$ openssl rand -base64 512 > my_secret_key
An encryption key can also be stored in an alternate file on the nodes that need it and specify the path location to the file inside an attribute; however, EncryptedDataBagItem.load expects to see the actual secret as the third argument, rather than a path to the secret file. In this case, you can use EncryptedDataBagItem.load_secret to slurp the secret file contents and then pass them:
# inside your attribute file:
# default[:mysql][:secretpath] = "C:\\chef\\any_secret_filename"
#
# inside your recipe:
# look for secret in file pointed to by mysql attribute :secretpath
mysql_secret = Chef::EncryptedDataBagItem.load_secret("#{node[:mysql][:secretpath]}")
mysql_creds = Chef::EncryptedDataBagItem.load("passwords", "mysql", mysql_secret)
mysql_creds["pass"] # will be decrypted
To demonstrate the use of encrypted data bags on a node, we’ll start by copying the secret_key file to an example node using scp and moving it to /etc/chef/encrypted_data_bag_secret:
scp ./secret_key $MY_NODE_IP:~/
ssh $MY_NODE_IP
sudo mv ./secret_key /etc/chef/encrypted_data_bag_secret
The knife bootstrap sub-command supports the encrypted_data_bag_secret setting in knife.rb. You will want to add this line:
encrypted_data_bag_secret '/path/to/your/data_bag_key'
And change /path/to/your/data_bag_key to the location of where the data bag key is located. When you run knife bootstrap afterwards it automatically adds this line to the client.rb for the node you are bootstrapping and copies the key over.
Next, we’ll create a recipe that will log the decrypted values for demonstration purposes (if these were real secrets, you would want to avoid logging them). Use Knife and run the following:
$ knife cookbook create edb_demo
Then, edit cookbooks/edb_demo/recipes/default.rb so that it contains the following:
# cookbooks/edb_demo/recipes/default.rb
passwords = Chef::EncryptedDataBagItem.load("prod", "passwords")
mysql = passwords["mysql"]
Chef::Log.info("The mysql password is: '#{mysql}'")
Finally, upload the cookbook and run chef-client on the node. You should see something like this:
$ knife cookbook upload edb_demo
# output clipped
knife ssh name:i-8a436fe5 -a ec2.public_hostname 'sudo chef-client'
INFO: *** Chef 0.10.0 ***
INFO: Run List is [recipe[edb_demo]]
INFO: Run List expands to [edb_demo]
INFO: Starting Chef Run for i-8a436fe5
INFO: Loading cookbooks [edb_demo]
INFO: The mysql password is: 'open-sesame-123'
INFO: Chef Run complete in 3.122228 seconds
INFO: Running report handlers
INFO: Report handlers complete
As you can see, the recipe was able to decrypt the values in the encrypted data bag. It did so by using the shared secret located in the default location of /etc/chef/encrypted_data_bag_secret.
If a cookbook has a dependency on a recipe that is located in another cookbook, that dependency must be declared in the metadata.rb file for that cookbook using the depends keyword.
Note
Declaring cookbook dependencies is not required with chef-solo.
For example, if the following recipe is included in a cookbook named “my_app”:
include_recipe "apache2::mod_ssl"
Then the metadata.rb file for that cookbook would have:
depends "apache2"
A recipe can write events to a Chef log file and can cause exceptions using Chef::Log. The levels include debug, info, warn, error, and fatal. For example, to just capture information:
Chef::Log.info('some useful information')
Or to trigger a fatal exception:
Chef::Log.fatal!('something bad')
A recipe can include one (or more) recipes found in other cookbooks by using the include_recipe keyword. When a recipe is included, the resources found in that recipe will be inserted (in the same exact order) at the point where the include_recipe keyword is located. The syntax for including a recipe is like this:
include_recipe "recipe"
For example:
include_recipe "apache2::mod_ssl"
If a recipe is included more than once in a recipe, only the first inclusion will be processed and any subsequent inclusion will be ignored.
Attributes sometimes depend on actions taken from within recipes, so it may be necessary to reload a given attribute from within a recipe. For example:
ruby_block 'some_code' do
block do
node.from_file(run_context.resolve_attribute("COOKBOOK_NAME", "ATTR_FILE"))
end
action :nothing
end
Attribute accessor methods are automatically created and the method invocation can be used interchangeably with the keys. For example:
default.apache.dir = "/etc/apache2"
default.apache.listen_ports = [ "80","443" ]
This is a matter of style and preference for how attributes are reloaded from recipes, and may be seen when “retrieving” the value of an attribute.
Anything that can be done with Ruby can be used within a recipe, such as expressions (if, unless, etc.), case statements, loop statements, arrays, hashes, and variables. In Ruby, the conditionals nil and false are false; every other conditional is true.
A variable uses an equals sign (“=”) to assign a value.
To assign a value to a variable:
package_name = "apache2"
A case statement can be used to compare an expression (specified by case) and then executing the code that matches the expression.
To select a package name based on platform:
package "apache2" do
case node[:platform]
when "centos","redhat","fedora","suse"
package_name "httpd"
when "debian","ubuntu"
package_name "apache2"
when "arch"
package_name "apache"
end
action :install
end
An if expression can be used to check for conditions (true or false).
To check for condition only for Debian and Ubuntu platforms:
if platform?(“debian”, “ubuntu”)
# do something if node[‘platform’] is debian or ubuntu
else
# do other stuff
end
An unless expression can be used to execute code when a condition returns a false value (effectively, an unless expression is the opposite of an if statement).
To use an expression to execute when a condition returns a false value:
unless node[:platform_version] == "5.0"
# do stuff on everything but 5.0
end
A loop statement is used to execute a block of code one (or more) times. A loop statement is created when .each is added to an expression that defines an array or a hash. An array is an integer-indexed collection of objects. Each element in an array can be associated with and referred to by an index.
To loop over an array of package names by platform:
["apache2", "apache2-mpm"].each do |p|
package p
end
A hash is a collection of key-value pairs. Indexing for a hash is done using arbitrary keys of any object (as opposed to the indexing done by an array). The syntax for a hash is: key => "value".
To loop over a hash of gem package names:
{"fog" => "0.6.0", "highline" => "1.6.0"}.each do |g,v|
gem_package g do
version v
end
end
A recipe must be assigned to a run-list using the appropriate name, as defined by the cookbook directory and namespace. For example, a cookbook directory has the following structure:
cookbooks/
apache2/
recipes/
default.rb
mod_ssl.rb
There are two recipes: a default recipe (that has the same name as the cookbook) and a recipe named mod_ssl. The syntax that applies a recipe to a run-list is similar to:
{
"run_list": [
"recipe[cookbook_name::default_recipe]",
"recipe[cookbook_name::recipe_name]"
]
}
where ::default_recipe is implied (and does not need to be specified). On a node, these recipes can be assigned to a node’s run-list similar to:
{
"run_list": [
"recipe[apache2]",
"recipe[apache2::mod_ssl]"
]
}
Use Knife to add the recipe to the node’s run-list. For example:
$ knife node run list add NODENAME "recipe[apache2]"
More than one recipe can to be added:
% knife node run list add NODENAME "recipe[apache2],recipe[mysql],role[ssh]"
run_list:
recipe[apache2]
recipe[mysql]
role[ssh]
Use a JSON file to pass run-list details to chef-solo as long as the cookbook in which the recipe is located is available to the system on which chef-solo is running. For example, a JSON file named “dna.json” contains the following details:
{
"run_list": ["recipe[apache2]"]
}
To add the run-list to the node, enter the following:
$ sudo chef-solo -j /etc/chef/dna.json
Search indexes allow queries to be made for any type of data that is indexed by the Chef Server, including data bags (and data bag items), environments, nodes, and roles. Chef has a defined query syntax that supports search patterns like exact, wildcard, range, and fuzzy. A search is a full-text query that can be done from several locations, including from within a recipe, by using the search subcommand in Knife, by using the search functionality in the Management Console, or by using the /search or /search/INDEX endpoints in the Chef Server API. The search engine is based on Apache Solr and is run from the Chef Server.
The results of a search query can be loaded into a recipe. For example, a very simple search query (in a recipe) might look like this:
search(:node, "attribute:value")
A search query can be assigned to variables and then used elsewhere in a recipe. For example, to search for all nodes that have a role assignment named “webserver”, and then render a template which includes those role assignments:
webservers = search(:node, "role:webserver")
template "/tmp/list_of_webservers" do
source "list_of_webservers.erb"
variables(:webservers => webservers)
end
A tag is a custom description that is applied to a node. A tag, once applied, can be helpful when managing nodes using Knife or when building recipes by providing alternate methods of grouping similar types of information.
Tags can be added and remove. Machines can be checked to see if they already have a specific tag. To use tags in your recipe simply add the following:
tag('mytag')
To test if a machine is tagged, add the following:
tagged?('mytag')
to return true or false. tagged? can also use an array as an argument.
To remove a tag:
untag('mytag')
For example:
tag("machine")
if tagged?("machine")
Chef::Log.info("Hey I'm #{node[:tags]}")
end
untag("machine")
if not tagged?("machine")
Chef::Log.info("I has no tagz")
end
Will return something like this:
[Thu, 22 Jul 2010 18:01:45 +0000] INFO: Hey I'm machine
[Thu, 22 Jul 2010 18:01:45 +0000] INFO: I has no tagz
Sometimes it may be necessary to end a Chef run before it completes. There are a few ways to do this:
The following sections show various approaches to ending a Chef run.
Using the return keyword and a condition is typically the most direct approach to stopping a Chef run. When the condition is met, stop the run. When the condition is not met, allow the run to continue. The following example shows how the return keyword can be used to set a condition that, if met, will stop a Chef run:
file '/tmp/name_of_file' do
action :create
end
return if node['platform'] == 'windows'
package 'name_of_package' do
action :install
end
where node['platform'] == 'windows' is the condition set on the return keyword. This condition is useful in a situation where a Microsoft Windows system cannot install the package named name_of_package. In a situation where this condition is met, there is no need for the recipe to continue and consequently it is OK for the Chef run to be stopped.
In certain situations it may be useful to stop a Chef run entirely, such as when an unhandled exception occurs. For example, a template resource may not be able to find its source file or a user who is running Chef does not have permission to create a directory. The raise keyword can be useful for stopping the Chef run if an unhandled exception occurs. There are two approaches:
For example, using the raise keyword in a recipe to raise an exception during the compile phase may look something like this:
file '/tmp/name_of_file' do
action :create
end
raise if node['platform'] == 'windows'
package 'name_of_package' do
action :install
end
where node['platform'] == 'windows' is the condition set on the raise keyword. This condition is useful in a situation where the Microsoft Windows system does not have a package manager available, but the package (name_of_package) should be installed. Because the package should be installed, but cannot be, Chef will exit the run with a fatal error and will provide a stack trace.
Since recipes are written in Ruby, they can be written to attempt to handle error conditions using the rescue block.
For example:
begin
dater = data_bag_item(:basket, "flowers")
rescue Net::HTTPServerException
# maybe some retry code here?
raise "message_to_be_raised"
end
where data_bag_item makes an HTTP request to the Chef Server to get a data bag item named flowers. If there is a problem, the request will return a Net::HTTPServerException. The rescue block can be used to try to retry or otherwise handle the situation. If the rescue block is unable to handle the situation, then the raise keyword is used to specify the message to be raised.
If a log entry is required, use Chef::Application.fatal! to log a fatal message to a log file using standard error output. After the log message is sent, Chef will stop the run. Something like the following can be used to trigger this type of log entry:
Chef::Application.fatal!("Didn't expect some_condition", return_code) if some_condition
where some_condition is the condition to be met, return_code is the code that will be identified in the log entry. When this condition is met, Chef will exit, send a log message and return the code specified with return_code from the process.