Chef Provisioning example

Why provisioning?

Deploying infrastructure to the cloud is something you want to automate. The ability to create machines and other infrastructure components programmatically is a powerful enabler. But to do it right, you will probably need an orchestrator. A tool that will help you manage your Infra creation, will take care of flow and inter-dependencies. Working with OpenStack based internal cloud I have evaluated some tools that can do the trick. In this post i want to talk about Chefs solution to the orchestrating challenge - Chef provisioning.

Why Chef provisioning?

Chef provisioning does have some drawbacks comparing to other tools i have used. It is not as feature reach and flexible. But, if you use chef for your configuration management and want to maintain the same way of modeling your world (that is: use recipes inside cookbooks, roles, data-bags etc...) You may want to consider chef provisioning. Using openstack as my cloud provider, I am using the fog driver of chef provisioning. So by having some basic ruby skills, knowing how to write chef resources and providers, and learning some fog - I am able to do mostly everything. I love the opportunity to mange my orchestrating flow without leaving the chef development environment and concepts and without the need to learn another tool.

One of the best feature of chef provisioning is that every VM you provision will be bootstrap and configure to use your chef server. You can provide the provisioning code with some runlist and trigger the next phase (Configuring your server, 'converging' in chef lingo) without the need to do anything or write any 'glue' code.

A basic use case:

In my evaluation POC I needed to provision an instance in an openstack cloud, provide it with a floating IP and a chef role and have chef do the rest. The recipe that does that can run on any machine that have chef client on. So i used my workstation for the development stage. After the code is ready you can create a dedicated server to be your provisioning server or run it as jenkins job. Since it is a chef recipe, you can overwrite attributes using roles or environments and all this chef stuff.

Preliminary setting: Fog and openstack

As i was saying, I used my workstation as the client running the provisioning recipe. I have a open.rc file that set up al environment variables needed to communicate with my cloud. This file must be sourced on every terminal session before i can run the recipe. Having that, I could use the ENV variables to set up my provisioning fog driver to talk to my cloud. The exact setting may very depending on your openstack setup. in my case - I add this lines to my client.rb file:

driver 'fog:OpenStack'
driver_options :compute_options => {
                                     :openstack_auth_url => "#{ENV[ 'OS_AUTH_URL' ]}/auth/tokens",
                                     :openstack_username => ENV[ 'OS_USERNAME' ],
                                     :openstack_domain_name   => ENV[ 'OS_USER_DOMAIN_NAME' ],
                                     :openstack_api_key   => ENV[ 'OS_PASSWORD' ],
                                     :openstack_project_name => ENV[ 'OS_PROJECT_NAME']
                                    }

The recipe

I created a cookbook called 'provision' in my chef developing repo and wrote a machine resource in the default recipe. With a default attribute file to define defaults for the attributes i used. The machine resource, being a chef resource and hence idempotent, will not create an instance if one with same name is already exist (In this case it will modify the existing instance if needed or do nothing). Since i wanted my recipe to create a new instance every run - i add a timestamp postfix to the machine name. So my code looked like this:

timestamp = Time.now.to_i
machine_name = "#{node[:provision][:machine][:name]}_#{timestamp}"

machine machine_name do
    machine_options({
        bootstrap_options: {
            :flavor_ref => node[:provision][:flavor],
            :image_ref =>  node[:provision][:image][:id],
            :nics => [{ :net_id => node[:provision][:internal_network][:id]}], 
            :key_name => 'mio-keypair',
            :floating_ip_pool => node[:provision][:external_network][:name]
        },
        :ssh_username => node[:provision][:image][:default_user]
    })
    ranlist node[:provision][:runlist]
end

Every image in my openstack environment have a default user that have my public key installed on. I provided this info to chef to enable it to bootstrap the newly created instance. It worked very well as long as i had available IPs in my external network. But, I found out that the machine resource has a limitation. If no there is no IP available in the network that was defined as the floating ip pool, the attachment of floating ip is failing and failing the run. I found no way to make sure that there is an available IP. So i wrote one ...

Floating IP hack

I wrote it as a resource provider so it will be easy to reuse. In my cookbook repository, in the resource folder, I created a file called: fip.rb, this file lists the 'interface' of my provider. I needed three actions: create, delete, and createifnone. The last one will be the one i will use in my recipe. To make it reusable, I add all the openstack configuration attributes as properties so one can use it outside my cookbook.

The file looked like this:

actions :create_if_none, :create, :delete
default_action :create_if_none
attribute  :openstack_auth_url,     kind_of: String, default: "#{ENV[ 'OS_AUTH_URL' ]}/auth/tokens"
attribute  :openstack_username,      kind_of: String, default: ENV[ 'OS_USERNAME' ]
attribute  :openstack_project_name,  kind_of: String, default: ENV[ 'OS_PROJECT_NAME' ]
attribute  :openstack_api_key,       kind_of: String, default: ENV[ 'OS_PASSWORD' ]
attribute  :openstack_domain_name,   kind_of: String, default: ENV[ 'OS_USER_DOMAIN_NAME' ]
attribute  :name,                kind_of: String,  name_attribute: true

Then, the provider itself was in the libraries folder in a file called: fip_provider. (I use a mash of LWRP and HWRP):

require 'fog/openstack'

class Chef
  class Provider
    class Create_ip < Chef::Provider

        def whyrun_supported?
            true
        end

        provides :provision_fip if Chef::Provider.respond_to?(:provides)

        def load_current_resource

        @service = Fog::Network::OpenStack.new(
            :openstack_auth_url => new_resource.openstack_auth_url, 
            :openstack_username => new_resource.openstack_username, 
            :openstack_domain_name   => new_resource.openstack_domain_name, 
            :openstack_api_key   => new_resource.openstack_api_key, 
            :openstack_project_name => new_resource.openstack_project_name
        )
            @current_ip_array = @service.list_floating_ips.body['floatingips']
            @fip_pool = new_resource.name
        end

        def action_creat
            result = @service.create_floating_ip(@fip_pool)
            fip = result.body['floatingip']['floating_ip_address']
        end

        def action_create_if_none
            fip = nil
            @current_ip_array.each do |ip|
                if ( ip['status'] == "DOWN" )
                    fip = ip['floating_ip_address']
                    break
                end
            end
            return fip unless fip.nil?

            return action_creat 
        end
    end
  end
end

This code create a fog service object and use it to create a new ip in the create action and to find out if there is an available ip in the create_if_none action. I removed the delete action from the code to make it a little bit shorter. Now i could use it in my recipe and have a new machine even if no floating ip is avialable:

provision_fip node['provision']['external_network']['id'] do
    action :create_if_none
end

I placed the call for the provision_fip before the call to the machine resource and the issue was solved.

Summary

Chef DSL enable you to a lot more than my naive POC. A set of hosts can be created and some properties can be set. Personally, I use the provision code to create the instances and use the server to manage it once created. I found out that the provision framework is not rich enough and some things are missing but everything i needed (as the fip hack in the example above) i could easily add using fog-openstack.