Custom Resources
A custom resource:
- Is a simple extension of Chef Infra Client that adds your own resources
- Is implemented and shipped as part of a cookbook
- Follows easy, repeatable syntax patterns
- Effectively leverages resources that are built into Chef Infra Client and/or custom Ruby code
- Is reusable in the same way as resources that are built into Chef Infra Client
For example, Chef Infra Client 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 state of properties, if the resource already exists
- Defines each action the custom resource may take
The syntax for a custom resource is. For example:
property :property_name, RubyType, default: 'value'
action :action_name do
# a mix of built-in Chef resources and Ruby
end
action :another_action_name do
# a mix of built-in Chef resources and Ruby
end
where the first action listed is the default action.
Warning
property :property_name
in the following invalid syntax:
property :name, String, default: 'thename'
.Example
This example site
utilizes Chef’s built-in file
, service
and
package
resources, and includes :create
and :delete
actions. Since
it uses built-in Chef Infra Client resources, besides defining the
property and actions, the code is very similar to that of a recipe.
property :homepage, String, default: '<h1>Hello world!</h1>'
action :create do
package 'httpd'
service 'httpd' do
action [:enable, :start]
end
file '/var/www/html/index.html' do
content new_resource.homepage
end
end
action :delete do
package 'httpd' do
action :remove
end
end
where
homepage
is a property that sets the default HTML for theindex.html
file with a default value of'<h1>Hello world!</h1>'
- the
action
block uses the built-in collection of resources to tell Chef Infra 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, because it is listed first;action :delete
must be called specifically (because it is not the default action)
Once written, the custom resource may be used in a recipe just like any
of the resources that are built into Chef Infra Client. 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>'
end
and to delete the exampleco website, do the following:
exampleco_site 'httpd' do
action :delete
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:
- Defining a cookbook named
website
- Defining two properties
- Defining an action
- For the action, defining the steps to configure the system using resources that are built into Chef Infra
- Creating two templates that support the custom resource
- Adding the resource to a recipe
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 /ctl_chef.html for more information about how to use the chef
command-line tool that is packaged with Chef Workstation 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 Chef Infra Client what to do)
- For each action, use a collection of resources that are built into Chef Infra Client 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 Chef Infra Client how to apply
these variables.
In the custom resource, add the following custom properties:
property :instance_name, String, name_property: true
property :port, Integer, required: true
where
String
andInteger
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-#{new_resource.instance_name}.service" do
source 'httpd.service.erb'
variables(
instance_name: new_resource.instance_name
)
action :create
end
where
source
gets thehttpd.service.erb
template from this cookbookvariables
assigns theinstance_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-#{new_resource.instance_name}.conf" do
source 'httpd.conf.erb'
variables(
instance_name: new_resource.instance_name,
port: new_resource.port
)
action :create
end
where
source
gets thehttpd.conf.erb
template from this cookbookvariables
assigns theinstance_name
andport
properties to variables in the template
Note
cookbook 'website'
directory
Use the directory resource to create the /var/www/vhosts
directory
on the node:
directory "/var/www/vhosts/#{new_resource.instance_name}" do
recursive true
action :create
end
service
Use the service resource to enable, and then start the service:
service "httpd-#{new_resource.instance_name}" do
action [:enable, :start]
end
Create Templates
The /templates
directory must contain two templates:
httpd.conf.erb
to configure Apache httpdhttpd.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
, 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
andinstance_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
, and then name it
httpd.service.erb
.
Final Resource
property :instance_name, String, name_property: true
property :port, Integer, required: true
action :create do
package 'httpd' do
action :install
end
template "/lib/systemd/system/httpd-#{new_resource.instance_name}.service" do
source 'httpd.service.erb'
variables(
instance_name: new_resource.instance_name
)
action :create
end
template "/etc/httpd/conf/httpd-#{new_resource.instance_name}.conf" do
source 'httpd.conf.erb'
variables(
instance_name: new_resource.instance_name,
port: new_resource.port
)
action :create
end
directory "/var/www/vhosts/#{new_resource.instance_name}" do
recursive true
action :create
end
service "httpd-#{new_resource.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
httpd.conf.erb
httpd.service.erb
Recipe
The custom resource name is inferred from the name of the cookbook
(website
), the name of the resource file (httpd
), and is separated
by an underscore(_
): website_httpd
. The custom resource may be used
in a recipe.
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
Custom Resource DSL
The following sections describe additional Custom Resource DSL methods that were not used in the preceding scenario:
action_class
Use the action_class
block to make methods available to the actions in
the custom resource. Modules with helper methods created as files in the
cookbook library directory may be included. New action methods may also
be defined directly in the action_class
block. Code in the
action_class
block has access to the new_resource properties.
Assume a helper module has been created in the cookbook
libraries/helper.rb
file.
module Sample
module Helper
def helper_method
# code
end
end
end
Methods may be made available to the custom resource actions by using an
action_class
block.
property file, String
action :delete do
helper_method
FileUtils.rm(new_resource.file) if file_exist
end
action_class do
def file_exist
::File.exist?(new_resource.file)
end
require 'fileutils'
include Sample::Helper
end
converge_if_changed
Use the converge_if_changed
method inside an action
block in a
custom resource to compare the desired property values against the
current property values (as loaded by the load_current_value
method).
Use the converge_if_changed
method to ensure that updates only occur
when property values on the system are not the desired property values
and to otherwise prevent a resource from being converged.
To use the converge_if_changed
method, wrap it around the part of a
recipe or custom resource that should only be converged when the current
state is not the desired state:
action :some_action do
converge_if_changed do
# some property
end
end
For example, a custom resource defines two properties (content
and
path
) and a single action (:create
). Use the load_current_value
method to load the property value to be compared, and then use the
converge_if_changed
method to tell Chef Infra Client what to do if
that value is not the desired value:
property :content, String
property :path, String, name_property: true
load_current_value do
if ::File.exist?(path)
content IO.read(path)
end
end
action :create do
converge_if_changed do
IO.write(new_resource.path, new_resource.content)
end
end
When the file does not exist, the
IO.write(new_resource.path, new_resource.content)
code is executed and
the Chef Infra Client output will print something similar to:
Recipe: recipe_name::block
* resource_name[blah] action create
- update my_file[blah]
- set content to "hola mundo" (was "hello world")
Multiple Properties
The converge_if_changed
method may be used multiple times. The
following example shows how to use the converge_if_changed
method to
compare the multiple desired property values against the current
property values (as loaded by the load_current_value
method).
property :path, String
property :content, String
property :mode, String
load_current_value do |desired|
if ::File.exist?(desired.path)
content IO.read(desired.path)
mode ::File.stat(desired.path).mode
end
end
action :create do
converge_if_changed :content do
IO.write(new_resource.path, new_resource.content)
end
converge_if_changed :mode do
::File.chmod(new_resource.mode, new_resource.path)
end
end
where
load_current_value
loads the property values for bothcontent
andmode
- A
converge_if_changed
block tests onlycontent
- A
converge_if_changed
block tests onlymode
Chef Infra Client will only update the property values that require updates and will not make changes when the property values are already in the desired state
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 :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 :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.
load_current_value
Use the load_current_value
method to load the specified property
values from the node, and then use those values when the resource is
converged. This method may take a block argument.
property :path, String
property :content, String
property :mode, String
load_current_value do |new_resource|
if ::File.exist?(new_resource.path)
content IO.read(new_resource.path)
mode ::File.stat(new_resource.path).mode
end
end
Use the load_current_value
method to guard against property values
being replaced. For example:
property :homepage, String
property :page_not_found, String
load_current_value do
if ::File.exist?('/var/www/html/index.html')
homepage IO.read('/var/www/html/index.html')
end
if ::File.exist?('/var/www/html/404.html')
page_not_found IO.read('/var/www/html/404.html')
end
end
This ensures the values for homepage
and page_not_found
are not
changed to the default values when Chef Infra Client configures the
node.
new_resource.property
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:
property :command, String, name_property: true
property :version, String
# Useful properties from the `execute` resource
property :cwd, String
property :environment, Hash, default: {}
property :user, [String, Integer]
property :sensitive, [true, false], 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}/#{new_resource.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 Chef Infra Client
to process the properties from the core resource instead of the
properties in the custom resource. For example:
property :command, String, name_property: true
property :version, String
# Useful properties from the `execute` resource
property :cwd, String
property :environment, Hash, default: {}
property :user, [String, Integer]
property :sensitive, [true, false], 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.
property
Use the property
method to define properties for the custom resource.
The syntax is:
property :property_name, ruby_type, default: 'value', parameter: 'value'
where
:property_name
is the name of the propertyruby_type
is the optional Ruby type or array of types, such asString
,Integer
,true
, orfalse
default: 'value'
is the optional default value loaded into the resourceparameter: 'value'
optional parameters
For example, the following properties define username
and password
properties with no default values specified:
property :username, String
property :password, String
ruby_type
The property ruby_type is a positional parameter. Use to ensure a
property value is of a particular ruby class, such as true
, false
,
nil
, String
, Array
, Hash
, Integer
, Symbol
. Use an array of
ruby classes to allow a value to be of more than one type. For example:
property :aaaa, String
property :bbbb, Integer
property :cccc, Hash
property :dddd, [true, false]
property :eeee, [String, nil]
property :ffff, [Class, String, Symbol]
property :gggg, [Array, Hash]
sensitive
A property can be marked sensitive by specifying sensitive: true
on
the property. This prevents the contents of the property from being
exported to data collection and sent to an Automate server.
Note: This feature was introduced in Chef Client 12.14.
validators
A validation parameter is used to add zero (or more) validation parameters to a property.
Parameter | Description |
---|---|
| Use to define a collection of unique keys and values (a ruby hash) for which the key is the error message and the value is a lambda to validate the parameter. For example: |
| Use to specify the default value for a property. For example: |
| Use to match a value with |
| Use to match a value to a regular expression. For example: |
| Indicates that a property is required. For example: |
| Use to ensure that a value has a given method. This can be a single method name or an array of method names. For example: |
Some examples of combining validation parameters:
property :spool_name, String, regex: /$\w+/
property :enabled, equal_to: [true, false, 'true', 'false'], default: true
desired_state
Add desired_state:
to set the desired state property for a resource.
This value may be true
or false
, and all properties default to true.
- When
true
, the state of the property is determined by the state of the system - When
false
, the value of the property impacts how the resource executes, but it is not determined by the state of the system.
For example, if you were to write a resource to create volumes on a
cloud provider you would need define properties such as volume_name
,
volume_size
, and volume_region
. The state of these properties would
determine if your resource needed to converge or not. For the resource
to function you would also need to define properties such as
cloud_login
and cloud_password
. These are necessary properties for
interacting with the cloud provider, but their state has no impact on
decision to converge the resource or not, so you would set
desired_state
to false
for these properties.
property :volume_name, String
property :volume_size, Integer
property :volume_region, String
property :cloud_login, String, desired_state: false
property :cloud_password, String, desired_state: false
identity
Add identity:
to set a resource to a particular set of properties.
This value may be true
or false
.
- When
true
, data for that property is returned as part of the resource data set and may be available to external applications, such as reporting - When
false
, no data for that property is returned.
If no properties are marked true
, the property that defaults to the
name
of the resource is marked true
.
For example, the following properties define username
and password
properties with no default values specified, but with identity
set to
true
for the user name:
property :username, String, identity: true
property :password, String
Block Arguments
Any properties that are marked identity: true
, desired_state: false
,
or name_property: true
will be directly available from
load_current_value
. If access to other properties of a resource is
needed, use a block argument with load_current_value. The block
argument will have the values of the requested resource. For example:
// Property is directly available example
property :action, String, name_property: true
property :content, String
load_current_value do |desired|
puts "The user requested action = #{action} in the resource"
puts "The user typed content = #{desired.content} in the resource"
end
// Block argument example
property :action, String
property :content, String
load_current_value do |desired|
puts "The user requested action = #{desired.action} in the resource"
puts "The user typed content = #{desired.content} in the resource"
end
property_is_set?
Use the property_is_set?
method to check if the value for a property
is set. The syntax is:
property_is_set?(:property_name)
The property_is_set?
method will return true
if the property is set.
For example, the following custom resource creates and/or updates user
properties, but not their password. The property_is_set?
method checks
if the user has specified a password and then tells Chef Infra Client
what to do if the password is not identical:
action :create do
converge_if_changed do
shell_out!("rabbitmqctl create_or_update_user #{username} --prop1 #{prop1} ... ")
end
if property_is_set?(:password)
if shell_out("rabbitmqctl authenticate_user #{username} #{password}").error?
converge_by "Updating password for user #{username} ..." do
shell_out!("rabbitmqctl update_user #{username} --password #{password}")
end
end
end
end
provides
Use the provides
method to associate a custom resource with the Recipe
DSL on different operating systems. When multiple custom resources use
the same DSL, specificity rules are applied to determine the priority,
from highest to lowest:
- provides :my_custom_resource, platform_version: ‘0.1.2’
- provides :my_custom_resource, platform: ‘platform_name’
- provides :my_custom_resource, platform_family: ‘platform_family’
- provides :my_custom_resource, os: ‘operating_system’
- provides :my_custom_resource
For example:
provides :my_custom_resource, platform: 'redhat' do |node|
node['platform_version'].to_i >= 7
end
provides :my_custom_resource, platform: 'redhat'
provides :my_custom_resource, platform_family: 'rhel'
provides :my_custom_resource, os: 'linux'
provides :my_custom_resource
This allows you to use multiple custom resources files that provide the same resource to the user, but for different operating systems or operation system versions. With this you can eliminate the need for platform or platform version logic within your resources.
reset_property
Use the reset_property
method to clear the value for a property as if
it had never been set, and then use the default value. For example, to
clear the value for a property named password
:
reset_property(:password)
coerce
coerce
is used to transform user input into a canonical form. The
value is passed in, and the transformed value returned as output. Lazy
values will not be passed to this method until after they are
evaluated.
coerce
is run in the context of the instance, which gives it access to
other properties.
property :mode, coerce: proc { |m| m.is_a?(String) ? m.to_s(8) : m }