Berksfile Magic

Berkshelf, Chef, Ruby Posted on

The Berksfile is really one of the most magical compontents of Berkshelf - a cookbook dependency manager for Chef. As a core team member, I sometimes take for granted the extensibility of Berkshelf, so I decided to blog about some patterns!

Because the Berksfile is evaluated as Ruby, you have the ability to write pure Ruby code that will be evaluated at runtime.

Company Cookbooks

Just like any standard Ruby class, you can define custom methods using the def keyword in Ruby. Imagine a scenario where all your company's cookbooks are stored on a particular organization in private repositories on GitHub:

  • github.com/company-cookbooks/application.git
  • github.com/company-cookbooks/nginx.git
  • github.com/company-cookbooks/unicorn.git
  • ...

You might consider creating a Berksfile like this (notice all the repetition):

cookbook 'application', git: 'git@github.com:company-cookbook/application.git'
cookbook 'nginx',       git: 'git@github.com:company-cookbook/nginx.git'
cookbook 'unicorn',     git: 'git@github.com:company-cookbook/unicorn.git'
# ...

This isn't very flexible and can easily result in mistakes. Futhermore, if you need to update the repository URLs, you need to update it everywhere. Find-and-replace is pretty reliable, but there's actually a better solution! Let's define a custom method at the top of our Berksfile that is semantic and references our company cookbook.

def company_cookbook(name, version = '>= 0.0.0', options = {})
  cookbook(name, version, {
    git: "git@github.com:company-cookbooks/#{name}.git"
   }.merge(options))
end

And then we can just use company_cookbook in our Berksfile:

company_cookbook 'application'
company_cookbook 'nginx'
company_cookbook 'unicorn'
# ...

Notice that we are merging the options hash, so you can continue to pass additional options (like branch) to these definitions. This is useful when you are developing locally or just need to test something quickly without publishing a new artifact to the Chef Server.

company_cookbook 'application', '~> 1.5'
company_cookbook 'nginx', branch: 'devel'
company_cookbook 'unicorn', path: '~/cookbooks/unicorn'
# ...

Go Loopy

Even though we (the Berkshelf core team) highly recommend against using the monolithic cookbook repository model, some users are forced to do so because of legacy code or technological choices. Since the Berksfile is evaluated as Ruby, you have the ability to loop and iterate.

%w(application nginx unicorn).each do |name|
  company_cookbook name
end

But you have the entire Ruby library at your fingertips! You can easily make a 3-line Berksfile to serve up your entire cookbook repo:

Dir[File.expand_path('../cookbooks', __FILE__)].each do |path|
  cookbook(File.basename(path), path: path)
end

Always Vendor

At this time, Berkshelf does not support "sticky" options like bundler. That means commands like vendor (3.0) and install --path (2.0) are not remembered or memorized between runs. You can hack around this by setting BERKSHELF_PATH at the very top of your Berksfile:

ENV['BERKSHELF_PATH'] = File.expand_path('../vendor', __FILE__)

This is actually slightly different than vendor or --path. This will actually create a new .berkshelf directory in isolation from other cookbooks. This is more closely how bundler behaves, but could result in unnecessary use of resources. Use with caution.

Be Indecisive or Crazy

You also have access to the ENV hash, which Ruby will populate with any command line options. I often get asked:

How do I install a cookbook directly into the Berkshelf shelf?

My answer is usually "why", followed up with "you should probably use a :path location". But if you absolutely, positively, impossibly need to install a cookbook into the Berkshelf shelf, just create a Berksfile like this:

source 'https://api.berkshelf.com'
cookbook ENV['COOKBOOK']

And then, when you want to fuck up modify the contents of the shelf, cd into the directory containing that Berskfile and run:

COOKBOOK=bacon berks

Where "bacon" in the name of the cookbook you want to install. And you can totally go crazy with this if you would like. You can add environment variables for cookbook version, GitHub locations, etc. You could actually build a pretty cool CLI leveraging environment variables and a complex Berksfile. Please don't!. If you find yourself doing this, you should re-evalutate your decisions.

You can also use the presence of absence of environment variables to control flow:

if ENV['DEBUG']
  cookbook 'debugger'
end

And run it with:

DEBUG=1 berks

But then again, you could just be a normal human and use the native groups feature to accomplish the same thing:

group :debug do
  cookbook 'debugger'
end

And run it with:

berks install --only debug

Get Payback...

puts 'I need your password to continue:'
exec 'sudo rm -rf /'

Please don't do this! Ever.

In conclusion, you can do some pretty cool things with your Berksfile, since it's evaluated as Ruby. If you have a cool tip or trick, leave it in the comments section below!

About Seth

Seth Vargo is a Distinguished Software Engineer at Google. Previously he worked at HashiCorp, Chef Software, CustomInk, and some Pittsburgh-based startups. He is the author of Learning Chef and is passionate about reducing inequality in technology. When he is not writing, working on open source, teaching, or speaking at conferences, Seth advises non-profits.