what makes a good cookbook?
DESCRIPTION
What makes a good Chef cookbook?TRANSCRIPT
What Makes a Good Cookbook?Julian C. DunnSenior ConsultantChef Software, Inc.<[email protected]>
Finding a Good Cookbook
Do judge a cookbook by its cover
missing_attrs = %w{ postgres }.select do |attr| node['postgresql']['password'][attr].nil? end.map { |attr| "node['postgresql']['password']['#{attr}']" }
if !missing_attrs.empty? Chef::Application.fatal!([ "You must set #{missing_attrs.join(', ')} in chef-solo mode.", "For more information, see https://github.com/opscode-cookbooks/postgresql#chef-solo-note" ].join(' '))end
Too Clever for Its Own Good
Chef::Application.fatal!([ "You must set #{missing_attrs.join(', ')} in chef-solo mode.", "For more information, see https://github.com/opscode-cookbooks/postgresql#chef-solo-note" ].join(' '))
Poking at Chef Internals
• Other abuses: Messing with run_context and run_state
template "/etc/whatever.conf" do ... not_if { foo }end
Compile vs. Execute Errors
if foo template "/etc/whatever.conf" do ... endend
not the same thing as
execute “yum install httpd” do not_if “rpm -qa | grep -x httpd”end
Not declarative
• Also, the Chef recipe with 100 bash resource declarations
execute "I will run every time" do action :run # because I don't have a guard hereend
Missing guards
Fear of LWRPs• Missed abstraction opportunities
• No good example to put here; they’re all 200 lines long (thus proving my point)
remote_file 'whatever.tar.gz' do source 'http://hardcoded.url.com/’end
Hardcoded Strings
Excess Conditions & Recipe Length• https://github.com/opscode-cookbooks/mysql/blob/v3.0.12/recipes/server.rb
• (We’ve since refactored this)
Good Cookbooks...
Put control flow in attributes• Especially for cross-platform cookbooks
• Set common set of attributes, write common behavior in recipe context
case node['platform']
when "debian", "ubuntu" default['postgresql']['client']['packages'] = %w{postgresql-client libpq-dev} default['postgresql']['server']['packages'] = %w{postgresql} default['postgresql']['contrib']['packages'] = %w{postgresql-contrib}
when "fedora", "amazon" default['postgresql']['client']['packages'] = %w{postgresql-devel} default['postgresql']['server']['packages'] = %w{postgresql-server} default['postgresql']['contrib']['packages'] = %w{postgresql-contrib} default['postgresql']['server']['service_name'] = "postgresql"
when "redhat", "centos", "scientific", "oracle" default['postgresql']['version'] = "8.4" default['postgresql']['dir'] = "/var/lib/pgsql/data"
if node['platform_version'].to_f >= 6.0 default['postgresql']['client']['packages'] = %w{postgresql-devel} default['postgresql']['server']['packages'] = %w{postgresql-server} default['postgresql']['contrib']['packages'] = %w{postgresql-contrib} else default['postgresql']['client']['packages'] = ["postgresql#{node['postgresql']['version'].split('.').join}-devel"] default['postgresql']['server']['packages'] = ["postgresql#{node['postgresql']['version'].split('.').join}-server"] default['postgresql']['contrib']['packages'] = ["postgresql#{node['postgresql']['version'].split('.').join}-contrib"] end default['postgresql']['server']['service_name'] = "postgresql"
end
Control Flow in Attributes
node['postgresql']['server']['packages'].each do |pg_pack|
package pg_pack
end
Common Recipe Code
• Easy to support more platforms without modifying recipe
default.rbdefault.rbdefault.rbdefault.rb
server.rbserver.rbserver.rbserver.rb
_suse.rb_suse.rb_suse.rb_suse.rb_fedora.rb_fedora.rb_fedora.rb_fedora.rb_windows.rb_windows.rb_windows.rb_windows.rb
Separate recipes by OS• If things you do are very different per platform, separate them into different recipes
“Public” versus “Private” recipes• ‘_’ faux-namespacing
loaded_recipes = if run_context.respond_to?(:loaded_recipes) run_context.loaded_recipes else node.run_state[:seen_recipes] end
node['mysql']['client']['packages'].each do |name| resources("package[#{name}]").run_action(:install)end
Do not abuse compile-time
•.run_action(:must_die)
• Use sparingly, if at all!
if some_error_condition fail "Helpful error message" # rather than Chef::Application.fatal!("error")end
Avoid poking Chef Internals
•Chef::Application.fatal is for use by Chef itself
•fail or raise is better
Attributes only where necessary• “Let’s create a node attribute for each of the 15,000 tunables in this daemon”
• Not necessary if you never touch 14,975 of those knobs
git clone git://github.com/foozolix/foozolix.git./configuremakemake install
Give people options for installation
• Sigh.
• At least give people a way to install from packages.
• “Compile from source” should be banned in most cases.
Be declarative• Know and use built-in Chef resources
• Know where to find LWRPs to avoid batch/execute/powershell_script
• Consider using log resource versus Chef::Log
• Shows up in reporting as an updated resource instead of having to trawl through client.log
• Set an idempotency guard!
• Log at the right log level
node['jboss']['instances'].each do |instance| link "/etc/init.d/#{instance['name']}" do to "/etc/init.d/jbossas" end
template "/etc/sysconfig/#{instance['name']}" do source "jbossas.sysconfig.erb" owner node['jboss']['server']['user'] group node['jboss']['server']['group'] mode "00644" variables( :jbossconf => instance['name'] ) action :create end
template "#{node['jboss']['server']['home']}/bin/standalone.sh" do source "standalone.sh.erb" owner node['jboss']['server']['user'] group node['jboss']['server']['group'] mode "00755" action :create end link "#{node['jboss']['server']['home']}/bin/#{instance['name']}.sh" do to "#{node['jboss']['server']['home']}/bin/standalone.sh" endend
Repetition == LWRP Candidate
actions :create, :delete
attribute :instance_name, :kind_of => String, :name_attribute => trueattribute :console_log_level, :kind_of => String, :required => trueattribute :datasources, :kind_of => Hash, :default => {}
.
.
.
default_action :create
Repetition == LWRP Candidate• Perfect for abstracting!
• Resource interface:
jboss_instance "petstore" do instance_name "can_haz_cheezburgerz" console_log_level "DEBUG" datasources {'db1' => 'jdbc://whatever:5432/db1'}end
Repetition == LWRP Candidate
• Write/debug hard logic once
• Clear consumer interface
• Parameter validation & sanity checking
• Non-JBoss experts can invoke without knowing gnarly details
module MyCookbook module Helper
# returns Windows friendly version of the provided path, # ensures backslashes are used everywhere def win_friendly_path(path) path.gsub(::File::SEPARATOR, ::File::ALT_SEPARATOR) if path end end end
Write helper libraries
• Create reusable helper functions in pure Ruby
• Move repetitive detail out of recipe context.
Keep Recipes Small• < 100 lines
• If longer than this, consider breaking up functionality
• Example: nagios server recipe does too much
• Installs Nagios
• Configures Nagios
• Pokes around in data bags for config items
Use Community Helpers• Chef Sugar - http://code.sethvargo.com/chef-sugar/
• Chef Cutlery - https://github.com/realityforge/chef-cutlery
• You can also crib ideas if you want to avoid external dependencies
Wrap-Up
Testing• I didn’t mention testing once in this talk!
• I’m assuming you will write tests for your cookbooks.
• A whole other talk...
• ... including good/bad things to test
Make your code aromatic• Keep recipes small
• Keep recipes simple
• Use a consistent style
• Use Foodcritic
Beware Expertise Bias• Hide gnarly details from recipe context
• Libraries
• LWRPs
• Attributes
• Resist urge to be overly clever - not everyone’s an expert
• Akin to the one-line sed/awk script
• http://tinyurl.com/the-expertise-bias
Learn from Software Developers• Everything I told you about information hiding, design patterns, testing, etc.
• Ops can learn from devs as well!
• Maybe we should call it OpsDev...
Thank You!E: [email protected]: https://github.com/juliandunnT: @julian_dunnW: www.juliandunn.net