Templates, attribute file for Lab, Staging, Prod environments

Disclaimer, Im pretty new to chef.
While I'm at it I'll also apologize for my abuse of functions to make this more readable.

Im going to use part of my elasticsearch config as my example.

Currently I have working production cookbook that I want to be able to add devlab and staging variables into the attributes and template files so I can use the same recipe,template,ect.. to deploy to these other environments as well.

I want to add variables for devlab in the attributes file like:

Prod
node.default["ES_HEAP_SIZE"] = "30g"

DevLab
node.default[:dev_default] ||= {}
default.dev_default["ES_HEAP_SIZE"] = "4g"

This would give me the ability to tweak things affected by server size between the environments like memory use.

So in my elasticsearch.sh template I have:

ES_HEAP_SIZE=<%= node.default["ES_HEAP_SIZE"] %`>

And it works great. But i would like to have:

Prod
ES_HEAP_SIZE=<%= node.default["ES_HEAP_SIZE"] %>

DevLab
ES_HEAP_SIZE=<%= default.dev_default["ES_HEAP_SIZE"] %>

My question is how do I configure the recipe, attributes file or template to know when I run the cookbook on server-dev.mydomain.com to use the dev variables instead of the Prod default variables.

This seems like the textbook use case for Chef environments, no?
Then you wouldn’t need to do this messing around with your defaults inside your cookbook.
You can set a sane default in your cookbook e.g. default['elasticsearch']['heap_size'] = '2G' and override it later by assigning a dev environment to your dev nodes, and a prod environment to your production nodes.

Your dev environment might look like this:

name "dev"
description "The dev environment"
cookbook_versions({
  "mycookbook" => "~> 1.0"
})
default_attributes({
  "elasticsearch" => {
    "heap_size" => "4G"
  }
})

While prod might look essentially the same:

name "prod"
description "The prod environment"
cookbook_versions({
  "mycookbook" => "~> 0.9"
})
default_attributes({
  "elasticsearch" => {
    "heap_size" => "30G"
  }
})

In both cases, that will override the default of 2G you set in your attributes/default.rb because environment data has priority over cookbook defaults. On that topic, you should read up on attribute precedence.

Also, the example bits above also showcase a feature of environments where you can pin a cookbook to a specific version in a given environment, so that your changes to that cookbook in one environment don’t touch nodes belonging to a different environment. This is off-topic but is good to know.

Lastly, some people in the Chef community advocate for using cookbooks to control this stuff, because cookbooks are more flexible and fully versioned. You can do all of the above with a wrapper cookbook.

Also, if you’re using Elasticsearch, is there a reason for you not to use Elastic’s official Elasticsearch cookbook? It’s a bit scary but very powerful.

To answer your last question first, I ended up creating my own cookbook for elasticsearch because I didn’t understand the official elastic cookbook at the time, I also had to consider how easily others after me would be able to make use of the cookbook. I am working on a large-ish graylog logging system and their cookbook made complete sense to me, its very readable to anyone familiar with their product so I made an elastic cookbook using the same methods the graylog team used.

Ill read over the other things you mentioned, I like the idea I presented of having all of the values in one location, the attribute file, but Ill give your suggestions a shot.

Thank you for the responce

If you’d rather keep it all within the cookbook, another way to do it might be based on your node’s FQDN. I don’t really recommend it but it should work. You could do a regex against node['fqdn'], e.g.:

case node['fqdn']
when /dev/
  default['elasticsearch']['heap_size'] = '2G'
when /prod/
  default['elasticsearch']['heap_size'] = '30G'
end

(That would go in your attributes/default.rb.)

I’ve done some reading about environments, roles and attributes and see I may be doing some things that might not be best practice. I don’t like the idea of using environments, it seems like attributes set this via environments are hidden and could get easily missed/forgotten.

I may use your suggestion and use node[‘fqdn’], our dev is all product.dev.domain.com so it should be fairly reliable. I’ll do some more research before making a choice, I can see where this could be problematic.

Thank you for the advice

Mike

So the thing is that you will ideally want to be setting cookbook version pins for production nodes so that your latest cookbook changes that have a bug won’t possibly take down everything. Having discrete dev, qa/stage, and prod environments allows you to validate your cookbook changes before applying them to production nodes. You may not want to use environment-based attributes, but don’t disregard environments totally.

Nathan Clemons

DevOps Engineer

Moxie Cloud Services (MCS)

O +1.425.467.5075

M +1.360.861.6291

E nclemons@gomoxie.com

W www.gomoxie.comhttp://www.gomoxie.com/

Again, alternatively (and this is what I was talking about with environment/wrapper cookbooks), you can design a generic, endlessly reusable ES cookbook called elasticsearch with sane defaults for everything.
Then, create new cookbooks, you can call them elasticsearch-dev and elasticsearch-prod.
In the attributes/default.rb, copy-paste some lines from your original elasticsearch cookbook’s attributes file(s) but override the default values.
In the recipes/default.rb, simply have include_recipe 'elasticsearch'. Then add this cookbook to your node’s run list instead of the original elasticsearch one.

You may like to read this: http://blog.vialstudios.com/the-environment-cookbook-pattern/

I have done something similar for “base” server cookbooks. I’ll warn you, my approach means understanding how ruby handles hashes, and somewhat “opinionated” on where attributes should live. I do agree with having a “worker” cookbook with sane defaults, as others have said, and a wrapper for non-standard items. My approach just cuts down on the number of potential wrappers.

So basically to set up the situation you are talking about:
_default env gets a es_heap_size of 30g, which will be used unless your chef_environment is dev. If it is dev, you will get 4g instead.

So basically:


##Your wrapper cookbook, attributes/default.rb:

#Save some typing later:
my_env = node.chef_environment

##Set up our default items
attr_hash = {'_default' => {} } # initialize a hash, note it is not a node attribute
attr_hash['_default']['es_heap_size'] = '30g'
# Other items here, same format, for example:
attr_hash['_default']['ntp']['servers'] = [ '0.pool.ntp.org', '1.pool.ntp.org']

## Now, clone out your defaults to your other environments
%w(dev prod other_env1 other_env2).each do |cur_env|
  ## This bit is a little bit of a hack,
  ## but it is the only way I've found to clone a hash with no dependency
  attr_hash[cur_env] = eval(attr_hash['_default']).to_s)
end

### Now we add items that differ, such as:
attr_hash['dev']['es_heap_size'] = '4g'

### After we assign out the differing items, we assign them to attributes
## First a failsafe:
if !(attr_hash.has_key?my_env) ## Nothing for this chef_env
  my_env = '_default' ## so fall back to default 
end
# walk the hash and assign a top level item to a normal node attribute
attr_hash[my_env].each do |key, value|
  normal[key] = value
end

This is complex, but it should do what you are asking for.

To explain some of my choices:

  • I use normal level attributes to avoid conflicting with defaults in cookbooks. They can still be overridden with override variables, and have (in my opinion) the benefit of being saved with the node.

  • In most cases, yes, eval is bad. Here you control the input, and using other methods to clone hashes ( a =b, b = a.clone) doesn’t break dependency for multilevel hash.
    for example:


irb(main):023:0> a={'foo' => { 'bar' => 1 } }
=> {"foo"=>{"bar"=>1}}
irb(main):024:0> b=a.clone
=> {"foo"=>{"bar"=>1}}
irb(main):025:0> b
=> {"foo"=>{"bar"=>1}}
irb(main):026:0> b['foo']['bar'] = 2
=> 2
irb(main):027:0> a
=> {"foo"=>{"bar"=>2}}
irb(main):028:0> 

b = eval(a.to_s) clones as deep as you like with no link.

Hope this helps,
Jp