Jonas Stawski

Everything .NET and More

Scaling Out/In with the Azure Management API

The Windows Azure Service Management REST API docs are very well written and informative, but they do lack in examples. In this post I will write a guide on how to use the Rest API to programmatically Scale Out/In (increase/decrease the number of instances).

In order to increase the number of instances of a deployment you must call the Change Deployment Configuration operation and post an updated configuration file to Azure. Therefore, the first thing to do is get the current configuration by calling the Get Deployment operation. After we retrieve the deployment we need to change the configuration file so we can scale out or in. The service returns the configuration file encoded as a base 64 so we abstract the decoding, modification, and re encoding using the UpdateInstanceCount method. After the change we call the ChangeDeploymentConfiguration which uploads the new configuration file to Azure.

   1: public void Scale(string serviceName, string deploymentName, string roleName, int count)
   2: {
   3:     X509Certificate2 cert = GetCertificate(); //from somewhere
   4:     Guid subscriptionId = new Guid("use your subscription guid");
   5:  
   6:     Deployment dep = GetDeployment(subscriptionId, serviceName, deploymentName, cert);
   7:  
   8:     if (dep == null)
   9:     {
  10:         //deployment doesn't exist anymore!
  11:         //TODO: do something
  12:         return;
  13:     }
  14:     string encodedConf = UpdateInstanceCount(dep.Configuration, roleName, count);
  15:     if (encodedConf == null)
  16:     {
  17:         //role doesn't exist anymore!
  18:         //TODO: do something
  19:         return;
  20:     }
  21:  
  22:     ChangeDeploymentConfiguration(subscriptionId, serviceName, deploymentName, encodedConf, false, cert);
  23:  
  24:     return View();
  25: }


Below is the GetDeployment function, which retrieves the Deployment information, including the encoded configuration file.

   1: const string getDeploymentUrl = "https://management.core.windows.net/{0}/services/hostedservices/{1}/deployments/{2}";
   2: const string getDeploymentVersion = "2012-03-01";
   3:  
   4: public static Deployment GetDeployment(Guid subscriptionId, string serviceName, string deploymentName, X509Certificate2 cert)
   5: {
   6:     Uri uri = new Uri(String.Format(getDeploymentUrl, subscriptionId, serviceName, deploymentName));
   7:  
   8:     HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(uri);
   9:     request.Method = "GET";
  10:     request.Headers.Add("x-ms-version", getDeploymentVersion);
  11:     request.ClientCertificates.Add(cert);
  12:     request.ContentType = "application/xml";
  13:  
  14:     XDocument responseBody = null;
  15:     HttpStatusCode statusCode;
  16:     HttpWebResponse response;
  17:     try
  18:     {
  19:         response = (HttpWebResponse)request.GetResponse();
  20:     }
  21:     catch (WebException ex)
  22:     {
  23:         // GetResponse throws a WebException for 400 and 500 status codes
  24:         response = (HttpWebResponse)ex.Response;
  25:     }
  26:     statusCode = response.StatusCode;
  27:     if (response.ContentLength > 0)
  28:     {
  29:         using (XmlReader reader = XmlReader.Create(response.GetResponseStream()))
  30:         {
  31:             responseBody = XDocument.Load(reader);
  32:         }
  33:     }
  34:     response.Close();
  35:     if (statusCode.Equals(HttpStatusCode.OK))
  36:     {
  37:         XNamespace wa = "http://schemas.microsoft.com/windowsazure";
  38:         XElement dep = responseBody.Element(wa + "Deployment");
  39:         Deployment deployment = new Deployment
  40:         {
  41:             Name = dep.Element(wa + "Name").Value,
  42:             Slot = (DeploymentSlot)Enum.Parse(typeof(DeploymentSlot), dep.Element(wa + "DeploymentSlot").Value),
  43:             PrivateID = dep.Element(wa + "PrivateID").Value,
  44:             Status = (DeploymentStatus)Enum.Parse(typeof(DeploymentStatus), dep.Element(wa + "Status").Value),
  45:             Label = dep.Element(wa + "Label").Value,
  46:             Url = new Uri(dep.Element(wa + "Url").Value),
  47:             Configuration = dep.Element(wa + "Configuration").Value,
  48:             Roles = (from r in dep.Element(wa + "RoleInstanceList").Elements(wa + "RoleInstance")
  49:                      select new RoleInstance
  50:                      {
  51:                          RoleName = r.Element(wa + "RoleName").Value,
  52:                          InstanceName = r.Element(wa + "InstanceName").Value,
  53:                          Size = (InstanceSize)Enum.Parse(typeof(InstanceSize), r.Element(wa + "InstanceSize").Value),
  54:                          Status = (InstanceStatus)Enum.Parse(typeof(InstanceStatus), r.Element(wa + "InstanceStatus").Value)
  55:  
  56:                      }).ToList()
  57:         };
  58:  
  59:         return deployment;
  60:     }
  61:     else
  62:     {
  63:         //TODO: return some error
  64:     }
  65:     return null;
  66: }

From the code above you can see we first create the Uri that we will be calling using subscriptionId, serviceName, and deploymentName for a very specific deployment. All those values can be retrieved from previous calls to the Azure Management API and/or the Azure Management Portal. We then go ahead and create a web request using GET, attaching the certificate, and specifying the type as application/xml. If the response is OK then we populate the model object Deployment using LINQ to XML.

Below is the UpdateInstanceCount, which decodes, modifies, and encodes the configuration file.

   1: private static string UpdateInstanceCount(string base64Conf, string roleName, int count)
   2: {
   3:     string conf = GetStringFromBase64String(base64Conf);
   4:  
   5:     XDocument c = XDocument.Parse(conf);
   6:  
   7:     XNamespace wa = "http://schemas.microsoft.com/ServiceHosting/2008/10/ServiceConfiguration";
   8:     XElement serviceConf = c.Element(wa + "ServiceConfiguration");
   9:  
  10:     var role = serviceConf.Elements(wa + "Role").Where(r => r.Attribute("name").Value == roleName).SingleOrDefault();
  11:  
  12:     if (role == null)
  13:         return null;
  14:  
  15:     role.Element(wa + "Instances").SetAttributeValue("count", count);
  16:  
  17:     string encoded = GetBase64StringFromString(serviceConf.ToString());
  18:  
  19:     return encoded;
  20: }
  21:  
  22: private static string GetStringFromBase64String(string base64Str)
  23: {
  24:     byte[] bs = System.Convert.FromBase64String(base64Str);
  25:     string s = System.Text.Encoding.UTF8.GetString(bs);
  26:     return s;
  27: }
  28:  
  29: private static string GetBase64StringFromString(string str)
  30: {
  31:     byte[] toEncodeAsBytes = System.Text.UTF8Encoding.UTF8.GetBytes(str);
  32:     string returnValue = System.Convert.ToBase64String(toEncodeAsBytes);
  33:     return returnValue;
  34: }


We first get a readable string of the base 64 encoded configuration and then create an XDocument so we can query it and change it. After changing the ServiceConfiguration\Role\Instances[count] attribute we base 64 encode the configuration again and return it to the calling method. Below is a sample configuration file:

   1: <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">
   2:   <Role name="mTender">
   3:     <ConfigurationSettings>
   4:       <Setting name="Microsoft.WindowsAzure.Plugins.Diagnostics.ConnectionString" value="UseDevelopmentStorage=true" />
   5:       <Setting name="StorageAccount" value="something" />
   6:     </ConfigurationSettings>
   7:     <Instances count="1" />
   8:     <Certificates />
   9:   </Role>
  10: </ServiceConfiguration>

The last step is calling the ChangeDeploymentConfiguration which looks like this:

   1: const string changeDeploymentConfigurationUrl = "https://management.core.windows.net/{0}/services/hostedservices/{1}/deployments/{2}/?comp=config";
   2: const string changeDeploymentConfigurationVersion = "2012-03-01";
   3:  
   4: public static void ChangeDeploymentConfiguration(Guid subscriptionId, string serviceName, string deploymentName, string base64Configuration, bool treatWarningsAsError, X509Certificate2 cert)
   5: {
   6:     Uri uri = new Uri(String.Format(changeDeploymentConfigurationUrl, subscriptionId, serviceName, deploymentName));
   7:  
   8:     HttpWebRequest request = (HttpWebRequest)HttpWebRequest.Create(uri);
   9:     request.Method = "POST";
  10:     request.Headers.Add("x-ms-version", changeDeploymentConfigurationVersion);
  11:     request.ClientCertificates.Add(cert);
  12:     request.ContentType = "application/xml";
  13:  
  14:     string xmlRequestFormat = @"<?xml version=""1.0"" encoding=""utf-8""?>
  15:                                 <ChangeConfiguration xmlns=""http://schemas.microsoft.com/windowsazure"">
  16:                                    <Configuration>{0}</Configuration>
  17:                                    <TreatWarningsAsError>{1}</TreatWarningsAsError>
  18:                                    <Mode>Auto</Mode>
  19:                                    <ExtendedProperties />
  20:                                 </ChangeConfiguration>";
  21:  
  22:     string xmlRequest = String.Format(xmlRequestFormat, base64Configuration, treatWarningsAsError.ToString().ToLower());
  23:  
  24:     byte[] bs = System.Text.Encoding.UTF8.GetBytes(xmlRequest);
  25:     request.ContentLength = bs.Length;
  26:  
  27:     Stream dataStream = request.GetRequestStream();
  28:     dataStream.Write(bs, 0, bs.Length);
  29:     dataStream.Close();
  30:  
  31:     XDocument responseBody = null;
  32:     HttpStatusCode statusCode;
  33:     HttpWebResponse response;
  34:     try
  35:     {
  36:         response = (HttpWebResponse)request.GetResponse();
  37:     }
  38:     catch (WebException ex)
  39:     {
  40:         // GetResponse throws a WebException for 400 and 500 status codes
  41:         response = (HttpWebResponse)ex.Response;
  42:     }
  43:     statusCode = response.StatusCode;
  44:     if (response.ContentLength > 0)
  45:     {
  46:         using (XmlReader reader = XmlReader.Create(response.GetResponseStream()))
  47:         {
  48:             responseBody = XDocument.Load(reader);
  49:         }
  50:     }
  51:     response.Close();
  52:     if (statusCode.Equals(HttpStatusCode.Accepted))
  53:     {
  54:         XNamespace wa = "http://schemas.microsoft.com/windowsazure";
  55:         //XElement hs = responseBody.Element(wa + "HostedService");
  56:  
  57:  
  58:         return;
  59:     }
  60:     else
  61:     {
  62:         //TODO: error! what do we return?
  63:     }
  64:     return;
  65: }

This code is very similar to the GetDeployment function, but with some major differences. Instead of a GET we have to perform a POST and post what we want to change. The API documentation specifies we need to post an XML that looks like the one from line 14-20. The Configuration node should hold the encoded configuration file we changed earlier. Something very important to note is line 22 where we change the treatWarningAsError boolean into a lower case string. XML is case sensitive and the service is expecting false or true, not False or True. If you fail to lower case it then you will receive an error from the service. We then post the information and check for the Status Code Accepted. Something else to note is that if you upload a configuration file without any modifications you will get a WebException with a Status Code of 400 (Bad Request) with a response body that looks like this:

   1: <Error xmlns="http://schemas.microsoft.com/windowsazure" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
   2:   <Code>BadRequest</Code>
   3:   <Message>No change in settings specified</Message>
   4: </Error>

Please note that this is only sample code and is missing a lot of checks like the one below. It’s only purpose is to demo what steps are needed to scale out or in programmatically in Azure. Feel free to use this code and modified as needed.

Happy programming (and scaling)!

Comments (2) -

Can you by any chance share the Deployment.cs file to make this object, or link me to where I can find this and other Azure classes?

Reply

hypnose erciksonienne
United States hypnose erciksonienne

bon cette question s'adresse aux...

Reply

Add comment

biuquote
Loading