I’ve been working through a method to add Scheduled Deployments with Azure Update Management. The particular problem is that the docs for Update Management don’t reference any kind of programmatic way to add Scheduled Deployments, and while I did find the PowerShell cmdlets (in preview), they don’t allow for use of the new features such as Reboot Control and Dynamic Groups.
After a little bit more investigation, I came across the Azure REST API for “Software Update Configurations”. Using a REST API is new to me but at first glance it seems like it would achieve what I wanted, so I dove in.
GET Configuration
I wanted to test with a GET command first, to make sure that I understood how to use the API correctly and interpret the output.
First I created a simple update deployment in the Portal to test with.
When I began looking at how to make the API call, I found a lot of instruction on creating a Service Principal in Azure Active Directory, in order for my code to authenticate against the Azure REST API and make the calls it needs. In my case I’m building a script to run ad-hoc, not part of an integrated program or code-base that needs repeated and fully automated authentication. My thinking was, “if I’m authenticated to Azure in a PowerShell session (through Login-AzureRMAccount) why can’t I just use that to make the call?”
This exact thing is possible, as I discovered through this PowerShell Gallery submission. After loading and testing with this function, I found that it could successfully use my PowerShell session as the access token for my API call.
Putting that together with the GET example, I ended up with this PowerShell script:
# Define my parameters to the Automation Account
$resourceGroupName = "test-rg"
$automationAccountName = "test-automation"
$SubscriptionId = "subscription id"
$ConfigName = "2018-08-SUG"
# Import the function, contained in a file from the same directory
. .\Get-AzureRmCachedAccessToken.ps1
# Use the function to get the Access Token
$BearerToken = ('Bearer {0}' -f (Get-AzureRmCachedAccessToken))
# Add the Access Token into proper format
$RequestHeader = @{
"Content-Type" = "application/json";
"Authorization" = "$BearerToken"
}
# Build the URI referencing my parameters
$URI = "https://management.azure.com/subscriptions/$($SubscriptionID)/"`
+ "resourceGroups/$resourcegroupname/providers/Microsoft.Automation/"`
+ "automationAccounts/$automationaccountname/softwareUpdateConfigurations/$($ConfigName)?api-version=2017-05-15-preview"
# Use the URI and the Request Header (with access token) and the method GET
$GetResponse = Invoke-RestMethod -Uri $URI -Method GET -header $RequestHeader |
# Define my parameters to the Automation Account
$resourceGroupName = "test-rg"
$automationAccountName = "test-automation"
$SubscriptionId = "subscription id"
$ConfigName = "2018-08-SUG"
# Import the function, contained in a file from the same directory
. .\Get-AzureRmCachedAccessToken.ps1
# Use the function to get the Access Token
$BearerToken = ('Bearer {0}' -f (Get-AzureRmCachedAccessToken))
# Add the Access Token into proper format
$RequestHeader = @{
"Content-Type" = "application/json";
"Authorization" = "$BearerToken"
}
# Build the URI referencing my parameters
$URI = "https://management.azure.com/subscriptions/$($SubscriptionID)/"`
+ "resourceGroups/$resourcegroupname/providers/Microsoft.Automation/"`
+ "automationAccounts/$automationaccountname/softwareUpdateConfigurations/$($ConfigName)?api-version=2017-05-15-preview"
# Use the URI and the Request Header (with access token) and the method GET
$GetResponse = Invoke-RestMethod -Uri $URI -Method GET -header $RequestHeader
This returned output that looked like this:
id : /subscriptions/f745d13d/resourceGroups/test-rg/providers/Microsoft.Autom
ation/automationAccounts/test-automation/softwareUpdateConfigurations/2018-08-SUG
name : 2018-08-SUG
type :
properties : @{updateConfiguration=; scheduleInfo=; provisioningState=Succeeded; createdBy={scrubbed}; error=; tasks=;
creationTime=2018-08-16T14:09:34.773+00:00; lastModifiedBy=;
lastModifiedTime=2018-10-24T03:56:51.02+00:00} |
id : /subscriptions/f745d13d/resourceGroups/test-rg/providers/Microsoft.Autom
ation/automationAccounts/test-automation/softwareUpdateConfigurations/2018-08-SUG
name : 2018-08-SUG
type :
properties : @{updateConfiguration=; scheduleInfo=; provisioningState=Succeeded; createdBy={scrubbed}; error=; tasks=;
creationTime=2018-08-16T14:09:34.773+00:00; lastModifiedBy=;
lastModifiedTime=2018-10-24T03:56:51.02+00:00}
Fantastic! This can be turned into read-able JSON by piping your $GetResponse like this:
$GetResponse | ConvertTo-JSON |
$GetResponse | ConvertTo-JSON
PUT Configuration
Let’s look at what it takes to use the PUT example from the REST API reference. Primarily, the difference is passing in a JSON Body to the “Invoke-RestMethod”, and changing the method to PUT rather than GET.
Before I get to working examples, I ran into problems trying to use the API reference that I want to highlight. I kept getting obscure errors on the “Targets” section of the JSON body that I couldn’t figure out for a while, until I began looking very closely at the output of my GET from an update deployment created in the portal and compared it against what I was trying to do with my JSON.
Something that particularly helped here was piping my “$Get-Response” like this:
$GetResponse | ConvertTo-JSON -Depth 10 |
$GetResponse | ConvertTo-JSON -Depth 10
This converts the output of the RestMethod into JSON, and ensures that the default depth of 5 is overridden so that it expands all the objects within the Targets array.
What I noticed is that the “Targets” object returns a proper structure with a single nested object (“azureQueries”) which itself is a list (as denoted by the square brackets):
"targets": {
"azureQueries": [
{
"scope": [ ],
"tagSettings": {},
"locations": null
}
]
} |
"targets": {
"azureQueries": [
{
"scope": [ ],
"tagSettings": {},
"locations": null
}
]
}
However, this is what the API reference uses as it’s structure:
"targets": [
{
"azureQueries": {
"scope": [ ],
"tagSettings": { },
"locations": null
}
}
] |
"targets": [
{
"azureQueries": {
"scope": [ ],
"tagSettings": { },
"locations": null
}
}
]
Note that the square brackets for the Target object shouldn’t be there, and that they’re missing on the AzureQueries object.
Once this was solved, my very basic “Create” script with static JSON seemed to work.
Working Example
Here’s an example of my full PowerShell script, which creates a recurring schedule for Definition Updates applied against a specific resource group with any VM having the tag = “Client1”.
# Define my parameters to the Automation Account
$resourceGroupName = "test-rg"
$automationAccountName = "test-automation"
$SubscriptionId = "<subscription_id>"
$ConfigName = "MalwareDefinitions"
# Import the function, contained in a file from the same directory
. .\Get-AzureRmCachedAccessToken.ps1
# Use the function to get the Access Token
$BearerToken = ('Bearer {0}' -f (Get-AzureRmCachedAccessToken))
# Add the Access Token into proper format
$RequestHeader = @{
"Content-Type" = "application/json";
"Authorization" = "$BearerToken"
}
$Body = @"
{
"properties": {
"updateConfiguration": {
"operatingSystem": "Windows",
"duration": "PT0H30M",
"windows": {
"excludedKbNumbers": [ ],
"includedUpdateClassifications": "Definition",
"rebootSetting": "Never"
},
"azureVirtualMachines": [
],
"targets":
{
"azureQueries": [{
"scope": [
"/subscriptions/$SubscriptionId/resourceGroups/$resourceGroupName"
],
"tagSettings": {
"tags": {
"ClientID": [
"Client1"
]
},
"filterOperator": "Any"
},
"locations": null
}]
}
},
"scheduleInfo": {
"frequency": "Hour",
"startTime": "2018-10-31T12:22:57+00:00",
"timeZone": "America/Los_Angeles",
"interval": 2,
"isEnabled": true
}
}
}
"@
$URI = "https://management.azure.com/subscriptions/$($SubscriptionId)/"`
+ "resourceGroups/$($resourcegroupname)/providers/Microsoft.Automation/"`
+ "automationAccounts/$($automationaccountname)/softwareUpdateConfigurations/$($ConfigName)?api-version=2017-05-15-preview"
#This creates the update scheduled deployment
$Response = Invoke-RestMethod -Uri $URI -Method Put -body $body -header $RequestHeader |
# Define my parameters to the Automation Account
$resourceGroupName = "test-rg"
$automationAccountName = "test-automation"
$SubscriptionId = "<subscription_id>"
$ConfigName = "MalwareDefinitions"
# Import the function, contained in a file from the same directory
. .\Get-AzureRmCachedAccessToken.ps1
# Use the function to get the Access Token
$BearerToken = ('Bearer {0}' -f (Get-AzureRmCachedAccessToken))
# Add the Access Token into proper format
$RequestHeader = @{
"Content-Type" = "application/json";
"Authorization" = "$BearerToken"
}
$Body = @"
{
"properties": {
"updateConfiguration": {
"operatingSystem": "Windows",
"duration": "PT0H30M",
"windows": {
"excludedKbNumbers": [ ],
"includedUpdateClassifications": "Definition",
"rebootSetting": "Never"
},
"azureVirtualMachines": [
],
"targets":
{
"azureQueries": [{
"scope": [
"/subscriptions/$SubscriptionId/resourceGroups/$resourceGroupName"
],
"tagSettings": {
"tags": {
"ClientID": [
"Client1"
]
},
"filterOperator": "Any"
},
"locations": null
}]
}
},
"scheduleInfo": {
"frequency": "Hour",
"startTime": "2018-10-31T12:22:57+00:00",
"timeZone": "America/Los_Angeles",
"interval": 2,
"isEnabled": true
}
}
}
"@
$URI = "https://management.azure.com/subscriptions/$($SubscriptionId)/"`
+ "resourceGroups/$($resourcegroupname)/providers/Microsoft.Automation/"`
+ "automationAccounts/$($automationaccountname)/softwareUpdateConfigurations/$($ConfigName)?api-version=2017-05-15-preview"
#This creates the update scheduled deployment
$Response = Invoke-RestMethod -Uri $URI -Method Put -body $body -header $RequestHeader