Larry Franks and Brian Swan on Open Source and Device Development in the Cloud
While Brian started discussing gathering Windows Azure diagnostics from PHP on Monday, I’m going to jump straight into scaling Windows Azure from Ruby and discuss gathering diagnostic information in a later post. Diagnostic information is great if you want to determine when to scale, but I think first we need to talk about how to accomplish scaling.
Scaling out an application hosted on Windows Azure is pretty easy; it’s just updating a field in a configuration file that tells Windows Azure how many instances to create for the role that hosts your application, and then uploading the new configuration file so it takes effect. The service configuration file is documented at http://msdn.microsoft.com/en-us/library/windowsazure/ee758710.aspx, and the following is an example of what one looks like:
<?xml version="1.0" encoding="utf-16"?> <ServiceConfiguration xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" serviceName="" osFamily="1" osVersion="*" xmlns="http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceConfiguration"> <Role name="HelloRole"> <ConfigurationSettings> <Setting name="Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString" value="DefaultEndpointsProtocol=https;AccountName=storageaccountname;AccountKey=storageaccountkey /> </ConfigurationSettings> <Instances count="1" /> <Certificates /> </Role> <Role name="ByeRole"> <ConfigurationSettings> <Setting name="Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString" value="UseDevelopmentStorage=true" /> </ConfigurationSettings> <Instances count="1" /> <Certificates /> </Role> </ServiceConfiguration>
See the count attribute on the Instances element? That’s what we want to change to control how many instances Windows Azure creates for a given role. There are a couple things we need to know in order to accomplish this though. We need to know the role name, the hosted service name, and the deployment slot that the role is deployed into. You can get this information through the Windows Azure portal, or using a tool such as waz-cmd (https://github.com/smarx/waz-cmd/blob/master/README.md.) Once you have this information, you need to perform the following steps:
You might also want to look at the role status to check that all your roles are in a 'Ready' state after changing the instance count, since it may take a few minutes for Windows Azure to spin up a new role.
Here’s an example of what you might do to accomplish this using Ruby:
require 'httparty'require 'openssl'require 'base64'require 'nokogiri'class Hash def seek(*_keys_) last_level = self sought_value = nil _keys_.each_with_index do |_key_, _idx_| if last_level.is_a?(Hash) && last_level.has_key?(_key_) if _idx_ + 1 == _keys_.length sought_value = last_level[_key_] else last_level = last_level[_key_] end else break end end sought_value end endclass AzureRole def initialize(params) @subscription = params[:subscription] @service_name = params[:service_name] @role_name = params[:role_name] @deployment_slot = params[:deployment_slot] @pem_file = File.read(params[:pem_path]) end # return the status def status role_state = query_azure.seek('Deployment','RoleInstanceList','RoleInstance').reject do |entry| entry['InstanceStatus']=='Ready' end if role_state.empty? then 'Ready' else 'Not Ready' end end def instances current_config=Nokogiri::XML(Base64.decode64(query_azure["Deployment"]["Configuration"])) current_config.xpath(role_xpath).attribute('count').value.to_i end def instances=(other) current_config = get_configuration # set the new value current_config.xpath(role_xpath).attribute('count').value = other.to_s new_configuration = Nokogiri::XML::Builder.new(:encoding => 'utf-8') { |xml| xml.ChangeConfiguration('xmlns' => 'http://schemas.microsoft.com/windowsazure') { xml.Configuration Base64.encode64(current_config.to_xml(:encoding=>'utf-8')).rstrip } }.to_xml # post the new value query_azure('post', new_configuration) end private def role_xpath "//xmlns:Role[@name='#{@role_name}']/xmlns:Instances" end # query the azure REST APIs def query_azure(verb = 'get', body = '') request_url = "https://management.core.windows.net/#{@subscription}/services/hostedservices/#{@service_name}/deploymentslots/#{@deployment_slot}" request_options = {:headers => {'x-ms-version' => '2011-08-01', 'Content-Type' => 'application/xml'}, :format => :xml, :body => body, :pem => @pem_file } request_url = "#{request_url}/?comp=config" if verb == 'post' req = verb=='get' ? HTTParty.get(request_url, request_options) : HTTParty.post(request_url, request_options) resp = req.parsed_response # error or result? if (200..299).include?(req.code) then resp else raise "#{resp['Error']['Code']}: #{resp['Error']['Message']}" end endend
Here's an explanation of how this works.
There are a few values that need to be passed in; your subscription ID, the service name. the role name, the deployment name, and the file containing your private key, which is used to authenticate to Windows Azure. The AzureAdmin class expects these parameters as a hash, so creating a new object would look like this: foo=AzureAdmin.new(:subscription=>'subscription guid', :service_name=>'myawesomeservice', :role_name=>'winning', :deployment_slot=>'staging', :pem_path=>'c:\temp\certificate.pem').
You might be asking "what's a pem file?" It's the file that contains the private key for the certificate I'm using to authenticate to Windows Azure. I generated it using the following OpenSSL commands:
openssl req -x509 -nodes -days 365 -newkey rsa:1024 -keyout mycert.pem -out mycert.pem
openssl x509 -inform pem -in mycert.pem -outform der -out mycert.cer
The first line generates the private key .pem file, which you use to authenticate to Windows Azure REST APIs, and the second generates the .cer file that is uploaded to the Windows Azure portal so that it will recognize you when you authenticate.
This method builds the URL, queries the service, and returns the result parsed into a hash.
def query_azure(verb = 'get', body = '') request_url = "https://management.core.windows.net/#{@subscription}/services/hostedservices/#{@service_name}/deploymentslots/#{@deployment_slot}" request_options = {:headers => {'x-ms-version' => '2011-08-01', 'Content-Type' => 'application/xml'}, :format => :xml, :body => body, :pem => @pem_file } request_url = "#{request_url}/?comp=config" if verb == 'post' req = verb=='get' ? HTTParty.get(request_url, request_options) : HTTParty.post(request_url, request_options) resp = req.parsed_response # error or result? if (200..299).include?(req.code) then resp else raise "#{resp['Error']['Code']}: #{resp['Error']['Message']}" end end
This method pulls out the status from the role instance(s). If all instances are 'Ready' then it returns ready, otherwise 'Not Ready'. I didn't really want to dip into returning an array containing the status of all possible results for the user to deal with, but that might be something to investigate if you need something that gives you more granular information.
def status role_state = query_azure.seek('Deployment','RoleInstanceList','RoleInstance').reject do |entry| entry['InstanceStatus']=='Ready' end if role_state.empty? then 'Ready' else 'Not Ready' end end
Here I'm just returning and setting the instance value. Returning it is trivial, and just pulls in the value from the decoded XML. Setting it is a bit more involved as you have to build the XML response message, base64 encode the config, and pass it to query_azure as a post.
def instances current_config=Nokogiri::XML(Base64.decode64(query_azure["Deployment"]["Configuration"])) current_config.xpath(role_xpath).attribute('count').value.to_i end def instances=(other) current_config = get_configuration # set the new value current_config.xpath(role_xpath).attribute('count').value = other.to_s new_configuration = Nokogiri::XML::Builder.new(:encoding => 'utf-8') { |xml| xml.ChangeConfiguration('xmlns' => 'http://schemas.microsoft.com/windowsazure') { xml.Configuration Base64.encode64(current_config.to_xml(:encoding=>'utf-8')).rstrip } }.to_xml # post the new value query_azure('post', new_configuration) end
This little piece of code patches the Hash class to add a seek method. This makes it easier to deal with the nested structures returned by HTTParty. For example, instead of doing query_azure['Deployment']['RoleInstanceList']['RoleInstance'].... I can instead do query_azure.seek('Deployment','RoleInstanceList','RoleInstance'). Looks a little cleaner. Much thanks to Corey O'Daniel, who's blog I found this on (http://coryodaniel.com/index.php/2009/12/30/ruby-getting-deeply-nested-values-from-a-hash-in-one-line-of-code/).
class Hash def seek(*_keys_) last_level = self sought_value = nil _keys_.each_with_index do |_key_, _idx_| if last_level.is_a?(Hash) && last_level.has_key?(_key_) if _idx_ + 1 == _keys_.length sought_value = last_level[_key_] else last_level = last_level[_key_] end else break end end sought_value end end
An example of using this would be:
test=AzureRole.new(:subscription => 'subscription id GUID', :service_name => 'myservice', :role_name=> 'myrole', :deployment_slot => 'staging', :pem_path=>'c:\temp\mycert.pem')
test.instance=test.instance+1
puts test.status
As the example code above demonstrates, it's not hard to tell Windows Azure to scale a hosted application; just one entry in a config file. And it's pretty easy to accomplish from a Ruby application using HTTParty for the REST calls, Nokogiri for XML, and OpenSSL for certificates. There are other gems that you can use to accomplish this, and there are probably cleaner ways to accomplish it using Ruby. If you have suggestions on how I might improve this code, or if there's already a better example of how to accomplish this that I've missed, let me know.