Engineering

Writing composable community Chef cookbooks: learning the new cookbook DSL

Jacobo García López de Araujo's profile picture Jacobo García López de Araujo on

In the first post of this series, we showed the flaws of the old attribute-based way of developing cookbooks and showed an example of the benefits that the newer resource-based pattern brings. In this cookbook we'll explain why those resources are so powerful and how to write resource-based open source community cookbooks using our PowerDNS cookbook.

Chef 12.5+ custom resources are designed to be easier to be written compared with the old LWRPs; they provide a comprehensive, simple, clean, DSL that allows operators without a deep knowledge of Ruby to effectively extend Chef using clear and meaningful syntax blocks.

Elements of a custom resource

A custom resource is usually defined in one or more resource files. There are the main elements of a custom resource: provides, properties, and actions. Let's start talking about all of them.

The most accepted pattern is to separate the different functional units of the cookbook in different resources, this allows to provide maximum flexibility to the cookbook users for specific needs. This is better illustrated with an example, in our case we extracted from the PowerDNS cookbook. We are just going to look at authoritative related files located in the resources folder, there are four different classes of resources for the authoritative server:

  • pdns_authoritative_install: only takes care of installing the server.
  • pdns_authoritative_service: only takes care of defining the service.
  • pdns_authoritative_config: this only creates configuration instances.
  • pdns_authoritative_backend: only takes care of installing the different backends.

If a user just needs to use two of the four available providers because it needs a special configuration, and our config resource is not flexible enough for them, they can do it in comparison with the old monolithic recipe-based way.

PowerDNS is a bit different than a regular service such as Apache, it usually requires one backend to store all the DNS data. There are many backends available, MySQL and PostgreSQL being the most used, these backends are usually available as additional packages—this is the reason that we are creating a dedicated backend for it.

Provides

The provides method allows users to specify which platforms, operating systems, and the versions the resource runs to. They also define the name of the resource itself. You usually define the same resource for different platforms in different files under the resources directory in a cookbook, this allows easy extensibility: if a contributor wants to add a new platform this can be defined in a new file that contains all the specific code for that plaform.

Let's take a look at the install resource for Debian:

resource_name :pdns_authoritative_install_debian

provides :pdns_authoritative_install, platform: 'ubuntu' do |node|
  node['platform_version'].to_f >= 14.04
end

provides :pdns_authoritative_install, platform: 'debian' do |node|
  node['platform_version'].to_i >= 8
end

The first line declares the resource name. The next two blocks are provides blocks that define which platforms are supported, in this case we support anything on Ubuntu above 14.04 and anything above Debian 8. Both provides blocks have exactly the same argument, pdns_authoritative_install. What this means is that the user have doesn't have to think about which platforms they are developing for as long as it is supported. All they have to do is call the resource name.

Now we'll look at the install resource for RHEL:

resource_name :pdns_authoritative_install_rhel

provides :pdns_authoritative_install, platform: 'centos' do |node|
  node['platform_version'].to_i >= 6
end

As you can see we are using the same argument for the block, again, the user does not need to worry about the underlying platform details to install the software. Just call the resource and Chef will do the rest.

Properties

We can define customized properties for our resources. These properties behave exactly the same as regular resources. For every property we need to determine their name, their type, and a default value. I have extracted a few properties from the Debian config resource:

property :instance_name, String, name_property: true

This property provides an identifier for the resource to be used by Chef and will probably be used as part of the resource in different situations. This is a special property which is defined by the name_property value.

property :launch, [Array,nil], default: ['bind']

We can define more than one type for a resource, so in this case we allow Array and nil as types. The default value is an array with the String 'bind'. This property defines the backend used by PowerDNS which by default is the Bind backend.

property :socket_dir, String, default: lazy { |resource| "/var/run/#{resource.instance_name}" }

Using lazy evaluation is allowed so we can define properties based on others. In this case we define the filename of the socket using the instance_name above. We use the keyword resource to call the resource itself.

Actions

Actions are equal to the regular resource's actions. The user will be able to call an action from inside of the resource block. At least one action must be defined, and they are defined with an action block. Actions usually combine Chef resources, Ruby, and the properties defined in the resource to achieve the desired state.

This is the :install action of the PowerDNS (Debian) install resource. Inside the action block there are a few default chef resources that conform the whole action itself.

action :install do
  apt_repository 'powerdns-authoritative' do
    uri "http://repo.powerdns.com/#{node['platform']}"
    distribution "#{node['lsb']['codename']}-auth-40"
    arch 'amd64'
    components ['main']
    key 'https://repo.powerdns.com/FD380FBB-pub.asc'
  end

  apt_preference 'pdns-*' do
    pin          'origin repo.powerdns.com'
    pin_priority '600'
  end

  apt_package 'pdns-server' do
    action :install
    version new_resource.version
  end

  apt_package 'pdns-server-dbg' do
    action :install
    only_if { new_resource.debug }
  end
end

Summing things up

  • Using the custom resources DSL is the official recommended approach in order to write custom resources.
  • Provides are very powerful because they allows extensibility with ease, always allow your user to override your decision and provide sane defaults.
  • Defining different resources divided per functionality allow your users to use just what they need.
  • Actions are key elements for providing flexibility and control to the cookbook users.

At DNSimple we love to share what we have learned in our quest for Domain Management automation.

Share on Twitter and Facebook

Jacobo García López de Araujo's profile picture

Jacobo García López de Araujo

Devops, infrastructure, urban cyclist, music nerd.

We think domain management should be easy.
That's why we continue building DNSimple.

Try us free for 30 days
4.5 stars

4.3 out of 5 stars.

Based on Trustpilot.com and G2.com reviews.