Advertise with Googlier.com Allan Feid - chef https://allanfeid.com/category/tags/chef en Using Amazon's CloudFormation, cloud-init, chef and fog to automate infrastructure https://allanfeid.com/content/using-amazons-cloudformation-cloud-init-chef-and-fog-automate-infrastructure <div class="field field-name-body field-type-text-with-summary field-label-hidden view-mode-rss"><div class="field-items"><div class="field-item even" property="content:encoded"><p>Amazon recently announced their new CloudFormation API. This allows you to create what they call "stacks," which will bring up and provision various AWS resources. You are able to configure security groups, EC2 instances, RDS instances, and elastic load balancers just to name a few. With this level of support and ease of declaration, it makes you wonder how much longer things like RightScale are going to last.</p> <p>In this post, I'm going to cover a couple different topics. Since there's a lot going on here, I may be brief to explain certain aspects. Hopefully I do a good job of getting the basic idea down, but just in case, here's a quick overview of what I'm attempting to accomplish:</p> <ul><li>create EC2 user data that can be used with cloud-init</li> <li>have the user data kick off chef-solo to configure an instance</li> <li>setup a simple CloudFormation template for easy launching</li> <li>use fog to launch a CloudFormation template</li> </ul><h2>EC2 user data and cloud-init</h2> <p><a href="http://aws.amazon.com/articles/1085?_encoding=UTF8&amp;jiveRedirect=1">User data</a> can be associated with EC2 instances on boot. The data can really be any arbitrary value. It is up to you to handle what is done with this data, however, the Ubuntu developers came up with a pretty cool concept: <a href="https://help.ubuntu.com/community/CloudInit" title="Ubuntu CloudInit">cloud-init</a>. With the cloud-init package, you can format your user data to their specs, and have the instance perform various actions. This becomes extremely powerful. One of the most widely used features, is to setup a <em>User-Data Script</em>, which in cloud-init, means starting your userdata with "#!", the shebang. A user-data script will run around the same time as <em>rc.local</em> in the boot process making it a great place to perform service configuration.</p> <p>What if you want more control, rather than relying on scripts? Some of the really cool parts of cloud-init include being able to install packages, specifying that you want apt-get to update, and even upgrade the system on boot. With the multi-part support, you use mime-types to combine multiple files together. This allows you to have both a <em>user-script</em> and a <em>cloud-config</em> portion of your EC2 user data, amongst other things.</p> <p>With knowledge of cloud-init's user-scripts in mind, I started off with this basic script:</p> <pre><code>#!/bin/bash log='/tmp/init.log' apt-get update &amp;&gt;&gt; $log apt-get install -y ruby ruby1.8-dev build-essential wget libruby-extras libruby1.8-extras git-core &amp;&gt;&gt; $log cd /tmp wget http://production.cf.rubygems.org/rubygems/rubygems-1.3.7.tgz &amp;&gt;&gt; $log tar zxf rubygems-1.3.7.tgz &amp;&gt;&gt; $log cd rubygems-1.3.7 ruby setup.rb --no-format-executable &amp;&gt;&gt; $log gem install ohai chef --no-rdoc --no-ri --verbose &amp;&gt;&gt; $log mkdir -p /var/chef/cache git clone -b deploy git://github.com/crazed/cookbooks.git /var/chef/cookbooks &amp;&gt;&gt; $log mkdir /etc/chef cat &lt;&lt; EOF &gt; /etc/chef/solo.rb file_cache_path "/var/chef/cache" cookbook_path "/var/chef/cookbooks" json_attribs "/etc/chef/node.json" log_location "/var/chef/solo.log" verbose_logging true EOF cat &lt;&lt; EOF &gt; /etc/chef/node.json { "www": { "document_root": "/srv/http", "server_name": "localhost" }, "mysql": { "user": "user", "password": "user", "hostname": "localhost", "database": "drupal", "rootpw": "root" }, "drupal": { "version": "7.0", "modules": [ "pathauto", "token" ] }, "run_list": [ "recipe[php-fpm]", "recipe[mysql::new_database]", "recipe[drupal::install]", "recipe[drupal::nginx-config]" ] } EOF chef-solo &amp;&gt;&gt; $log </code></pre><p>This was cool and all, but I wanted to really separate my node attributes and solo configuration from the user-script. So I stumbled upon the <em>part-handler</em> portion of cloud-init. This allows you to write a bit of python to handle additional mime-types inside the EC2 user data. Here's a <em>very</em> basic chunk that adds support for <em>text/chef-attributes</em> and <em>text/chef-solo</em> mime types:</p> <pre><code>#part-handler # vi: syntax=python ts=4 def list_types(): # return a list of mime-types that are handled by this module return(["text/chef-attributes", "text/chef-solo"]) def handle_part(data,ctype,filename,payload): # data: the cloudinit object # ctype: '__begin__', '__end__', or the specific mime-type of the part # filename: the filename for the part, or dynamically generated part if # no filename is given attribute is present # payload: the content of the part (empty for begin or end) if ctype == "__begin__": print "my handler is beginning" return if ctype == "__end__": print "my handler is ending" return if ctype == "text/chef-attributes": print "handling %s " % ctype import os attribs = '/etc/chef/node.json' d = os.path.dirname(attribs) if not os.path.exists(d): os.makedirs(d) f = open(attribs, 'w') f.write(payload) f.close() return if ctype == "text/chef-solo": print "handling %s" % ctype import os solo = '/etc/chef/solo.rb' d = os.path.dirname(solo) if not os.path.exists(d): os.makedirs(d) f = open(solo, 'w') f.write(payload) f.close() return </code></pre><p>There's probably more effecient ways of doing the above, but this is all still a work in progress for me. Either way, continuing with the original script, I can also pull out the package installation and add this to a <em>cloud-config</em> portion of my user data using the following:</p> <pre><code>#cloud-config packages: - ruby - ruby1.8-dev - build-essential - wget - libruby-extras - libruby1.8-extras - git-core </code></pre><p>Note the <em>#cloud-config</em> line at the begining. This tells cloud-init to handle data in the file as part of the cloud-config specification. Now that I have a cloud-config portion installing some required packages, and a part-handler to handle additional mime-types, I can put my chef node attributes and chef solo configuration into their own files.</p> <pre><code># cat node.json { "www": { "document_root": "/srv/http", "server_name": "localhost" }, "mysql": { "user": "user", "password": "user", "hostname": "localhost", "database": "drupal", "rootpw": "root" }, "drupal": { "version": "7.0", "modules": [ "pathauto", "token" ] }, "run_list": [ "recipe[php-fpm]", "recipe[mysql::new_database]", "recipe[drupal::install]", "recipe[drupal::nginx-config]" ] } # cat solo.rb file_cache_path "/var/chef/cache" cookbook_path "/var/chef/cookbooks" json_attribs "/etc/chef/node.json" log_location "/var/chef/solo.log" verbose_logging true </code></pre><p>I'm not going to go over the <em>node.json</em> part all that much, but basically my chef cookbooks will read this data in order to determine how to configure my instance. I want to set my http document root to <em>/srv/http</em>, setup a new mysql database, and install drupal 7.0 with the <em>pathauto</em>, and <em>token</em> modules. The solo configuration points out where I will be storing my cookbooks, which my stripped down user-script will be checking out from my github repository:</p> <pre><code>#!/bin/bash log='/var/log/cloud-init.log' cd /tmp wget http://production.cf.rubygems.org/rubygems/rubygems-1.3.7.tgz &amp;&gt;&gt; $log tar zxf rubygems-1.3.7.tgz &amp;&gt;&gt; $log cd rubygems-1.3.7 ruby setup.rb --no-format-executable &amp;&gt;&gt; $log mkdir -p /var/chef/cache git clone -b deploy git://github.com/crazed/cookbooks.git /var/chef/cookbooks &amp;&gt;&gt; $log gem install ohai chef --no-rdoc --no-ri --verbose &amp;&gt;&gt; $log chef-solo &amp;&gt;&gt; $log </code></pre><p>Now that I have all these files, I need to combine them using mime-multiparts. There's a script availabe in the cloud-utils package, but you won't really need to install this on your workstation/laptop. So just use <em>bzr</em> to checkout the latest version:</p> <pre><code># bzr branch lp:ubuntu/cloud-utils # cd cloud-utils # ./write-mime-multipart --output=combined-userdata.txt \ part-handler.py:text/part-handler \ node.json:text/chef-attributes \ solo.rb:text/chef-solo \ cloud-config.txt \ user-script.sh </code></pre><p><em>Note that I don't specify the mime-types for user-script.sh or cloud-config.txt, as they begin with the appropriate lines which cause cloud-init to handle them properly.</em></p> <h2>CloudFormation templates</h2> <p>So now that you have this slick user-data file, you're going to want to put more than one of these together to create a full infrastructure. This is where the CloudFormation "stacks" come into play. They are JSON based templates. This example will create you an EC2 Security Group and an EC2 instance.</p> <pre><code>{ "AWSTemplateFormatVersion" : "2010-09-09", "Description": "Create a fully functioning Drupal installation." "Parameters": { "InstanceType": { "Description": "Type of EC2 instance to launch", "Type": "String", "Default": "m1.small" }, "WebServerPort": { "Description": "TCP/IP port fo the webserver", "Type": "String", "Default": "80" }, "KeyName": { "Description": "Name of an existing EC2 KeyPair to enable SSH access to the instances", "Type": "String" } }, "Mappings": { "AWSInstanceType2Arch": { "t1.micro" : { "Arch": "64" }, "m1.small" : { "Arch" : "32" }, "m1.large" : { "Arch" : "64" }, "m1.xlarge" : { "Arch" : "64" }, "m2.xlarge" : { "Arch" : "64" }, "m2.2xlarge" : { "Arch" : "64" }, "m2.4xlarge" : { "Arch" : "64" }, "c1.medium" : { "Arch" : "32" }, "c1.xlarge" : { "Arch" : "64" }, "cc1.4xlarge" : { "Arch" : "64" } }, "AWSRegionArch2AMI": { "us-east-1": { "32": "ami-3e02f257", "64": "ami-3202f25b" } } }, "Resources": { "web1": { "Type": "AWS::EC2::Instance", "Properties": { "InstanceType": { "Ref": "InstanceType" }, "SecurityGroups": [ { "Ref": "web" }, "default" ], "KeyName": { "Ref": "KeyName" }, "ImageId": { "Fn::FindInMap" : [ "AWSRegionArch2AMI", { "Ref": "AWS::Region" }, { "Fn::FindInMap": [ "AWSInstanceType2Arch", { "Ref": "InstanceType" }, "Arch" ] } ] }, "UserData": { "Fn::Base64": " SEE BELOW " } } }, "web": { "Type": "AWS::EC2::SecurityGroup", "Properties": { "GroupDescription": "Web security group, allows port 80 in", "SecurityGroupIngress": [ { "IpProtocol": "tcp", "FromPort": "22", "ToPort": "22", "CidrIp": "0.0.0.0/0" }, { "IpProtocol": "tcp", "FromPort": { "Ref": "WebServerPort" }, "ToPort": { "Ref": "WebServerPort" }, "CidrIp": "0.0.0.0/0" } ] } } } } </code></pre><p>Taking a look at the template, you'll see it gets a bit messy in the user data portion of my instance. Essentially you need to make sure your userdata is compliant with the JSON format. To do that, I used this simple hackish ruby script:</p> <pre><code># cat json_encode.rb #!/usr/bin/ruby require 'rubygems' require 'json' if ARGV.size != 1 puts "Usage: #{$0} &lt;file&gt;" exit 1 end def escape(string) parse = JSON.parse({ 'json' =&gt; string }.to_json) parse['json'] end data = '' File.open(ARGV[0]) {|f| data &lt;&lt; f.read} p escape(data) # ./json_encode.rb combined-userdata.txt ... </code></pre><h2>Manipulating stacks with fog</h2> <p>Now you have a stack template that's fairly powerful. You get to configure a few things, such as what EC2 keypair to use and what instance type you want for the stack. You can even configure what web port will be opened on security group. How do you actually use this data? While Amazon provides command line api tools, I decided to add CloudFormation support to the <a href="https://github.com/geemus/fog" title="Fog">fog gem</a> and use this to create a basic tool for myself. Realistically, you could take fog and build a nice interface to launch or build your own templates using your own chef cookbooks/node attributes. As of this writing, CloudFormation is not included in the released fog gem, so you're going to have to checkout the latest code from github:</p> <pre><code># mkdir projects &amp;&amp; cd projects # git clone git://github.com/geemus/fog.git </code></pre><p>With everything in place, here's a simple script that will allow you to launch a stack:</p> <pre><code>#!/usr/bin/env ruby require 'optparse' require "#{ENV['HOME']}/projects/fog/lib/fog" options = {} parser = OptionParser.new do |opts| opts.banner = "Usage: #{$0} -n STACK_NAME -t TEMPLATE_FILE [options]" options[:name] = nil opts.on('-n', '--name NAME', 'Required name attribute') do |name| options[:name] = name end options[:parameters] = {} opts.on('-p', '--parameters KEY=VALUE', 'Set the parameters for a stack template', '(can be comma separated key value pairs)') do |params| params.split(',').each do |param| k,v = param.split('=') raise "Invalid parameter definition" unless v options[:parameters][k] = v end end options[:template] = nil opts.on('-t', '--template FILE', 'Set the template file for this stack') do |file| options[:template] = file end end parser.parse! [:name, :template].each do |p| raise "Missing required parameter: --#{p}" unless options[p] end config = YAML::load(File.open(ENV['HOME']+'/.fog'))[:default] template_body = '' File.open(options[:template]) {|f| template_body &lt;&lt; f.read} cf = Fog::AWS::CloudFormation.new( :aws_access_key_id =&gt; config[:aws_access_key_id], :aws_secret_access_key =&gt; config[:aws_secret_access_key] ) def aws_params(hash) r = Hash.new c = 1 hash.each do |k,v| r["Parameters.member.#{c}.ParameterKey"] = k r["Parameters.member.#{c}.ParameterValue"] = v c += 1 end r end cf.create_stack(options[:name], {'TemplateBody' =&gt; template_body}.merge!(aws_params(options[:parameters]))) </code></pre><p>You can use it as follows:</p> <pre><code># ./create-stack.rb --name drupal --parameters KeyName=allan,InstanceType=m1.large --template drupal_dev.template </code></pre><p>This will upload your template definition and have Amazon read and launch the various resources that you have defined. As these resources are launched, you can get status messages using the DescribeStackEvents API call. Again, here's a simple script that's using fog to pull down these messages:</p> <pre><code>#!/usr/bin/env ruby require 'rubygems' require 'yaml' require "#{ENV['HOME']}/projects/fog/lib/fog" if ARGV.size != 1 puts "Usage: #{$0} &lt;stack name&gt;" exit 1 end config = YAML::load(File.open(ENV['HOME']+'/.fog'))[:default] cf = Fog::AWS::CloudFormation.new( :aws_access_key_id =&gt; config[:aws_access_key_id], :aws_secret_access_key =&gt; config[:aws_secret_access_key] ) events = cf.describe_stack_events('StackName' =&gt; ARGV[0]).body['Events'] events.each do |event| puts "Timestamp: #{event['Timestamp']}" puts "LogicalResourceId: #{event['LogicalResourceId']}" puts "ResourceType: #{event['ResourceType']}" puts "ResourceStatus: #{event['ResourceStatus']}" puts "ResourceStatusReason: #{event['ResourceStatusReason']}" if event['ResourceStatusReason'] puts "--" end </code></pre><p>And what the output will look like:</p> <pre><code># ./describe_stack_events.rb drupal Timestamp: 2011-03-02T17:13:08Z LogicalResourceId: drupal ResourceType: AWS::CloudFormation::Stack ResourceStatus: CREATE_COMPLETE -- Timestamp: 2011-03-02T17:13:05Z LogicalResourceId: web1 ResourceType: AWS::EC2::Instance ResourceStatus: CREATE_COMPLETE -- Timestamp: 2011-03-02T17:12:27Z LogicalResourceId: web1 ResourceType: AWS::EC2::Instance ResourceStatus: CREATE_IN_PROGRESS -- Timestamp: 2011-03-02T17:12:27Z LogicalResourceId: web ResourceType: AWS::EC2::SecurityGroup ResourceStatus: CREATE_COMPLETE -- Timestamp: 2011-03-02T17:12:23Z LogicalResourceId: web ResourceType: AWS::EC2::SecurityGroup ResourceStatus: CREATE_IN_PROGRESS -- Timestamp: 2011-03-02T17:12:21Z LogicalResourceId: drupal ResourceType: AWS::CloudFormation::Stack ResourceStatus: CREATE_IN_PROGRESS ResourceStatusReason: User Initiated -- </code></pre><p>Again you could simply use the provided Amazon tools, as they are probaly more feature-complete, but sometimes you want to be able to query for specific bits of data. Learning how to use fog will save you tons of time compared to parsing data that comes out of the Amazon tools. Fog really lets you do some powerful things with the Amazon APIs and is always gaining more features.</p> <h2>Current shortcommings and summary</h2> <p>This setup has a few issues that I currently don't know how to solve. Let's say you were to launch a stack that included two EC2 instances, a load balancer, and an RDS instance. During the creation phase, you want to import a basic DB schema into your RDS instance. Since you can't get shell on the RDS instance, you will have to use one of your EC2 instances to import the schema. You have a race condition here, how do you make sure your RDS instance is available before your EC2 instance attemps to import data? Right now the only thing I can think of is to have your chef recipe gracefully handle retries. It seems we need a way for resources of a stack to depend on other resources before being launched, or a notification system that could potentially kick off the chef run (SNS?).</p> <p>Either way, I think CloudFormation, chef and fog can be used in conjuction with cloud-init to create quick deployments that are OS independent. This allows you to maintain your automation portions separately from your images, which as a consequence, promotes the use of vanilla images. With these basic ideas, you can easily swap out the <em>node.json</em> file for your own, and replace the user-script in such a way that it checks out your own cookbooks. The possibilities are really endless. Currently cloud-init supports Puppet out of the box, and I'd like to see some chef support here as well.</p> </div></div></div><section class="field field-name-taxonomy-vocabulary-1 field-type-taxonomy-term-reference field-label-above view-mode-rss"><h2 class="field-label">Tags:&nbsp;</h2><ul class="field-items"><li class="field-item even"><a href="/category/tags/chef" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">chef</a></li><li class="field-item odd"><a href="/category/tags/cloudformation" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">CloudFormation</a></li><li class="field-item even"><a href="/category/tags/aws" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">aws</a></li><li class="field-item odd"><a href="/category/tags/fog" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">fog</a></li><li class="field-item even"><a href="/category/tags/cloud-init" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">cloud-init</a></li><li class="field-item odd"><a href="/category/tags/ubuntu" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">Ubuntu</a></li><li class="field-item even"><a href="/category/tags/devops" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">devops</a></li><li class="field-item odd"><a href="/category/tags/sysadmin" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">sysadmin</a></li><li class="field-item even"><a href="/category/tags/ruby" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">Ruby</a></li></ul></section> Thu, 03 Mar 2011 02:08:59 +0000 Allan Feid 73 at https://allanfeid.com Cooking with Chef https://allanfeid.com/content/cooking-chef <div class="field field-name-body field-type-text-with-summary field-label-hidden view-mode-rss"><div class="field-items"><div class="field-item even" property="content:encoded"><p>I wanted to take some time to explain one of my favorite configuration management tools. That is Chef. I've been utilizing it pretty heavily over the passed few months and thought I'd share some of what I learned. I'm by no means an expert, but I've been managing my own <em>chef-server</em> as well as writing plenty of my own recipes.</p> <!--break--> <h2>Part 1: Cookbooks and Recipes</h2> <p>It is important to understand what Recipes and Cookbooks really are. A cookbook is a collection of <a href="http://wiki.opscode.com/display/chef/Recipes">Recipes</a>, <a href="http://wiki.opscode.com/display/chef/Attributes">Attributes</a>, <a href="http://wiki.opscode.com/display/chef/Templates">Templates</a>, <a href="http://wiki.opscode.com/display/chef/Files">Files</a>, and possibly even <a href="http://wiki.opscode.com/display/chef/Resources">Resources</a> and <a href="http://wiki.opscode.com/display/chef/Providers">Providers</a>. On the file system, it looks like this:</p> <pre><code>dpadmin@chef-east:~/cookbooks$ ls haproxy/ attributes metadata.json metadata.rb README.rdoc recipes templates </code></pre><p>Each piece of your cookbook will be under the sub-directory related to what that piece represents. So for example, your template files will go under templates, recipes under recipes, etc. The main pieces of your cookbooks will be the Recipes. This is where you define what will happen when applied to a node. Let's start with something basic, like installing and starting HAproxy:</p> <pre><code>package "haproxy" do action :install end service "haproxy" do supports :restart =&gt; true, :status =&gt; true, :reload =&gt; true action [:enable, :start] end </code></pre><p>More information can be found on <a href="http://wiki.opscode.com/display/chef/Resources">Opscode's Wiki for Resources</a>, which you should get familiar with because it really helps you configure individual resources and understand what is going on. In the above example, you have two Resources: a Package and a Service resource. The package resource does nothing more than install a particular package. The service resource defines a service that we want to control in our recipes. In this case it is <em>haproxy</em> (a load balancer). We tell chef that haproxy supports the restart, status, and reload actions (think init scripts). Then we tell the service to enable itself (<em>chkconfig</em>, <em>update-rc.d</em>, etc), then start the service.</p> <p>Now that we have the required package installed and service configured, we want to start using templates for the configuration files. Here's a template resource for haproxy.cfg:</p> <pre><code>template "/etc/haproxy/haproxy.cfg" do source "haproxy.cfg.erb" owner "root" group "root" mode "644" variables(:servers =&gt; server_list) notifies :reload, resources(:service =&gt; "haproxy") end </code></pre><p>In this resource definition, you can see we specify the full path to the file we will be templating (<em>/etc/haproxy/haproxy.cfg</em>) and the source of the template (<em>haproxy.cfg.erb</em>). Here's a great explanation of <a href="http://wiki.opscode.com/display/chef/Templates#Templates-TemplateLocationSpecificity">template location specificity</a>, but for now we will focus more on the Recipe aspect rather than Templates. The owner, group, and mode directives here specify the permissions the template will have. The variables are exactly what they are called, variables. These variables are passed to the Erubis template engine to be parsed. In this case we are passing a list of servers called <code class="prettyprint">:servers</code> and defining it as the contents of <code class="prettyprint">server_list</code> (an array, for those familiar with programming concepts). Finally, since the template is the configuration for a service, we need to tell our haproxy service to reload its configuration. The notifies directive can be used in any Resource definition and allows you to tell another resource to perform some sort of action. Here we tell haproxy to reload (<em>/etc/init.d/haproxy reload</em>).</p> <h2>Part 2: Templates</h2> <p>Templates are part of Cookbooks, but really need their own section because they can be confusing if you don't know how they are parsed. Take a look at this template for haproxy.cfg:</p> <pre><code>dpadmin@chef-east:~/cookbooks$ cat haproxy/templates/default/haproxy.cfg.erb global log 127.0.0.1 local0 log 127.0.0.1 local1 notice #log loghost local0 info maxconn 4096 #debug #quiet user haproxy group haproxy defaults log global mode http option httplog option dontlognull option forwardfor retries 3 option redispatch maxconn 2000 contimeout 5000 clitimeout 50000 srvtimeout 50000 # Set up application listeners here. listen application *:80 mode http balance roundrobin stats enable stats auth myuser:mypass &lt;% @servers.each do |name,ip| -%&gt; server &lt;%= name %&gt; &lt;%= ip %&gt; check &lt;% end -%&gt; </code></pre><p>For the most part this configuration is pretty static. The only rendered piece is at the bottom:</p> <pre><code> &lt;% @servers.each do |name,ip| -%&gt; server &lt;%= name %&gt; &lt;%= ip %&gt; check &lt;% end -%&gt; </code></pre><p>As you saw in the template definition in the previous section, we pass the variable <code class="prettyprint">:servers</code>, which in the template file is called an instance variable. In Ruby, instance variables are prefixed by an <code class="prettyprint">@</code>. The rest is Ruby syntax, but I will do my best to explain what's going on here. A hash (key-&gt;value store), <a href="http://ruby-doc.org/core/classes/Hash.html">has various methods</a> (functions). In this case we are calling the <code class="prettyprint">each</code> method, which will loop through each key-&gt;value pair in the hash. The <code class="prettyprint">|name,ip|</code> part gets assigned the key and hash values, then we output these values by calling them directly inside the loop. This allows us to generate a final product that looks like this:</p> <pre><code> server web2.example.com 10.204.39.249 check server web1.example.com 10.203.83.213 check </code></pre><p>As you can see, our <code class="prettyprint">@servers</code> hash had two elements. The values of each element were as follows:</p> <pre><code>{ "web2.example.com" =&gt; "10.204.39.249", "web1.example.com" =&gt; "10.203.83.213" } </code></pre><p>So to wrap it up, the Templates get passed variables from Recipes. These variables can be anything including hashes, arrays, strings, and integers. Then the ERB template engine is in charge of parsing the data to come up with our final result.</p> <p>You may be wondering where we got <code class="prettyprint">server_list</code> to begin with. I left it out of the previous examples as it's necessary to understand the guts of Chef before moving towards where data is stored. The real power of Chef is being able to separate all of your data from the actual working parts. This allows easy re-use of the same recipes across any number of different data stores. In this case, we would be talking about each config environment having its own data to be used with any pre-written recipes.</p> <h2>Part 3: Understanding where data is stored</h2> <p>Chef stores a wealth of information that is indexed and search-able from within recipes. When you are writing your recipes, you don't want to keep variables that are statically assigned. This makes them less re-usable and way too specific. So, continuing with the HAproxy example, I will show you where <code class="prettyprint">server_list</code> (the hash of servers to generate our config) came from. In a <a href="http://wiki.opscode.com/display/chef/Data+Bags">Data Bag Item, we have the following </a><a href="http://en.wikipedia.org/wiki/JSON">JSON</a> data:</p> <pre><code>... "haproxy": { "eip": "204.236.219.111", "production_role": "prod-web" }, "common_names": { "lb1": "lb1.example.com", "web1": "web1.example.com", "web2": "web2.example.com" }, ... </code></pre><p>Then, within our recipe file we have the following logic to generate the <code class="prettyprint">server_list</code> hash:</p> <pre><code># get our config data bag item config_info = data_bag_item("configs", node.default[:cid]) # pull out the common_names hash for later use common_names = config_info['common_names'] # assign a new hash that we will store hostnames to ip addresses server_list = Hash.new # basic logging Chef::Log.debug("Searching for role #{config_info['haproxy']['production_role']}") # search for nodes that have the "config_info['haproxy']['production_role']" role applied to them, # loop through each one and assign any matches to the server variable search(:node, "role:#{config_info['haproxy']['production_role']}").each do |server| Chef::Log.debug("Found a server: #{server} IP: #{server.cloud.private_ips.first}") # add to our hash. use the common name for the server as key and the private ip as the value server_list[common_names[server.name]] = server.cloud.private_ips.first Chef::Log.info("Added #{common_names[server.name]} =&gt; #{server_list[common_names[server.name]]}") end </code></pre><p>In this case we retrieve some data from a Data Bag Item. The Data Bag being "configs" and the item being <code class="prettyprint">node.default[:cid]</code>. Here we access a custom attribute called <code class="prettyprint">:cid</code>. So when running this recipe against a group of nodes with the config ID of "example," the Data Bag Item is <em>configs[example]</em>. Now, inside that data bag item, we have a bunch of key-&gt;value pairs (think hashes). We access the information stored under '<em>common_names</em>', '<em>haproxy</em>', and whatever else we want to store. All of this is just being pulled from an arbitrary JSON data store that Chef refers to as a Data Bag.</p> <p>When it comes down to it, what we are doing is searching for any nodes that have the role "prod-web" applied to them. I created this role for any production web servers that example.com will be load balancing between. So when our HAproxy recipe is applied to a different node, it generates the configuration file based off of which servers have that role applied to them. Data Bags are not the only place information can be stored. There are three main places we can store data:</p> <ul><li>Data Bag Items</li> <li>Role Attributes</li> <li>Node Attributes</li> </ul><p>In the HAproxy example, we make use of all three of the above. The "common_names" for each host in a solution and which role to search for are stored in a Data Bag Item. The config identifier (cid) is being stored in a Role Attribute, and our IP address information comes from Node Attributes. All of this data is easily viewable using the knife command line tool.</p> <h3>Data Bags</h3> <p>For Chef, there's two parts to this. There's the Data Bag, then the Data Bag Item. A data bag can store multiple items. Each Item is nothing more than an arbitrary JSON data store. This is a great place to store information that will be used in various recipes. I've been using it as a central configuration for various services. What this means is, I have the configs data bag, and each Item is called by the configs ID. Inside each data store, I keep configuration variables and general data for any recipes that will be applied to nodes in that config's environment. To give you a brief idea, here's what example.com has:</p> <pre><code>{ "ldap": { "bindpw": "xxxxxxxxxxxxxxxxxxx", "binddn": "cn=proxy,ou=services,dc=example,dc=cid", }, "common_names": { "lb1": "lb1.example.com", "web1": "web1.example.com", "web2": "web2.example.com" }, "hosts": { "us-east-1": [ "10.204.39.249 web2.example.com web2", "10.203.83.213 web1.example.com web1", "10.203.29.220 lb1.example.com lb1" ] }, "id": "example", "aws": { "aws_access_key_id": "XXXXXXXXXXXXXXXXXXXX", "aws_secret_access_key_id": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" }, "apache": { "vhosts": { "www.example.com": { "DocumentRoot": "/ebs/sites/example.com" }, ... } }, "haproxy": { "eip": "204.236.219.111", "production_role": "prod-web" } } </code></pre><p>You sort of have to think of this data store as a central place to manipulate the entire environment. Rather than thinking about individual nodes, think of configuring everything in one place. Accessing data from a data bag in a recipe, requires that you use:</p> <pre><code>data = data_bag_item('configs', 'example') puts data['haproxy']['production_role'] # prints prod-web to stdout </code></pre><p>From there you will have a nice hash with all of the JSON data in that data bag item.</p> <h3>Role Attributes</h3> <p>Again stored in JSON, Role Attributes come from your <a href="http://wiki.opscode.com/display/chef/Roles">Role</a> definitions. When you define a role, it will look like this:</p> <pre><code>{ "name": "base", "default_attributes": { "cid": "example" }, "json_class": "Chef::Role", "run_list": [ "recipe[ldap::auth]", "recipe[ldap::sudoers]", "recipe[sshd]", "recipe[zabbix]", "recipe[clients::hosts]", "recipe[clients::hostname]", "recipe[postfix]" ], "description": "", "chef_type": "role", "override_attributes": { } } </code></pre><p>Don't let the JSON make you think creating new roles is difficult. Creating a role with a properly configured knife client is pretty straight forward:</p> <pre><code># knife role create &lt;some_role&gt; </code></pre><p>This will give you a basic role with no run_list and no default_attributes. All I do for this role (base), is define what recipes to run, and set the config ID that we use in various recipes. Accessing the data from these attributes is done the same way as the Node Attributes. Really we can just call them <a href="http://wiki.opscode.com/display/chef/Attributes">Attributes</a>, but because they are technically stored in different places I wanted to cover them separately.</p> <h3>Node Attributes</h3> <p>If you're thinking that this is stored using JSON, you are correct. Each node, upon being bootstrapped, gets populated with a large data store. This contains everything from the contents of /etc/passwd, to the amount of bytes transmitted on eth0. Each time a node checks in with the server, this data gets updated. You can modify any data for a node directly using knife, and to give you an example of the extensive information stored by Chef, here's what the ec2 section looks like:</p> <pre><code> "ec2": { "public_hostname": "ec2-174-129-1.compute-1.amazonaws.com", "placement_availability_zone": "us-east-1a", "block_device_mapping_root": "/dev/sda1", "instance_id": "i-d1535fbb", "instance_type": "m1.large", "local_ipv4": "10.204.39.249", "block_device_mapping_ephemeral0": "/dev/sdb", "reservation_id": "r-940419ff", "public_ipv4": "174.129.1.1", "local_hostname": "ip-10-204-39-249.ec2.internal", "kernel_id": "aki-2407f24d", "hostname": "ip-10-204-39-249.ec2.internal", "ami_id": "ami-6006f309", "userdata": null, "security_groups": [ "web" ], "block_device_mapping_ami": "/dev/sda1", "ami_manifest_path": "(unknown)", "ami_launch_index": "0" }, </code></pre><p>Accessing any of these attributes from recipes can be done in the following manner:</p> <pre><code># the node's name, ex: web1 node.name # the AMI a node is running, ex: ami-6006f309 node.ec2.ami_id # node's uptime, ex: "16 days 16 hours 35 minutes 23 seconds" node.uptime # distro, ex: "ubuntu" node.platform # version of php on the instance, ex: "5.3.2-1ubuntu4.5" node.languages.php.version # gcc version node.languages.c.gcc.version # number of packets sent/received node.network.interfaces.eth0.tx.packets node.network.interfaces.eth0.rx.packets </code></pre><p>As long as you have a node object, you can access any of this data via the object's methods. When you are in a recipe, you can use the variable node as it is defined automatically by Chef.</p> <h2>Further Reading</h2> <ul><li><a href="http://en.wikipedia.org/wiki/JSON">Wikipedia's JSON entry</a></li> <li><a href="http://wiki.opscode.com/display/chef/Resources">Opscode Wiki for Resources</a></li> <li><a href="http://wiki.opscode.com/display/chef/Attributes">Opscode Wiki for Attributes</a></li> <li><a href="http://wiki.opscode.com/display/chef/Knife">Opscode Knife Documentation</a></li> <li><a href="http://wiki.opscode.com/display/chef/Knife+Bootstrap">Opscode Bootstrap Documentation</a></li> </ul></div></div></div><section class="field field-name-taxonomy-vocabulary-1 field-type-taxonomy-term-reference field-label-above view-mode-rss"><h2 class="field-label">Tags:&nbsp;</h2><ul class="field-items"><li class="field-item even"><a href="/category/tags/sysadmin" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">sysadmin</a></li><li class="field-item odd"><a href="/category/tags/ruby" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">Ruby</a></li><li class="field-item even"><a href="/category/tags/chef" typeof="skos:Concept" property="rdfs:label skos:prefLabel" datatype="">chef</a></li></ul></section> Sun, 09 Jan 2011 19:01:56 +0000 Allan Feid 65 at https://allanfeid.com