Changing Chef Resources at Runtime

Chef, Hacks Posted on

As the cookbook wrapper pattern becomes more prevalent, you may find it necessary to alter a parameter of a Chef resource in the library cookbook. For example, you may need to update the cookbook from where a file should be found, execute an action before a service starts, or change the variables passed to a template.

If you care not to dig into the Chef internals, I highly recommend Bryan Berry's Chef Rewind. But if you want to learn a little bit about resources, let's jump in.

Consider the library cookbook "bacon", which drops a template in /tmp:

template '/tmp/bacon' do
  owner 'root'
  group 'root'
  variables(
    :ham  => true,
    :pork => false
  )
end

And the bacon cookbook is a community cookbook that works out of the box 99.5293% of the time... But there's that 0.4707% chance that you need to modify the template resource in your wrapper cookbook, because every infrastructure is a special snowflake. You want to use the template in your wrapper cookbook, instead of the one packaged in the library cookbook.

So you have this cool wrapper cookbook called "company_bacon" that depends on the bacon cookbook as recommended by the wrapper cookbook pattern. In your "company_bacon" recipe, you have access to the template resource; you can change its attributes during the compilation phase:

include_recipe 'bacon::default'

r = resources(template: '/tmp/bacon')
r.cookbook('company_bacon')
r.source('some_other_name.txt.erb')
r.variables(
  :ham  => false,
  :pork => false
)

The resources method looks up a resource by it's name in the resource collection. For templates the "identity" attribute is the path to the file on disk. This varies between resources. For packages, it's the name of the package to install. This method returns an instance of the resource in the collection. We can save this result to a variable and modify its properties.

In the recipe DSL, the "stuff" between a do and end are actually methods that get called on the resource. So:

template '/tmp/bacon' do
  source 'template.erb'
end

is equivalent to:

t = Chef::Resource::Template.new('/tmp/bacon', run_context)
t.source('template.erb')

It's worth noting that the resources method will raise an exception if the resource does not exist. If you can't guarantee the resource will exist in the collection, you should wrap the clause with error handling:

begin
  r = resources(template: '/tmp/bacon')
rescue Chef::Exceptions::ResourceNotFound
  # Some error handling
end

As I said before, this is a nice exploration of the Chef internals. If you do need to adopt this pattern, please look at chef-rewind.

About Seth

Seth Vargo is an engineer at Google. Previously he worked at HashiCorp, Chef Software, CustomInk, and some Pittsburgh-based startups. He is the author of Learning Chef and is passionate about reducing inequality in technology. When he is not writing, working on open source, teaching, or speaking at conferences, Seth advises non-profits.