Using Azure Automation with only REST API’s

Azure Automation is the next step in Microsoft’s Orchestration path.  Using predefined workflows / with REST/Odata/WebAPI has been a core component of Orchestrator, Service Management Automation and the Cloud based Azure Automation.

This post is aimed at developers to give an overview of purely using Web API’s for calling runbooks and retrieving results.  In most situations it is a lot easier to use Azure Devkit libraries or PowerShell cmdlets but a major strength of Web API’s is that they require no additional client files and are completely technology agnostic. 

Using Azure Automation runbooks with REST/Odata/WebAPI isn’t well documented at this stage.   This is not intended to be a full walkthrough but it should help in giving enough tips as how to move forward.

Setting Up Azure

This example relies on an Azure Resource Manager environment being created for the hosted runbook.  It also requires an Azure hosted Active Directory account (which will be used for initiating and reading results from the runbook).

The created user account needs to be assigned roles for “Reader” and “Automation Operator”

The Azure Automation runbook in this example is a single line that simply writes “Hello World”.  Not particularly useful as runbooks go!

The REST/ODATA/WebAPI Process

Retrieving results from the custom runbook will take place under the following steps:

  1. Request an Authorisation Token
  2. Start the Runbook
  3. Poll for runbook completion
  4. Return the Runbook results.

The following code snippets will use a standard set of variables.

#// Constants
[string]$ARMResource           = "https://management.core.windows.net/"
#PowerShell ClientID
[string]$ClientId              = "1950a258-227b-4e31-a9cf-717495945fc2" 
 
#// Unique Azure Account Information
[string]$ResourceGroup         = "Sandpit"
[string]$AutomationAccount     = "DevAutomation"
[string]$adTenant              = "laurie.onmicrosoft.com"
[string]$SubscriptionId        = "53f3177b-40bb-4696-83f2-df1feEXAMPLE"
 
#// User Credentials
[string]$UserName              = "automationaccount@$($adTenant)"
[string]$Password              = "ExamplePassword!"
 
[string]$TokenEndpoint = "https://login.windows.net/$($adTenant)/oauth2/token"

 

The client ID being used is a common ID utilised by PowerShell.

Resource Groups and Automation Accounts and your AD tenant are created when you create your Directory services and your runbook.

1.      Requesting a token

A token is obtained against a valid AD user Account and password by sending a POST request containing the username and password.

[string]$UserAuthPayload = "resource=$($resourceAppIdURI)&client_id=$($clientId)"`
  +"&grant_type=password&username=$($userName)&password=$($password)&scope=openid";
 
 
#// Payload strings must be websafe
 
[System.Reflection.Assembly]::LoadWithPartialName(“System.Web”) | out-null
 
 
 
#// Construct a Token request payload and request header - then submit via POST
 
$payload =@"
   $($UserAuthPayload),
   $([System.Web.HttpUtility]::UrlEncode($ARMResource)),
   $([System.Web.HttpUtility]::UrlEncode($UserName)),
   $([System.Web.HttpUtility]::UrlEncode($Password)),
"@
 
$Header = @{
 "Content-Type" = "application/x-www-form-urlencoded";
}
 
$AuthResult = Invoke-RestMethod -Uri $TokenEndpoint -Method Post -body $Payload -Headers $Header

The returned object (if successful) holds properties for the token type & token itself.  These values are used within the Authorisation header in the next request when the new Automation Job is created.

   "Authorization" = "$($authResult.token_type) $($authResult.access_token)"

 

2.      Starting a new Runbook Job

New runbook jobs are created by submitting a JSON object to the listening Automation jobs service.  We need to create a random GUID for the new job.  We will also specify the runbook name (and any additional parameters) within the body of the request.

The “HelloWorld” runbook doesn’t have any input parameters but the name is included as a runbook property within the JSON document.   Notice how the ”token type” and “access token” properties from the previous request are used within the authorisation header.

#create a new, random Job ID
$JobId = [GUID]::NewGuid().ToString()
 
$RequestHeader = @{
  "Content-Type" = "application/json";
  "x-ms-version" = "2013-08-01";
  "Authorization" = "$($authResult.token_type) $($authResult.access_token)"
}
 
#// Declare the body of the POST request - including any input
#// parameters that may be required
 
$Body = @"
        {
           "properties":{
           "runbook":{
               "name":"HelloWorld"
           },
           "parameters":{
           }
          }
       }
"@
 
$URI = “https://management.azure.com/subscriptions/$($SubscriptionId)/"`
     +"resourceGroups/$($ResourceGroup)/providers/Microsoft.Automation/"`
     +"automationAccounts/DevAutomation/jobs/$($jobid)?api-version=2015-10-31"
 
$Response = Invoke-RestMethod -Uri $URI -Method Put -body $body -Headers $requestHeader

Tip – PowerShell Convertto-Json

The example uses a formatted text file for demonstrating a JSON request.  With simple requests the approach works well.  PowerShell’s ConvertTo-JSON and Convertfrom-JSON cmdlets are arguably a cleaner way to manipulate JSON.  The JSON text representation of :

… Is represented by a custom PowerShell object of:

The same JSON output can be constructed by forming a multi-level, custom PowerShell object and using the Convertfrom-JSON cmdlet.  I could have relatively easily created a unique object for the runbook job and converted to JSON to achieve the same result as using the more basic text element shown above.  The custom object would be of more use for more complex programming.

… However, – this example is about showing the Automation process so I’ll stick with my hardcoded JSON text format!

3.      Poll for Runbook Completion

As the runbook job waits for provisioning, it’s “Provisioning State” property may be queried with a GET request (using the same authorisation headers).

$URI  = "https://management.azure.com/subscriptions/$($subscriptionId)/"`
      +"resourceGroups/$($ResourceGroup)/providers/Microsoft.Automation/"`
      +"automationAccounts/$($AutomationAccount)/Jobs/"`
      +"$($JobId)?api-version=2015-10-31"
 
$doLoop = $true
While ($doLoop) {
   $job = Invoke-RestMethod -Uri $URI -Method GET -Headers $RequestHeader
   $status = $job.properties.provisioningState
   write-output "      Provisioning State = $($status)"
   $doLoop = (($status -ne "Succeeded") -and 
   ($status -ne "Failed") -and ($status -ne "Suspended") -and 
   ($status -ne "Stopped"))
} 

4.      Return the Runbook Results

Assuming runbook processing succeeded, a further GET call can then be made to retrieve the output stream.  Note that the stream type is queried within the URI.

$URI  = "https://management.azure.com/subscriptions/$($subscriptionId)/"`
      +"resourceGroups/$($ResourceGroup)/providers/Microsoft.Automation/"`
      +"automationAccounts/$($AutomationAccount)/jobs/$($jobid)/"`
      +"streams?$filter=properties/streamType%20eq%20'Output'&api-version=2015-10-31" 
 
$response = Invoke-RestMethod -Uri $URI -Method GET -Headers $requestHeader
 
"+----- Result"
($Response.value).properties.summary

 

 

My thanks to David Ebbo for demonstrating token generation for Azure.  Have a look at his community published Authentication Helper for a a deeper understanding of the authentication process.

 

 

Full Script - Below

<#
  Example to demonstrate Web API hooks for Azure Automation Workflows
#>
 
#// Constants
[string]$ARMResource           = "https://management.core.windows.net/"
#PowerShell ClientID
[string]$ClientId              = "1950a258-227b-4e31-a9cf-717495945fc2" 
 
#// Unique Azure Account Information
[string]$ResourceGroup         = "Sandpit"
[string]$AutomationAccount     = "DevAutomation"
[string]$adTenant              = "laurie.onmicrosoft.com"
[string]$SubscriptionId        = "53f3177b-40bb-4696-83f2-df1feEXAMPLE"
 
#// User Credentials
[string]$UserName              = "automationaccount@$($adTenant)"
[string]$Password              = "ExamplePassword!"
 
[string]$TokenEndpoint = "https://login.windows.net/$($adTenant)/oauth2/token"
 
#// +--------------------
#// Construct the Payload for receiving a Token
#// This Example uses a username but could also authenticate against a Certificate
#// 
#// +--------------------
 
[string]$UserAuthPayload = "resource=$($resourceAppIdURI)&client_id=$($clientId)"`
  +"&grant_type=password&username=$($userName)&password=$($password)&scope=openid";
 
#// Payload strings must be websafe
 
[System.Reflection.Assembly]::LoadWithPartialName(“System.Web”) | out-null
 
#// Construct a Token request payload and request header - then submit via POST
 
$payload =@"
   $($UserAuthPayload),
   $([System.Web.HttpUtility]::UrlEncode($ARMResource)),
   $([System.Web.HttpUtility]::UrlEncode($UserName)),
   $([System.Web.HttpUtility]::UrlEncode($Password)),
"@
 
$Header = @{
 "Content-Type" = "application/x-www-form-urlencoded";
}
 
$AuthResult = Invoke-RestMethod -Uri $TokenEndpoint -Method Post -body $Payload -Headers $Header
 
#// +--------------------
#// Construct Azure Automation Runbook Invocation
#// The proviously returned token is used in the Autorisation Header
#// 
#// +--------------------
 
$RequestHeader = @{
  "Content-Type" = "application/json";
  "x-ms-version" = "2013-08-01";
  "Authorization" = "$($authResult.token_type) $($authResult.access_token)"
}
 
#create a new, random Job ID
$JobId = [GUID]::NewGuid().ToString()
 
$URI =  “https://management.azure.com/subscriptions/$($SubscriptionId)/"`
     +"resourceGroups/$($ResourceGroup)/providers/Microsoft.Automation/"`
     +"automationAccounts/DevAutomation/jobs/$($jobid)?api-version=2015-10-31"
 
#// Declare the body of the POST request - including any input
#// parameters that may be required
 
$Body = @"
        {
           "properties":{
           "runbook":{
               "name":"HelloWorld"
           },
           "parameters":{
           }
          }
       }
"@
 
$Response = Invoke-RestMethod -Uri $URI -Method Put -body $body -Headers $requestHeader
 
#// +--------------------
#// Check the submitted job status
#// & poll until the job is completed
#// 
#// +--------------------
 
 
$URI  = "https://management.azure.com/subscriptions/$($subscriptionId)/"`
      +"resourceGroups/$($ResourceGroup)/providers/Microsoft.Automation/"`
      +"automationAccounts/$($AutomationAccount)/Jobs/"`
      +"$($JobId)?api-version=2015-10-31"
 
$doLoop = $true
While ($doLoop) {
   $job = Invoke-RestMethod -Uri $URI -Method GET -Headers $RequestHeader
   $status = $job.properties.provisioningState
   write-output "      Provisioning State = $($status)"
   $doLoop = (($status -ne "Succeeded") -and 
   ($status -ne "Failed") -and ($status -ne "Suspended") -and 
   ($status -ne "Stopped"))
}
 
#// +--------------------
#// Retrieve the Output stream from the Runbook Job
#// 
#// +--------------------
 
$URI  = "https://management.azure.com/subscriptions/$($subscriptionId)/"`
      +"resourceGroups/$($ResourceGroup)/providers/Microsoft.Automation/"`
      +"automationAccounts/$($AutomationAccount)/jobs/$($jobid)/"`
      +"streams?$filter=properties/streamType%20eq%20'Output'&api-version=2015-10-31" 
 
$response = Invoke-RestMethod -Uri $URI -Method GET -Headers $requestHeader
 
"+----- Result"
($Response.value).properties.summary