Custom Resources

Warning

This approach to building custom resources was introduced in chef-client, version 12.5. If you are using an older version of the chef-client, please use the version picker (in the top left of the navigation) to select your version, and then choose the same topic from the navigation tree (“Extend Chef > Custom Resources”). See also https://github.com/chef-cookbooks/compat_resource for using this features with previous versions of the chef-client.

A custom resource:

  • Is a simple extension of Chef
  • Is implemented as part of a cookbook
  • Follows easy, repeatable syntax patterns
  • Effectively leverages resources that are built into Chef
  • Is reusable in the same way as resources that are built into Chef

For example, Chef includes built-in resources to manage files, packages, templates, and services, but it does not include a resource that manages websites.

Syntax

A custom resource is defined as a Ruby file and is located in a cookbook’s /resources directory. This file

  • Declares the properties of the custom resource
  • Loads current properties, if the resource already exists
  • Defines each action the custom resource may take

The syntax for a custom resource is. For example:

property :name, RubyType, default: 'value'

load_current_value do
  # some Ruby
end

action :name do
 # a mix of built-in Chef resources and Ruby
end

action :name do
 # a mix of built-in Chef resources and Ruby
end

where the first action listed is the default action.

Example

For example, the site.rb file in the exampleco cookbook could be similar to:

property :homepage, String, default: '<h1>Hello world!</h1>'

load_current_value do
  if ::File.exist?('/var/www/html/index.html')
    homepage IO.read('/var/www/html/index.html')
  end
end

action :create do
  package 'httpd'

  service 'httpd' do
    action [:enable, :start]
  end

  file '/var/www/html/index.html' do
    content homepage
  end
end

action :delete do
  package 'httpd' do
    action :delete
  end
end

where

  • homepage is a property that sets the default HTML for the index.html file with a default value of '<h1>Hello world!</h1>'
  • the (optional) load_current_value block loads the current values for all specified properties, in this example there is just a single property: homepage
  • the if statement checks to see if the index.html file is already present on the node. If that file is already present, its contents are loaded instead of the default value for homepage
  • the action block uses the built-in collection of resources to tell the chef-client how to install Apache, start the service, and then create the contents of the file located at /var/www/html/index.html
  • action :create is the default resource; action :delete must be called specifically (because it is not the default resource)

Once built, the custom resource may be used in a recipe just like the any of the resources that are built into Chef. The resource gets its name from the cookbook and from the file name in the /resources directory, with an underscore (_) separating them. For example, a cookbook named exampleco with a custom resource named site.rb is used in a recipe like this:

exampleco_site 'httpd' do
  homepage '<h1>Welcome to the Example Co. website!</h1>'
  action :create
end

and to delete the exampleco website, do the following:

exampleco_site 'httpd' do
  action :delete
end

resource_name

By default, the custom resource name is inferred from the name of the cookbook and the name of the resource file, separated by an underscore(_). For example, a cookbook named website and a custom resource file named httpd is used in a recipe with website_httpd.

Use the resource_name method at the top of a custom resource to declare a custom name for that resource. For example:

resource_name :name

where :name declares the resource name as it may be used in a recipe.

For example, the httpd.rb file in the website cookbook could be assigned a custom resource name like this:

resource_name :httpd

property :homepage, String, default: '<h1>Hello world!</h1>'

load_current_value do
  if ::File.exist?('/var/www/html/index.html')
    homepage IO.read('/var/www/html/index.html')
  end
end

action :create do
  package 'httpd'

  service 'httpd' do
    action [:enable, :start]
  end

  file '/var/www/html/index.html' do
    content homepage
  end
end

and is then usable in a recipe like this:

httpd 'build website' do
  homepage '<h1>Welcome to the Example Co. website!</h1>'
  action :create
end

Scenario: website Resource

Create a resource that configures Apache httpd for Red Hat Enterprise Linux 7 and CentOS 7.

This scenario covers the following:

  1. Defining a cookbook named website
  2. Defining two properties
  3. Defining an action
  4. For the action, defining the steps to configure the system using resources that are built into Chef
  5. Creating two templates that support the custom resource
  6. Adding the resource to a recipe

Note

Read this scenario as an HTML presentation at https://docs.chef.io/decks/custom_resources.html.

Create a Cookbook

This article assumes that a cookbook directory named website exists in a chef-repo with (at least) the following directories:

/website
  /recipes
  /resources
  /templates

You may use a cookbook that already exists or you may create a new cookbook.

See https://docs.chef.io/ctl_chef.html for more information about how to use the chef command-line tool that is packaged with the Chef development kit to build the chef-repo, plus related cookbook sub-directories.

Objectives

Define a custom resource!

A custom resource typically contains:

  • A list of defined custom properties (property values are specified in recipes)
  • At least one action (actions tell the chef-client what to do)
  • For each action, use a collection of resources that are built into Chef to define the steps required to complete the action

What is needed?

This custom resource requires:

  • Two template files
  • Two properties
  • An action that defines all of the steps necessary to create the website

Define Properties

Custom properties are defined in the resource. This custom resource needs two:

  • instance_name
  • port

These properties are defined as variables in the httpd.conf.erb file. A template block in recipes will tell the chef-client how to apply these variables.

In the custom resource, add the following custom properties:

property :instance_name, String, name_property: true
property :port, Fixnum, required: true

where

  • String and Fixnum are Ruby types (all custom properties must have an assigned Ruby type)
  • name_property: true allows the value for this property to be equal to the 'name' of the resource block

The instance_name property is then used within the custom resource in many locations, including defining paths to configuration files, services, and virtual hosts.

Define Actions

Each custom resource must have at least one action that is defined within an action block:

action :create do
  # the steps that define the action
end

where :create is a value that may be assigned to the action property for when this resource is used in a recipe.

For example, the action appears as a property when this custom resource is used in a recipe:

custom_resource 'name' do
  # some properties
  action :create
end

Define Resource

Use the package, template (two times), directory, and service resources to define the website resource. Remember: order matters!

package

Use the package resource to install httpd:

package 'httpd' do
  action :install
end

template, httpd.service

Use the template resource to create an httpd.service on the node based on the httpd.service.erb template located in the cookbook:

template "/lib/systemd/system/httpd-#{instance_name}.service" do
  source 'httpd.service.erb'
  variables(
    :instance_name => instance_name
  )
  owner 'root'
  group 'root'
  mode '0644'
  action :create
end

where

  • source gets the httpd.service.erb template from this cookbook
  • variables assigns the instance_name property to a variable in the template

template, httpd.conf

Use the template resource to configure httpd on the node based on the httpd.conf.erb template located in the cookbook:

template "/etc/httpd/conf/httpd-#{instance_name}.conf" do
  source 'httpd.conf.erb'
  variables(
    :instance_name => instance_name,
    :port => port
  )
  owner 'root'
  group 'root'
  mode '0644'
  action :create
end

where

  • source gets the httpd.conf.erb template from this cookbook
  • variables assigns the instance_name and port properties to variables in the template

directory

Use the directory resource to create the /var/www/vhosts directory on the node:

directory "/var/www/vhosts/#{instance_name}" do
  recursive true
  owner 'root'
  group 'root'
  mode '0755'
  action :create
end

service

Use the service resource to enable, and then start the service:

service "httpd-#{instance_name}" do
  action [:enable, :start]
end

Create Templates

The /templates directory must contain two templates:

  • httpd.conf.erb to configure Apache httpd
  • httpd.service.erb to tell systemd how to start and stop the website

httpd.conf.erb

httpd.conf.erb stores information about the website and is typically located under the /etc/httpd:

ServerRoot "/etc/httpd"
Listen <%= @port %>
Include conf.modules.d/*.conf
User apache
Group apache
<Directory />
  AllowOverride none
  Require all denied
</Directory>
DocumentRoot "/var/www/vhosts/<%= @instance_name %>"
<IfModule mime_module>
  TypesConfig /etc/mime.types
</IfModule>

Copy it as shown, add it under /templates/default, and then name the file httpd.conf.erb.

Template Variables

The httpd.conf.erb template has two variables:

  • <%= @instance_name %>
  • <%= @port %>

They are:

  • Declared as properties of the custom resource
  • Defined as variables in a template resource block within the custom resource
  • Tunable from a recipe when using port and instance_name as properties in that recipe
  • instance_name defaults to the 'name' of the custom resource if not specified as a property

httpd.service.erb

httpd.service.erb tells systemd how to start and stop the website:

[Unit]
Description=The Apache HTTP Server - instance <%= @instance_name %>
After=network.target remote-fs.target nss-lookup.target

[Service]
Type=notify

ExecStart=/usr/sbin/httpd -f /etc/httpd/conf/httpd-<%= @instance_name %>.conf -DFOREGROUND
ExecReload=/usr/sbin/httpd -f /etc/httpd/conf/httpd-<%= @instance_name %>.conf -k graceful
ExecStop=/bin/kill -WINCH ${MAINPID}

KillSignal=SIGCONT
PrivateTmp=true

[Install]
WantedBy=multi-user.target

Copy it as shown, add it under /templates/default, and then name it httpd.service.erb.

Final Resource

property :instance_name, String, name_property: true
property :port, Fixnum, required: true

action :create do
  package 'httpd' do
    action :install
  end

  template "/lib/systemd/system/httpd-#{instance_name}.service" do
    source 'httpd.service.erb'
    variables(
      :instance_name => instance_name
    )
    owner 'root'
    group 'root'
    mode '0644'
    action :create
  end

  template "/etc/httpd/conf/httpd-#{instance_name}.conf" do
    source 'httpd.conf.erb'
    variables(
      :instance_name => instance_name,
      :port => port
    )
    owner 'root'
    group 'root'
    mode '0644'
    action :create
  end

  directory "/var/www/vhosts/#{instance_name}" do
    recursive true
    owner 'root'
    group 'root'
    mode '0755'
    action :create
  end

  service "httpd-#{instance_name}" do
    action [:enable, :start]
  end

end

Final Cookbook Directory

When finished adding the templates and building the custom resource, the cookbook directory structure should look like this:

/website
  metadata.rb
  /recipes
    default.rb
  README.md
  /resources
    httpd.rb
  /templates
    /default
      httpd.conf.erb
      httpd.service.erb

Recipe

The custom resource name is inferred from the name of the cookbook (website), the name of the recipe (httpd), and is separated by an underscore(_): website_httpd.

website_httpd 'httpd_site' do
  port 81
  action :create
end

which does the following:

  • Installs Apache httpd
  • Assigns an instance name of httpd_site that uses port 81
  • Configures httpd and systemd from a template
  • Creates the virtual host for the website
  • Starts the website using systemd

Advanced Options

The following sections describe advanced options that may be used (or may be required) when building custom resources.

default_action

The default action in a custom resource is, by default, the first action listed in the custom resource. For example, action aaaaa is the default resource:

property :name, RubyType, default: 'value'

...

action :aaaaa do
 # the first action listed in the custom resource
end

action :bbbbb do
 # the second action listed in the custom resource
end

The default_action method may also be used to specify the default action. For example:

property :name, RubyType, default: 'value'

default_action :aaaaa

action :aaaaa do
 # the first action listed in the custom resource
end

action :bbbbb do
 # the second action listed in the custom resource
end

defines action aaaaa as the default action. If default_action :bbbbb is specified, then action bbbbb is the default action. Use this method for clarity in custom resources, if deliberately stating the default resource is desired, or to specify a default action that is not listed first in the custom resource.

Override Properties

Custom resources are designed to use core resources that are built into Chef. In some cases, it may be necessary to specify a property in the custom resource that is the same as a property in a core resource, for the purpose of overriding that property when used with the custom resource. For example:

resource_name :node_execute

property :command, kind_of: String, name_property: true
property :version, kind_of: String

# Useful properties from the `execute` resource
property :cwd, kind_of: String
property :environment, kind_of: Hash, default: {}
property :user, kind_of: [String, Integer]
property :sensitive, kind_of: [TrueClass, FalseClass], default: false

prefix = '/opt/languages/node'

load_current_value do
  current_value_does_not_exist! if node.run_state['nodejs'].nil?
  version node.run_state['nodejs'][:version]
end

action :run do
  execute 'execute-node' do
    cwd cwd
    environment environment
    user user
    sensitive sensitive
    # gsub replaces 10+ spaces at the beginning of the line with nothing
    command <<-CODE.gsub(/^ {10}/, '')
      #{prefix}/#{version}/#{command}
    CODE
  end
end

where the property :cwd, property :environment, property :user, and property :sensitive are identical to properties in the execute resource, embedded as part of the action :run action. Because both the custom properties and the execute properties are identical, this will result in an error message similar to:

ArgumentError
-------------
wrong number of arguments (0 for 1)

To prevent this behavior, use new_resource. to tell the chef-client to process the properties from the core resource instead of the properties in the custom resource. For example:

resource_name :node_execute

property :command, kind_of: String, name_property: true
property :version, kind_of: String

# Useful properties from the `execute` resource
property :cwd, kind_of: String
property :environment, kind_of: Hash, default: {}
property :user, kind_of: [String, Integer]
property :sensitive, kind_of: [TrueClass, FalseClass], default: false

prefix = '/opt/languages/node'

load_current_value do
  current_value_does_not_exist! if node.run_state['nodejs'].nil?
  version node.run_state['nodejs'][:version]
end

action :run do
  execute 'execute-node' do
    cwd new_resource.cwd
    environment new_resource.environment
    user new_resource.user
    sensitive new_resource.sensitive
    # gsub replaces 10+ spaces at the beginning of the line with nothing
    command <<-CODE.gsub(/^ {10}/, '')
      #{prefix}/#{new_resource.version}/#{new_resource.command}
    CODE
  end
end

where cwd new_resource.cwd, environment new_resource.environment, user new_resource.user, and sensitive new_resource.sensitive correctly use the properties of the execute resource and not the identically-named override properties of the custom resource.