Monday, June 23, 2014

Chef on Windows: Web Deploy LWRP

This past April I had the pleasure of speaking at #ChefConf 2014 on the topic of Windows Web Server Management With ASP.NET. In that presentation I briefly touched on the topic of creating a Light Weight Resource Provider for Web Deploy. 

Today I thought I would walk you through creating that LWRP. The first thing you need to do when creating a LWRP is create the resource definition. The resource definition defines both the actions of the LWRP as well as the attributes. Our web deploy LWRP needs to support Web Deploys sync verb. I've chosen to use :sync for the name of my action because it corresponds to the domain specific language used by msdeploy.

There are three pieces of information that we want the consumers of our LWRP to be able to change in their recipe. The first is the name of the package being deployed. The second is the destination where the package should be deployed to. And last is any additional parameters that should be passed along to msdeploy.

We're also going to implement the initialize method of the resource in order to set :sync as the default action.  This is not strictly necessary but allows the consumer to not have to set the action in their recipe.

Here's what our resource definition looks like.

your_cookbook/resources/web_deploy.rb

actions :sync

attribute :package, :kind_of => String, :name_attribute => true
attribute :destination, :kind_of => String, :default => "auto"
attribute :parameters, :kind_of => Hash

def initialize(name, run_context=nil)
 super
 @action = :sync
end

Now that we've got our resource defined our next step in creating our Web Deploy LWRP is to create an implementation of our resource. We do this by defining a provider with the same name as the resource we previously defined. One of the really nice things about LWRP's is that Chef takes care of most of the heavy lifting in defining your provider. All we really have to do is implement the actions we declared in our resource.

Our :sync action implementation does 3 main things. First it creates the msdeploy command. This command is exactly what you'd type if you were running the command through a command prompt. Notice as we generate the command that we're setting the package and destination to the attributes with the same name on our new resource.

The second thing our implementation does is append any parameters set on the new resource to the msdeploy command.

The last thing our implementation does is to execute the command using the built in execute resource. It is VERY important to note that this IS NOT idempotent, meaning that this command will get run every time this recipe is run. This is really important to think about when creating your LWRP as you will need to build in the idempotency for implementations that run dynamically generated scripts.

your_cookbook/providers/web_deploy.rb

action :sync do
  msdeploy_cmd = "\"%programfiles%\\IIS\\Microsoft Web Deploy V2\\msdeploy.exe\" "
  msdeploy_cmd << "-verb:sync "
  msdeploy_cmd << "-source:package=\"#{@new_resource.package}\" " unless @new_resource.package.nil?
  msdeploy_cmd << "-dest:\"#{@new_resource.destination}\" " unless @new_resource.destination.nil?

  @new_resource.parameters.each do |name, value|
   msdeploy_cmd << "-setParam:name=\"#{name}\",value=\"#{value}\" "
  end unless @new_resource.parameters.nil?

  Chef::Log.info("MSDeploy Command: #{msdeploy_cmd}")

  execute "webdeploy" do
   command msdeploy_cmd
  end
end

You can build in that idempotency fairly easily in Chef. The first thing you need to do is define an attr_accessor on your resource. Something like attr_accessor :has_run. Then in your provider you need to override the load_current_resource method and perform any check necessary that will validate whether you need to perform your action or not. For example:

def load_current_resource
 ... perform check to see if your action has already been performed.
end

After you have that plumbing in place you can update your action :sync do implementation to wrap your command execution with a check to see if the action has already been run. For example.

 if @new_resource.has_run
  Chef::Log.info("Web Deploy has already been run")
 else
  ... put the msdeploy command execution logic here
 end

Now you have an LWRP which will allow you to idempotently deploy a web deploy package. Using your new LWRP within a recipe is very simple. Here's an example where we get the source package and additional parameters from an attribute. Notice that we don't set the destination parameter because our resource definition includes a default.

your_cookbook_web_deploy "#{node['webapp']['local_directory']}/#{node['webapp']['package']}" do
  parameters node['webapp']['parameters']
end


No comments:

Post a Comment