Mastering Chef - Sample Chapter
Mastering Chef - Sample Chapter
ee
Sa
pl
during his 10-year long career. He has been a developer, a systems analyst, a systems
administrator, a software consultant, and for the past 6 years, he has been fascinated
with the phenomenal growth witnessed in cloud environments and the challenges of
automation associated with the hosting of the infrastructure in such environments.
Prior to Indix, he worked for start-ups such as SlideShare, R&D organizations such
as CDAC, and even had a stint at a highly automated chemical plant of IFFCO.
Preface
The core philosophy behind configuration management systems has its roots in
the US Department of Defense, where it was adopted as a technical management
discipline. Today, the philosophy has been adopted by many other disciplines,
including systems and software engineering. The basic idea behind a configuration
management system is to establish and maintain the consistency of a system or
product throughout its lifetime. The following are the fundamental activities
associated with any configuration management system:
Revision Control
Configuration
Identification
Change
Management
Product Release
The purpose of configuration management systems is to ensure that the state of the
system isn't residing in the minds of people, but inside a revision control system, from
which it's easy to figure out the current state of the system along with the changes that
have been made to the underlying system over the course of time. It not only allows to
record "what" changes were made, but also "why" the changes were made.
With a phenomenal increase in the usage of cloud platforms, new challenges have
emerged for system architects, as they now need to design systems that are able
to scale up the size of the infrastructure upon the demands laid down by the
application's needs, and the manual configuration of systems in such a dynamic
environment is just not possible.
Preface
Preface
Chapter 7, Roles and Environments, explains that, most of the time, a server is not just
associated with one particular task and can perform many different operations.
For example, you might have a web server that is also performing the role of an
application server and a proxy. Roles allow users to attach multiple recipes to
a server. Also, in most organizations, infrastructure is classified into different
environments depending upon the use. For example, an organization might have a
dev, QA, staging, and production environment. The configuration of applications
running across these environments will be different to some extent. This chapter
will explain what a role is, how we can group multiple recipes in a role, and how
to use roles inside a recipe to do things conditionally. We'll also learn how you
can manage different environments in your infrastructure using Chef.
Chapter 8, Attributes and Their Uses, explains that every service and a server can be
identified with a role and set of properties associated with it. Some properties are
system specific, such as the IP address, kernel, hostname, and so on. While they are
necessary, an effective infrastructure code always needs more properties that can
define the services and the server itself in a more precise manner. In this chapter,
we'll see what the different types of attributes are and how to override values of
the attributes.
Chapter 9, Ohai and Its Plugin Ecosystem, explains that as part of a chef-client run,
many details related to the underlying system, such as architecture, operating system,
network details, filesystem, and so on, are required to be collected by Chef. Ohai is a
tool that allows for this. In this chapter, we'll learn about Ohai and its plugin-based
architecture and associated plugins. We'll also learn how to write our own custom
Ohai plugins.
Chapter 10, Data Bags and Templates, explains that in highly dynamic environments
such as cloud, a configuration management system is only as good as its support for
allowing the specification of the configuration in a form that is dynamic. Templates
are just what the doctor ordered for this use case. Data bags, on the other hand, are
data stores containing the data stored in a JSON format. In this chapter, we'll learn
how to make effective use of databags and templates to define our infrastructure.
Chapter 11, Chef API and Search, explains that the Chef API is perhaps one of the most
powerful features of Chef. Chef has a really wonderful API and its search facility is
what makes it really fun to use. There are lots of cases where you can make use of
Chef's API to build tools that can help in the efficient automation of the tasks. In this
chapter, we'll look at Chef's API, using search in a recipe using Chef API, and also
using a search through Knife.
Chapter 12, Extending Chef, covers the writing of a custom code suited for our
requirements that will help us to extend the functionality of Chef. We'll learn
how to write custom Knife plugins and custom Chef handlers.
Preface
Chapter 13, (Ab)Using Chef, explores some fun uses of Chef, which will allow an
increase in productivity, while managing a large scale infrastructure. We'll see how we
can extend tools such as Capistrano by using Chef API. We'll also learn how to manage
large distributed clusters using an extension of Chef called Ironfan. We will also look at
tools such as the Push Job server, which can be used for the orchestration of chef-client
runs across a set of instances.
Extending Chef
So far, we have seen the different components of Chef and we have also seen what is
possible by making use of the Chef server API. The Chef ecosystem is built for use by
operations people and developers alike, and it comes with a bunch of tools such as
Ohai, Knife, and so on, which can be used to manage your infrastructure easily using
Chef.
However, every now and then you'll find that the available tools just aren't good
enough to meet your requirements and this is the time when you can utilize the
knowledge that you gathered about the API and internals of Ohai and Knife, and
extend the Chef ecosystem by developing your very own resource providers, Ohai
plugins, Knife plugins, or an all together different tool set using Chef API meshed
up with other APIs.
We have already seen how to write our own custom resource provider and Ohai
plugin in the previous chapters. In this chapter, we'll learn how to go about building
custom Knife plugins and we'll also see how we can write custom handlers that can
help us extend the functionality provided by a chef-client run to report any issues
with a chef-client run.
[ 277 ]
Extending Chef
Management of nodes
However, apart from these functions, there are plenty more functions that can be
performed using Knife; all this is possible through the use of plugins. Knife plugins
are a set of one (or more) subcommands that can be added to Knife to support an
additional functionality that is not built into the base set of Knife subcommands.
Most of the Knife plugins are initially built by users such as you, and over a period
of time, they are incorporated into the official Chef code base. A Knife plugin is
usually installed into the ~/.chef/plugins/knife directory, from where it can
be executed just like any other Knife subcommand. It can also be loaded from the
.chef/plugins/knife directory in the Chef repository or if it's installed through
RubyGems, it can be loaded from the path where the executable is installed.
Ideally, a plugin should be kept in the ~/.chef/plugins/knife directory so that it's
reusable across projects, and also in the .chef/plugins/knife directory of the Chef
repository so that its code can be shared with other team members. For distribution
purpose, it should ideally be distributed as a Ruby gem.
[ 278 ]
Chapter 12
:short => "-l value",
:long => "--long-option-name value",
:description => "The description of the option",
:proc => Proc.new { code_to_be_executed },
:boolean => true | false,
:default => default_value
def run
#Code
end
end
end
require: This is used to require other Knife plugins required by a new plugin.
module ModuleName: This defines the namespace in which the plugin will
live. Every Knife plugin lives in its own namespace.
def run: This is the place in which we specify the Ruby code that needs
to be executed.
:boolean defines whether an option is true or false; if the :short and :long
names define value, then this attribute should not be used
:proc defines the code that determines the value for this option
[ 279 ]
Extending Chef
[ 280 ]
Chapter 12
s.on_command_complete do |host|
host = host == :all ? 'All Servers' : host
Chef::Log.debug("command complete on #{host}")
end
s
end
end
... # more def blocks
end
end
end
Namespace
As we saw with skeleton, the Knife plugin should have its own namespace and the
namespace is declared using the module method as follows:
require 'chef/knife'
#Any other require, if needed
module NameSpace
class SubclassName < Chef::Knife
Here, the plugin is available under the namespace called NameSpace. One should
keep in mind that Knife loads the subcommand irrespective of the namespace to
which it belongs.
Class name
The class name declares a plugin as a subclass of both Knife and Chef. For example:
class SubclassName < Chef::Knife
The capitalization of the name is very important. The capitalization pattern can be
used to define the word grouping that makes the best sense for the use of a plugin.
For example, if we want our plugin subcommand to work as follows:
knife bootstrap hdfs
[ 281 ]
Extending Chef
It's important to remember that a plugin can override an existing Knife subcommand.
For example, we already know about commands such as knife cookbook upload. If
you want to override the current functionality of this command, all you need to do is
create a new plugin with the following name:
class CookbookUpload < Chef::Knife
Banner
Whenever a user enters the knife help command, he/she is presented with a list
of available subcommands. For example:
knife --help
Usage: knife sub-command (options)
-s, --server-url URL
** BACKUP COMMANDS **
knife backup export [COMPONENT [COMPONENT ...]] [-D DIR] (options)
knife backup restore [COMPONENT [COMPONENT ...]] [-D DIR] (options)
** BOOTSTRAP COMMANDS **
knife bootstrap FQDN (options)
....
Let us say we are creating a new plugin and we would want Knife to be able to list it
when a user enters the knife help command. To accomplish this, we would need
to make use of banner.
For example, let's say we've a plugin called BootstrapHdfs with the following code:
module NameSpace
class BootstrapHdfs < Chef::Knife
...
[ 282 ]
Chapter 12
banner "knife bootstrap hdfs (options)"
...
end
end
Now, when a user enters the knife help command, he'll see the
following output:
** BOOTSTRAPHDFS COMMANDS **
knife bootstrap hdfs (options)
Dependencies
Reusability is one of the key paradigms in development and the same is true for
Knife plugins. If you want a functionality of one Knife plugin to be available in
another, you can use the deps method to ensure that all the necessary files are
available. The deps method acts like a lazy loader, and it ensures that dependencies
are loaded only when a plugin that requires them is executed.
This is one of the reasons for using deps over require, as the overhead of the
loading classes is reduced, thereby resulting in code with a lower memory
footprint; hence, faster execution.
One can use the following syntax to specify dependencies:
deps do
require 'chef/knife/name_of_command'
require 'chef/search/query'
#Other requires to fullfill dependencies
end
Requirements
One can acquire the functionality available in other Knife plugins using the require
method. This method can also be used to require the functionality available in other
external libraries. This method can be used right at the beginning of the plugin script,
however, it's always wise to use it inside deps, or else the libraries will be loaded
even when they are not being put to use.
The syntax to use require is fairly simple, as follows:
require 'path_from_where_to_load_library'
[ 283 ]
Extending Chef
Let's say we want to use some functionalities provided by the bootstrap plugin.
In order to accomplish this, we will first need to require the plugin:
require 'chef/knife/bootstrap'
Once we've the object with us, we can use it to pass arguments or options to that
object. This is accomplished by changing the object's config and the name_arg
variables. For example:
obj.config[:use_sudo] = true
Finally, we can run the plugin using the run method as follows:
obj.run
Options
Almost every other Knife plugin accepts some command line option or other. These
options can be added to a Knife subcommand using the option method. An option
can have a Boolean value, string value, or we can even write a piece of code to
determine the value of an option.
Let's see each of them in action once:
An option with a Boolean value (true/false):
option :true_or_false,
:short => "-t",
:long => "true-or-false",
:description => "True/False?",
:boolean => true | false,
:default => true
[ 284 ]
Chapter 12
Here the proc attribute will convert a list of comma-separated values into an array.
All the options that are sent to the Knife subcommand through a command line are
available in form of a hash, which can be accessed using the config method.
For example, say we had an option:
option :option1
:short => "-s VALUE",
:long => "some-string-value VALUE",
:description => "Some string value for option1",
:default => "option1"
Now, while issuing the Knife subcommand, say a user entered something like this:
$ knife subcommand option1 "option1_value"
We can access this value for option1 in our Knife plugin run method using
config[:option1]
When a user enters the knife help command, the description attributes are
displayed as part of help. For example:
**EXAMPLE COMMANDS**
knife example
-s, --some-type-of-string-value
-t, --true-or-false
value false?
-T, --tags
virtual machine.
[ 285 ]
Extending Chef
Arguments
A Knife plugin can also accept the command-line arguments that aren't specified
using the option flag, for example, knife node show NODE. These arguments are
added using the name_args method:
require 'chef/knife'
module MyPlugin
class ShowMsg << Chef::Knife
banner 'knife show msg MESSAGE'
def run
unless name_args.size == 1
puts "You need to supply a string as an argument."
show_usage
exit 1
end
msg = name_args.join(" ")
puts msg
end
end
end
...
Here, we didn't pass any argument to the subcommand and, rightfully, Knife sent
back a message saying You need to supply a string as an argument.
Now, let's pass a string as an argument to the subcommand and see how it behaves:
knife show msg "duh duh"
duh duh
Under the hood what's happening is that name_args is an array, which is getting
populated by the arguments that we have passed in the command line. In the
last example, the name_args array would've contained two entries ("duh","duh").
We use the join method of the Array class to create a string out of these two entities
and, finally, print the string.
[ 286 ]
Chapter 12
Since the name of a node is generally FQDN, you can use the values returned in
node.name to connect to remote machines and use any library such as net-scp
to allow users to upload their files/folders to a remote machine. We'll try to
accomplish this task when we write our custom plugin at the end of this chapter.
We can also use this information to edit nodes. For example, say we had a set of
machines acting as web servers. Initially, all these machines were running Apache
as a web server. However, as the requirements changed, we wanted to switch over
to Nginx. We can run the following piece of code to accomplish this task:
require 'chef/search/query'
query_object = Chef::Search::Query.new
[ 287 ]
Extending Chef
query = 'run_list:*recipe\\[apache2\\]*'
query_object.search('node',query) do |node|
ui.msg "Changing run_list to recipe[nginx] for #{node.name}"
node.run_list("recipe[nginx]")
node.save
ui.msg "New run_list: #{node.run_list}"
end
knife.rb settings
Some of the settings defined by a Knife plugin can be configured so that they can be
set inside the knife.rb script. There are two ways to go about doing this:
By using the :proc attribute of the option method and code that references
Chef::Config[:knife][:setting_name]
An option that is defined in this way can be configured in knife.rb by using the
following syntax:
knife [:setting_name]
This approach is especially useful when a particular setting is used a lot. The
precedence order for the Knife option is:
1. The value passed via a command line.
2. The value saved in knife.rb
3. The default value.
The following example shows how the Knife bootstrap command uses a value in
knife.rb using the :proc attribute:
option :ssh_port
:short => '-p PORT',
:long => 'ssh-port PORT',
:description => 'The ssh port',
:proc => Proc.new { |key| Chef::Config[:knife][:ssh_port] = key
}
[ 288 ]
Chapter 12
A series of settings in Knife ssh are associated with a Knife bootstrap using
the ssh.config[:setting_name] syntax
[ 289 ]
Extending Chef
User interactions
The ui object provides a set of methods that can be used to define user
interactions and to help ensure a consistent user experience across all different
Knife plugins. One should make use of these methods, rather than handling user
interactions manually.
Method
ui.ask(*args, &block)
Description
The ask method calls the corresponding ask
method of the HighLine library. More details about
the HighLine library can be found at http://www.
rubydoc.info/gems/highline/1.7.2.
ui.ask_question(question,
opts={})
ui.color?()
ui.confirm(question,
append_instructions=true)
ui.edit_data(data,parse_
output=true)
ui.edit_object(class,name)
Chapter 12
Method
ui.error
Description
ui.fatal
ui.highline
ui.info
ui.interchange
ui.list(*args)
ui.msg(message)
ui.output(data)
ui.pretty_print(data)
ui.use_
presenter(presenter_class)
ui.warn(message)
For example, to show a fatal error in a plugin in the same way that it would be
shown in Knife, do something similar to the following:
unless name_args.size == 1
ui.fatal "Fatal error !!!"
show_usage
exit 1
end
Exception handling
In most cases, the exception handling available within Knife is enough to ensure
that the exception handling for a plugin is consistent across all the different plugins.
However, if the required one can handle exceptions in the same way as any other
Ruby program, one can make use of the begin-end block, along with rescue clauses,
to tell Ruby which exceptions we want to handle.
[ 291 ]
Extending Chef
For example:
def raise_and_rescue
begin
puts 'Before raise'
raise 'An error has happened.'
puts 'After raise'
rescue
puts 'Rescued'
end
puts 'After begin block'
end
raise_and_rescue
By environments
By roles
Actually, any valid Chef search query that returns a node
list can be the criteria to identify machines. However, we are
limiting ourselves to these two criteria for now.
[ 292 ]
Chapter 12
Often, there are situations where a user might want to upload a file or folder to all the
machines in a particular environment, or to all the machines belonging to a particular
role. This plugin will help users accomplish this task with lots of ease. The plugin
will accept three arguments. The first one will be a key-value pair with the key being
chef_environment or a role, the second argument will be a path to the file or folder
that is required to be uploaded, and the third argument will be the path on a remote
machine where the files/folders will be uploaded to. The plugin will use Chef's search
functionality to find the FQDN of machines, and eventually make use of the net-scp
library to transfer the file/folder to the machines.
Our plugin will be called knife-scp and we would like to use it as follows:
knife scp chef_environment:production /path_of_file_or_folder_locally /
path_on_remote_machine
[ 293 ]
Extending Chef
show_usage
exit 1
end
Chef::Config.from_file(File.expand_
path("#{config[:knife_config_path]}"))
query = name_args[0]
local_path = name_args[1]
remote_path = name_args[2]
query_object = Chef::Search::Query.new
fqdn_list = Array.new
query_object.search('node',query) do |node|
fqdn_list << node.name
end
if fqdn_list.length < 1
ui.msg "No valid servers found to copy the files to"
end
unless File.exist?(local_path)
ui.msg "#{local_path} doesn't exist on local machine"
exit 1
end
Parallel.each((1..fqdn_list.length).to_a, :in_processes => fqdn_
list.length) do |i|
puts "Copying #{local_path} to #{Chef::Config[:knife][:ssh_
user]}@#{fqdn_list[i-1]}:#{remote_path} "
Net::SCP.upload!(fqdn_list[i-1],"#{Chef::Config[:knife]
[:ssh_user]}","#{local_path}","#{remote_path}",:ssh => { :keys =>
["#{Chef::Config[:knife][:identity_file]}"] }, :recursive => true)
end
end
end
end
The net-scp gem to do the actual transfer. This gem is a pure Ruby
implementation of the SCP protocol. More information about the gem
can be found at https://github.com/net-ssh/net-scp.
[ 294 ]
Chapter 12
Both these gems and the Chef search library are required in the deps block to define
the dependencies.
This plugin accepts three command line arguments and uses knife.rb to get
information about which user to connect over SSH and also uses knife.rb to fetch
information about the SSH key file to use. All these command line arguments are
stored in the name_args array.
A Chef search is then used to find a list of servers that match the query, and
eventually a parallel gem is used to parallely SCP the file from a local machine
to a list of servers returned by a Chef query.
As you can see, we've tried to handle a few error situations, however, there is still
a possibility of this plugin throwing away errors as the Net::SCP.upload function
can error out at times.
Let's see our plugin in action:
Case1: The file that is supposed to be uploaded doesn't exist locally. We expect the
script to error out with an appropriate message:
knife scp 'chef_environment:ft' /Users/mayank/test.py /tmp
/Users/mayank/test.py doesn't exist on local machine
[ 295 ]
Extending Chef
The s.files variable contains the list of files that will be deployed by a gem install
command. Knife can load the files from gem_path/lib/chef/knife/<file_name>.
rb, and hence we've kept the knife-scp.rb script in that location.
The s.add_runtime_dependency dependency is used to ensure that the required
gems are installed whenever a user tries to install our gem.
[ 296 ]
Chapter 12
Once the file is there, we can just run a gem build to build our gem file as follows:
Use a license
http://opensource.org/licenses/alphabetical
WARNING:
help
The gem file is created and now, we can just use gem install knife-scp1.0.0.gem to install our gem. This will also take care of the installation of any
dependencies such as parallel, net-scp gems, and so on.
You can find a source code for this plugin at the following location:
https://github.com/maxc0d3r/knife-plugins.
Once the gem has been installed, the user can run it as mentioned earlier.
For the purpose of distribution of this gem, it can either be pushed using a local
gem repository, or it can be published to https://rubygems.org/. To publish it to
https://rubygems.org/, create an account there.
Run the following command to log in using a gem:
gem push
That's it! Now you should be able to access your gem at the following location:
http://www.rubygems.org/gems/your_gem_name.
[ 297 ]
Extending Chef
As you might have noticed, we've not written any tests so far to check the plugin.
It's always a good idea to write test cases before submitting your plugin to the
community. It's useful both to the developer and consumers of the code, as both
know that the plugin is going to work as expected. Gems support adding test files
into the package itself so that tests can be executed when a gem is downloaded.
RSpec is a popular choice to test a framework, however, it really doesn't matter
which tool you use to test your code. The point is that you need to test and ship.
Some popular Knife plugins, built by a community, and their uses, are as follows:
knife-elb:
This plugin allows the automation of the process of addition and deletion of nodes
from Elastic Load Balancers on AWS.
knife-inspect:
This plugin allows you to see the difference between what's on a Chef server versus
what's on a local Chef repository.
knife-community:
This plugin helps to deploy Chef cookbooks to Chef Supermarket.
knife-block:
This plugin allows you to configure and manage multiple Knife configuration files
against multiple Chef servers.
knife-tagbulk:
This plugin allows bulk tag operations (creation or deletion) using standard Chef
search queries. More information about the plugin can be found at: https://
github.com/priestjim/knife-tagbulk.
You can find a lot of other useful community-written plugins at: https://docs.
chef.io/community_plugin_knife.html.
[ 298 ]
Chapter 12
The exception handler: This is used to identify situations that have caused
a chef-client run to fail. This can be used to send out alerts over an email
or dashboard.
The report handler: This is used to report back when a chef-client run has
successfully completed. This can report details about the run, such
as the number of resources updated, time taken for a chef-client run to
complete, and so on.
Writing custom Chef handlers is nothing more than just inheriting your class from
Chef::Handler and overriding the report method.
Let's say we want to send out an email every time a chef-client run breaks. Chef
provides a failed? method to check the status of a chef-client run. The following
is a very simple piece of code that will help us accomplish this:
require 'net/smtp'
module CustomHandler
class Emailer < Chef::Handler
def send_email(to,opts={})
opts[:server] ||= 'localhost'
opts[:from] ||='[email protected]'
opts[:subject] ||='Error'
opts[:body] ||= 'There was an error running chef-client'
msg = <<EOF
From: <#{opts[:from]}>
To: #{to}
Subject: #{opts[:subject]}
#{opts[:body]}
EOF
[ 299 ]
Extending Chef
Net::SMTP.start(opts[:server]) do |smtp|
smtp.send_message msg, opts[:from], to
end
end
def report
name = node.name
subject = "Chef run failure on #{name}"
body = [run_status.formatted_exception]
body += ::Array(backtrace).join("\n")
if failed?
send_email(
"[email protected]",
:subject => subject,
:body => body
)
end
end
end
end
If you don't have the required libraries already installed on your machine, you'll
need to make use of chef_gem to install them first before you actually make use of
this code.
With your handler code ready, you can make use of the chef_handler cookbook
to install this custom handler. To do so, create a new cookbook, email-handler,
and copy the file emailer.rb created earlier to the file's source. Once done, add the
following recipe code:
include_recipe 'chef_handler'
handler_path = node['chef_handler']['handler_path']
handler = ::File.join handler_path, 'emailer'
cookbook_file "#{handler}.rb" do
source "emailer.rb"
end
chef_handler "CustomHandler::Emailer" do
source handler
action :enable
end
[ 300 ]
Chapter 12
Now, just include this handler into your base role, or at the start of run_list and
during the next chef-client run, if anything breaks, an email will be sent across to
[email protected].
You can configure many different kinds of handlers like the ones that push
notifications over to IRC, Twitter, and so on, or you may even write them for
scenarios where you don't want to leave a component of a system in a state that
is undesirable. For example, say you were in a middle of a chef-client run that
adds/deletes collections from Solr. Now, you might not want to leave the Solr setup
in a messed-up state if something were to go wrong with the provisioning process.
In order to ensure that a system is in the right state, you can write your own custom
handlers, which can be used to handle such situations and revert the changes done
until now by the chef-client run.
Summary
In this chapter, we learned about how custom Knife plugins can be used. We also
learned how we can write our own custom Knife plugin and distribute it by packaging
it as a gem. Finally, we learned about custom Chef handlers and how they can be used
effectively to communicate information and statistics about a chef-client run to users/
admins, or handle any issues with a chef-client run.
In the next chapter, we'll go about building a set of tools that can be used to manage
your infrastructure with a lot of ease using Chef. These tools will combine the Chef
API with some other APIs to accomplish goals that otherwise would be very difficult
to accomplish.
[ 301 ]
www.PacktPub.com
Stay Connected: