8 min read

(For more resources related to this topic, see here.)

Chef’s declarative language

Chef recipes are declarative, which means that it provides a high-level language for describing what to do to accomplish the task at hand without requiring that you provide a specific implementation or procedure. This means that you can focus on building recipes and modeling infrastructure using abstract resources so that it is clear what is happening without having to know how it is happening. Take, as an example, a portion of the recipes we looked at earlier for deploying an IIS application that is responsible for installing some Windows features:

features = %w{IIS-ISAPIFilter IIS-ISAPIExtensions NetFx3ServerFeatures NetFx4Extended-ASPNET45 IIS-NetFxExtensibility45} features.each do |f| windows_feature f do action :install end end

Because of Chef’s declarative language, the preceding section of code reads in a natural way. We have a list of features. For each of those features, which we know to be Windows features, install them.

Because of this high-level abstraction, your recipe can describe what is going on without containing all of the logic necessary to do the actual work. If you were to look into the windows cookbook, you would see that there are a number of implementations using DISM, PowerShell, and ServerManagerCmd. Rather than worrying about that in the recipe itself, the logic is deferred to the provider that is selected for the given resource. The feature resource knows that if a host has DISM, it will use the DISM provider; otherwise, it will look for the existence of servermanagercmd.exe and, if it is present, use that as the installation provider. This makes recipes more expressive and much less cluttered.

If Chef did not provide this high-level abstraction, your recipe would look more like the following code snippet:

features = %w{IIS-ISAPIFilter IIS-ISAPIExtensions NetFx3ServerFeatures NetFx4Extended-ASPNET45 IIS-NetFxExtensibility45} features.each do |f| if ::File.exists?(locate_cmd('dism.exe')) install_via_dism(f) elsif ::File.exists?(locate_ cmd('servermanagercmd.exe')) install_via_servermgrcmd(f) else fail end end def install_via_dism(feature_name) ## some code here to execute DISM end def install_via_servermgrcmd(feature_name) ## some code here to execute servermgrcmd.exe end

This, while understandable, significantly increases the overall complexity of your recipes and reduces readability. Now, rather than simply focusing on installing the features, the recipe contains a lot of logic about how to perform the installation. Now, imagine writing a recipe that needs to create files and set ownership on those files and be usable across multiple platforms. Without abstractions, the recipe would contain implementation details of how to determine if a platform is Windows or Linux, how to determine user or group IDs from a string representation, what file permissions look like on different platforms, and so on. However, with the level of abstraction that Chef provides, that recipe would look like the following code snippet:

file_names = %w{hello.txt goodbye.txt README.md myfile.txt} file_names.each do |file_name| file file_name action :create owner "someuser" mode 0660 end end

Behind the scenes, when the recipe is executed, the underlying providers know how to convert these declarations into system-level commands. Let’s take a look at how we could build a single recipe that is capable of installing something on both Windows and Linux.

Handling multiple platforms

One of Chef’s strengths is the ability to integrate Windows hosts with non-Windows hosts. It is important to not only develop recipes and cookbooks that are capable of supporting both Linux and Windows hosts, but also to be able to thoroughly test them before rolling them out into your infrastructure. Let’s take a look at how you can support multiple platforms in your recipes as well as use a popular testing framework, ChefSpec, to write tests to test your recipes and validate platformspecific behaviors.

Declaring support in your cookbook

All Chef cookbooks have a metadata.rb file; this file outlines dependencies, ownership, version data, and compatibility. Compatibility in a homogeneous environment is a less important property—all the hosts run the same operating system. When you are modeling a heterogeneous environment, the ability to describe compatibility is more important; without it, you might try to apply a Windows-only recipe to a Linux host or the other way around. In order to indicate the platforms that are supported by your cookbook, you will want to add one or more supports stanzas to the metadata.rb file. For example, a cookbook that supports Debian and Windows would have two supports statements as follows:

supports "windows" supports "debian"

However, if you were to support a lot of different platforms, you can always script your configuration. For example, you could use something similar to the following code snippet:

%w(windows debian ubuntu redhat fedora).each |os| supports os end

Multiplatform recipes

In the following code example, we will look at how we could install Apache, a popular web server, on both a Windows and a Debian system inside of a single recipe:

if platform_family? 'debian' package 'apache2' elsif platform_family? 'windows' windows_package node['apache']['windows']['service_name'] do source node['apache']['windows']['msi_url'] installer_type :msi # The last four options keep the service from failing # before the httpd.conf file is created options %W[ /quiet INSTALLDIR="#{node['apache']['install_dir']}" ALLUSERS=1 SERVERADMIN=#{node['apache']['serveradmin']} SERVERDOMAIN=#{node['fqdn']} SERVERNAME=#{node['fqdn']} ].join(' ') end end template node['apache']['config_file'] do source "httpd.conf.erb" action :create notifies :restart, "service[apache2]" end # The apache service service "apache2" do if platform_family? 'windows' service_name "Apache#{node['apache']['version']}" end action [ :enable, :start ] end

In this example, we perform a very basic installation of the Apache 2.x service on the host. There are no modules enabled, no virtual hosts, or anything else. However, it does allow us to define a recipe that will install Apache, generate an httpd.conf file, and then enable and start the Apache 2 service. You will notice that there is a little bit of platform-specific configuration going on here, first with how to install the package and second with how to enable the service.

Because the package resource does not support Windows, the installation of the package on Windows will use the windows_package resource and the package resource on a Debian host.

To make this work, we will need some configuration data to apply during installation; skimming over the recipe, we find that we would need a configuration hash similar to the following code snippet:

'config': { 'apache': { 'version': '2.2.48', 'config_file': '/opt/apache2/conf/httpd.conf', 'install_dir': '/opt/apache2', 'serveradmin': '[email protected]', 'windows': { 'service_name': 'Apache 2.x', 'msi_url': 'http://some.url/apache2.msi' } } }

This recipe allows our configuration to remain consistent across all nodes; we don’t need to override any configuration values to make this work on a Linux or Windows host. You may be saying to yourself “but, /opt/apache2 won’t work on Windows”. It will, but it is interpreted as optapache2 on the same drive as the Chef client’s current working directory; thus, if you ran Chef from C:, it would become c:optapache2. By making our configuration consistent, we do not need to construct any special roles to store our Windows configuration separate from our Linux configuration.

If you do not like installing Apache in the same directory on both Linux and Windows hosts, you could easily modify the recipe to have some conditional logic as follows:

apache_config_file = if platform_family? 'windows' node['apache']['windows']['config_file'] else node['apache']['linux']['config_file'] end template apache_config_file do source "httpd.conf.erb" action :create notifies :restart, "service[apache2]" end

Here, the recipe is made slightly more complicated in order to accommodate the two platforms, but at the benefit of one consistent cross-platform configuration specification.

Alternatively, you could use the recipe as it is defined and create two roles, one for Windows Apache servers and the other for Linux Apache servers, each with their own configuration. An apache_windows role may have the following override configuration:

'config': { 'apache': { 'config_file': "C:\Apps\Apache2\Config\httpd.conf", 'install_dir': "C:\Apps\Apache2" } }

In contrast, an apache_linux role might have a configuration that looks like the following code snippet:

'config': { 'apache': { 'config_file': "/usr/local/apache2/conf/httpd.conf", 'install_dir': "/usr/local/apache2" } }

The impact of this approach is that now you have to maintain separate platformspecific roles. When a host is provisioned or being configured (either through the control panel or via knife), you need to remember that it is a Windows host and therefore has a specific Chef role associated with it. This leads to potential mistakes in host configuration as a result of increased complexity.

Summary

In this article, we have learned about the declarative language of Chef, we understood the various ways to handle multiple platforms, and also learned about the multiplatform recipes.

Resources for Article:


Further resources on this subject:


LEAVE A REPLY

Please enter your comment!
Please enter your name here