What Makes a Good Chef Cookbook? (May 2014 Edition)

2,341 views

Published on

What makes a good or bad Chef cookbook?
Presented at the Boston Chef meetup on May 22, 2014.

Published in: Technology, Self Improvement
1 Comment
3 Likes
Statistics
Notes
No Downloads
Views
Total views
2,341
On SlideShare
0
From Embeds
0
Number of Embeds
8
Actions
Shares
0
Downloads
39
Comments
1
Likes
3
Embeds 0
No embeds

No notes for slide

What Makes a Good Chef Cookbook? (May 2014 Edition)

  1. 1. What Makes a Good Cookbook? Julian C. Dunn Senior Consulting Engineer Engineering Team Lead Chef Software, Inc. <jdunn@getchef.com>
  2. 2. $ whoami • Consulting Engineer Engineering Team Lead at Chef • System Administrator • Reformed Java Developer • Writes silly code like this •https://github.com/juliandunn/ doge-chef-formatter
  3. 3. Finding a Good Cookbook LMGTCFY
  4. 4. Do judge a cookbook by its cover
  5. 5. 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
  6. 6. 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
  7. 7. if node.run_list.recipes.include?('foo::bar') ... end Poking run_list and environment if node.chef_environment == 'production' ... end • Use feature flags!
  8. 8. template "/etc/whatever.conf" do ... not_if { foo } end Compile vs. Execute Errors if foo template "/etc/whatever.conf" do ... end end not the same thing as
  9. 9. execute 'yum install httpd' do not_if 'rpm -qa | grep -x httpd' end Not declarative • Also, the Chef recipe with 100 bash or powershell_script resource declarations
  10. 10. execute '/i/will/run/every/time' do action :run # because I don't have a guard here end Missing guards
  11. 11. default['mydaemon']['port'] = '1433' # don't you mean the integer 1433? default['mydaemon']['knob'] = 'disabled' # don't you mean false? Not using native Ruby data types • If you use native data types you can validate people’s input.
  12. 12. Fear of LWRPs • Missed abstraction opportunities • No good example to put here; they’re all 200 lines long (thus proving my point)
  13. 13. remote_file 'whatever.tar.gz' do source 'http://hardcoded.url.com/' end Hardcoded Strings
  14. 14. Excess Conditions & Recipe Length • https://github.com/opscode- cookbooks/mysql/blob/v3.0.12/recipes/server.rb • (We’ve since refactored this)
  15. 15. Good Cookbooks...
  16. 16. Put control flow in attributes • Especially for cross-platform cookbooks • Set common set of attributes, write common behavior in recipe context
  17. 17. 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" Control Flow in Attributes
  18. 18. node['postgresql']['server']['packages'].each do |pg_pack| package pg_pack end Common Recipe Code • Easy to support more platforms without modifying recipe
  19. 19. default.rb server.rb _suse.rb_fedora.rb_windows.rb Separate recipes by OS • If things you do are very different per platform, separate them into different recipes
  20. 20. “Public” versus “Private” recipes • ‘_’ faux-namespacing
  21. 21. 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!
  22. 22. 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
  23. 23. 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
  24. 24. git clone git://github.com/foozolix/foozolix.git cd foozolix && ./configure make make install Give people options for installation • At least give people a way to install from packages. • “Compile from source” should be banned in most cases.
  25. 25. Be declarative • Know and use built-in Chef resources • Know where to find LWRPs to avoid batch/execute/powershell_script • Consider 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
  26. 26. Run System Commands Safely • system • backticks •Chef::Mixin::ShellOut •shell_out •shell_out!
  27. 27. $ chef-apply -s Chef::Recipe.send(:include, Chef::Mixin::ShellOut) cmd = shell_out!("echo -n Ohai, world") log cmd.stdout ^D Recipe: (chef-apply cookbook)::(chef-apply recipe) * log[Ohai, world] action write Example Recipe Context
  28. 28. unless node.chef_environment('pigsty') include_recipe 'bacon::default' end Feature Flags Example if node['foo']['bar']['can_haz_bacon'] include_recipe 'bacon::default' end • Instead:
  29. 29. 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" end end Repetition == LWRP Candidate
  30. 30. actions :create, :delete attribute :instance_name, :kind_of => String, :name_attribute => true attribute :console_log_level, :kind_of => String, :required => true attribute :datasources, :kind_of => Hash, :default => {} . . . default_action :create Repetition == LWRP Candidate • Perfect for abstracting! • Resource interface:
  31. 31. 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
  32. 32. 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. • http://tinyurl.com/chef-libraries
  33. 33. 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
  34. 34. Use Community Helpers • Chef Sugar - http://code.sethvargo.com/chef-sugar/ • Chef Cutlery - https://github.com/realityforge/chef-cutlery • Attribute Validator - https://github.com/clintoncwolfe/attribute-validator • You can also crib the ideas if you want to avoid external dependencies
  35. 35. Wrap-Up
  36. 36. 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
  37. 37. Make your code aromatic • Keep recipes small • Keep recipes simple • Use a consistent style • Use Foodcritic
  38. 38. 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
  39. 39. 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...
  40. 40. Don’t Yet Know Chef? • 2-Day Chef Fundamentals Training in Boston • June 16-17 • New Horizons, 75 Federal St., Suite 1205 • Use code MEETUP to save 10%
  41. 41. Thank You! E: jdunn@getchef.com G: https://github.com/juliandunn T: @julian_dunn W: www.juliandunn.net

×