Chef Patterns: Part 1 - Modeling Environment-Specific Differences
At work we have the notion of multiple
environments. Dev (used by the developers), Staging (used for showcasing
stories), QA/Testing (used by testers) and Production (caters to end
user). At times, a pre-production environment is also present to simulate
production like scenarios.
Though we fight hard to minimize the
differences between these environments, they do exist and it is
necessary to measure them, which in turn can provide us with some automated
mechanisms to easily spot environment-specific differences. In this
write up I'll be writing some of the techniques I'm using to model them.
There are two generic goals for this techniques:
- Testability of the recipe.
- Ease with which one can spot the environment-specific differences
The techniques:
- Conditionals: Use
raw if else conditions to apply different resources in different
environments. If you are sure that the difference will only be limited
to certain minute details I will start employing raw conditional statements
like this:
if node.chef_environment == 'development' do # # do not configure basic auth # else # # configure basic auth # end
What is important is that we should also write equivalent unit tests for such scenarios. - Attributes: Use attributes to model the differences in environments. In Chef, attributes can be overridden on per environment basis. This
provides an elegant way to implement environment-specific changes. In
the following example, the version of Ruby needs to be different in two
environments.
# the default attribute looks like default[:ruby][:version] = '1.8.7' # # the resource inside ruby recipe might use it like this # ruby_version= 'ruby-'+ node[:ruby][:version] execute "download_ruby" do cwd "/opt" command "wget -c http://someurl/#{ruby_version}.tgz" end # #Then this can be overridden in all servers within the staging environment with this snippet
name "staging" override_attributes("ruby"=>{"version"=>'1.9.3'}) - Environment-specific Chef recipes/components: Use environment specific recipes and other components on the fly. If
the amount of differences is large enough, involving multiple resources
and significant configuration differences within a single file, you can
use the environment name itself to dynamically load an environment-specific template or environment-specific sub recipe. For example the
deploy recipe invokes database backup as well as load balancer rotation
(the instance should be out of the load balancer during the deployment)
tasks that are only in production and not in any other environments. For this we can
have a dedicated recipe name deploy::production. On the other hand the
main deploy recipe uses something like this
pre_deployment_sub_recipe = "deploy::#{node.chef_environment}_pre_deployment" include_recipe sub_recipe # # Perform deployment # post_deployment_sub_recipe = "deploy::#{node.chef_environment}_post_deployment"You can use similar strategies to include environment-specific configurations as well. Here's an example:template "/etc/nginx/nginx.conf" do source "nginx." + node.chef_environment + "conf.erb" end
This one will pick up nginx.staging.conf.erb as a template in the staging environment's nginx configuration while nginx.production.conf.erb is in production environments. This does impose a problem because now every environment should have a corresponding sub-recipe or template. But again you can further scope this using one of the earlier two techniques. In this case, it is possible to do a file glob and pick up the environment-specific differences.
(Note: Opinions expressed in this article and its replies are the opinions of their respective authors and not those of DZone, Inc.)





