Berksfile Magic
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.