Azure Update Management – Creating Multiple Deployment Schedules with PowerShell

Have you ever been sent an email from a boss or director asking for your thoughts on a possible solution about to be given to a client? Not to long ago I did and now I am sitting here about to blog about some of my experiences while on this project. The project on a whole is a fairly large Azure Governance project dealing with Azure Policy Guest Configuration all the way to Azure Update Management. For this blog I am going to write about my experiences with Azure Update Management. More specifically I am going to write about how I created a PowerShell script that would deploy over 126 unique deployment schedules that could be repeated across multiple subscriptions.

I will be honest about one thing. Up until this engagement my experience with deploying Update Management solutions has all been based off the portal. However, when you are working in an environment as large as this current client, the portal isn’t going to cut it. So I put on my PowerShell pants and started to explore.

The first thing I discovered, there wasn’t many people out there actually deploying Update Management via PowerShell or Azure Cli. Most, if not all blogs and resources are all focused on doing everything through the portal.

So, the first thing I went looking for was the newest commands that I would need to learn. There where some older blogs around using the old AzureRM module. However, I knew there had to be something around the new Azure AZ module. I found some resources, all from Microsoft, and away I went.

The Solution

This process was actually a two part solution. With my deployment schedules, I want configure them to use dynamic Azure queries to create groups based off of Azure VM’s with a specific Tag and Tag value.

Tags and Tag Values and Deployment Schedule Names

The first thing I needed was a plan around these tags. So I came up with a list of tags that would then be associated with the scheduled deployment. The theory behind this was to push across each subscription a standardized tag called “UpdateWindow” with a tag value of “Default.” This would be done via another PowerShell script. Once that Tag was created, someone who was responsible for the management of that subscription would than edit the tag value to match a defined set of tag values.

The client and I created 63 possible tag values the subscription owners could use. Each of these tag values are lined up with 2 deployment schedules, one being a Windows schedule and the other being a Linux schedule. There is a tag for each day of the week up to the first 3 weeks of the month. Each day had 3 times based off of UTC time, the first being 12:00 AM, then 8:00 AM, then 4:00 PM. This would give people a fairly wide range of options on when their VM’s would be patched and rebooted. For a quick example, if I managed 5 VM’s that are in the East US region, I would have 21 options to patch these VM’s at 8:00 PM or 21 options to patch them at 4:00 AM. I could pick the 21 options to patch them at 12:00 PM but more than likely that is during business hours. So if I would to have picked that time period more than likely it would be on Saturday or Sunday.

Below is a small example of the table they can pick from. As you can see, a deployment schedule named 1st Monday UTC0 – Windows Update would represent a tag value called MON1UTCO. This would let you know that this was the Monday, the 1st Monday of the week, and set to UTC0 which represents 12:00 AM UTC time. We included a larger table with various locations around the globe that they could reference to what the actual time would be for them if they selected MON1UTC0. This would repeat for each of the remaining days of the week.

TagDeployment Schedule NameUTC TimeLos Angeles (-7)New York (-4)
MON1UTC01st Monday UTC0 – Windows Update12:00 AM5:00 PM8:00 PM
 1st Monday UTC0 – Linux Update12:00 AM5:00 PM8:00 PM
MON2UTC02nd Monday UTC0 – Windows Update12:00 AM5:00 PM8:00 PM
 2nd Monday UTC0 – Linux Update12:00 AM5:00 PM8:00 PM
MON3UTC03rd Monday UTC0 – Windows Update12:00 AM5:00 PM8:00 PM
 3rd Monday UTC0 – Linux Update12:00 AM5:00 PM8:00 PM
MON1UTC81st Monday UTC8 – Windows Update8:00 AM1:00 AM4:00 AM
 1st Monday UTC8 – Linux Update8:00 AM1:00 AM4:00 AM
MON2UTC82nd Monday UTC8 – Windows Update8:00 AM1:00 AM4:00 AM
 2nd Monday UTC8 – Linux Update8:00 AM1:00 AM4:00 AM
MON3UTC83rd Monday UTC8 – Windows Update8:00 AM1:00 AM4:00 AM
 3rd Monday UTC8 – Linux Update8:00 AM1:00 AM4:00 AM
MON1UTC161st Monday UTC16 – Windows Update4:00 PM9:00 AM12:00 PM
 1st Monday UTC16 – Linux Update4:00 PM9:00 AM12:00 PM
MON2UTC162nd Monday UTC16 – Windows Update4:00 PM9:00 AM12:00 PM
 2nd Monday UTC16 – Linux Update4:00 PM9:00 AM12:00 PM
MON3UTC163rd Monday UTC16 – Windows Update4:00 PM9:00 AM12:00 PM
 3rd Monday UTC16 – Linux Update4:00 PM9:00 AM12:00 PM

Assigning The Tag and Tag Values Script

I am not a PowerShell expert. I can get by with most things. I can create decent enough scripts to get the things I need to get done, done. The following script did exactly that. It got what I need done. There more things I could have added to this script like logging, error control, etc. However, I was happy with it and it worked.

The script itself is simple, it using the Azure AZ module. It has two required variables that need to be configured before running the script. Those are Tenant Id and a Subscription Id.

The script then uses the Connect-AzAccount command along with the -Tenant and -SubscriptionID parameters to login.

I do ask for two more variables but they didn’t need to be changed across subscriptions so I just kept them there with a comment that they could be edited. These two variables are $AzTag and $TagValue. I have them currently configured for “UpdateWindow” and “Default”

I then do a query of all Azure VM’s within this subscription using the following command:

$azurevms = Get-AzVM | Select-Object -ExpandProperty Name

This will get me all Azure VM’s in the current subscription and only return the Name and also exclude the header.

The last section is a smiple foreach loop. This is where I ran the following against each VM that was discovered.

foreach ($azurevm in $azurevms) {
    
Write-Host Adding tag "$aztag" to "$azurevm" with the value of "$tagvalue"

$ResourceGroupName = get-azvm -name $azurevm | Select-Object -ExpandProperty ResourceGroupName
    
    $tags = (Get-AzResource -ResourceGroupName $ResourceGroupName `
                        -Name $azurevm).Tags

    $tags.Add($aztag,$tagvalue)
  
    Set-AzResource -ResourceGroupName $ResourceGroupName `
               -ResourceName $azurevm `
               -ResourceType Microsoft.Compute/virtualMachines `
               -Tag $tags `
               -Force `
               
}

The one thing I want to point out if you don’t specify the .add for your tags variable here, it will replace all existing tags with just this tag. Good thing I practice in an R&D environment. To my coworkers that share that environment, please forgive me for erasing your tags. :)

The foreach statement runs and first sets a variable for the current resource group of the azure VM. This is required later when the script runs the Set-AzResource command. The next step will get the current tags that already exist on that resource. Hence, this is how I don’t overwrite the current tags. It then runs and updates the current $tags variable with the .add command to add the defined Tag and TagValues. The final step is using the Set-AzResource command to set the resource on the current VM.

This is where I could use some error control and logging if I wanted to add it. The script will run, but will throw errors while doing so if the tag already exist. It will also not overwrite the existing tag’s current value.

Here is the complete script. I also have it on a GitHub repository if you want to use it.

###################################
#  This script is will add a new tag and value all Azure VM's running within Subscription.
#  Current Version:  .01000001 01111010 01110101 01110010 01100101
#  Date Written:   8-29-2019
#  Created By:    Kristopher J. Turner (The Country Cloud Boy)
#  Uses the Az Module and not the AzureRM module
#####################################

#  Need Authentication Step if not running within Azure CloudShell
#  If not using Cloudshell use the current version of Azure PowerShell
#  Use the AZ Module and not AzureRM Module

# Variables - Required
$TenantID=""
$SubscriptionID=""

Connect-AzAccount -Tenant $TenantID -SubscriptionId $SubscriptionID


#  Discovery of all Azure VM's in the current subscription.
$azurevms = Get-AzVM | Select-Object -ExpandProperty Name
Write-Host "Discovering Azure VM's.  Please hold...."

#  Variables that can change but really shouldn't.  Unless you want to change these variables then they should be changed. 
$aztag = "UpdateWindow"
$tagvalue = "Default"

foreach ($azurevm in $azurevms) {
    
    Write-Host Adding tag "$aztag" to "$azurevm" with the value of "$tagvalue"
    $ResourceGroupName = get-azvm -name $azurevm | Select-Object -ExpandProperty ResourceGroupName
    
    $tags = (Get-AzResource -ResourceGroupName $ResourceGroupName `
                        -Name $azurevm).Tags

    $tags.Add($aztag,$tagvalue)
  
    Set-AzResource -ResourceGroupName $ResourceGroupName `
               -ResourceName $azurevm `
               -ResourceType Microsoft.Compute/virtualMachines `
               -Tag $tags `
               -Force `
               
}

Write-Host "All tagging is done (and hopfully it worked).  Please exit the ride to your left.  Have a nice day!"

So now I have the tags and tag values in place. I now have to create the deployment schedules using the dynamic groups based off of these tags.

The Fun…. the tears…. Creating the Deployment Schedules

As I mentioned before there wasn’t that many examples of how to deploy deployment schedules using PowerShell. As I kept working away at a solution I would find a few here and there. I did bounce around a few Microsoft Doc resources which ended up leading me the right direction, mostly.

First of all, if you are headed down this route. Get familiar with these three commands.

That last one is a mouthful let me tell you! These three commands are the core commands at deploying multiple deployment schedules.

Along the way I found some examples within the Microsoft Docs. The best place to start is going to each of the Microsoft Doc pages for each command and just looking over everything.

The Script!

So let’s just get into the meat and potatoes here. The core of my script came from the example provided on the page for New-AzAutomationSoftwareUpdateConfiguration. I did do a lot of configuration changes to make it more dynamic for my use. One of the big edits was the way they targeted subscriptions and resource groups. The way they did it didn’t work for me plus it was selecting only one resource group when the scope was created. However, it was a good foundation to start with and to learn about the commands and required parameters, etc.

The first thing is how I referenced the data I needed for all the parameters it was going to ask for. I need this script to be as dynamic as possible. As I mentioned I am not a PowerShell SME. I know what I know and I can get by. I Bing’d and DuckDuckGo’d everything still. I knew I needed an array of some sort to get the data in, that could then be referenced later. I found the following site that pretty helped me build out an array of objects. A quick shout out to Kevin Marquette and his blog “PowerShell: Everything you wanted to know about arrays” This helped me with my big first step.

Building the array

Using Kevin’s example I built an array of objects. This would help me to query the data and use it for variables later in my script. The array consisted of the following values for each object: ScheduleName, OS, DayofWeek, TagName, TagValue, StartTime, and DaysofWeekOccurrence.

# Build the array
$ScheduleConfig = @(
[pscustomobject]@{ScheduleName='1st Monday UTC0 - Windows Update';OS='Windows';DayofWeek='Monday';TagName='UpdateWindow';TagValue='MON1UTC0';StartTime='19:00';DaysofWeekOccurrence='First'}
[pscustomobject]@{ScheduleName='1st Monday UTC0 - Linux Update';OS='Linux';DayofWeek='Monday';TagName='UpdateWindow';TagValue='MON1UTC0';StartTime='19:00';DaysofWeekOccurrence='First'}
)

The formatting of course in WordPress is terrible but that should give an idea of what the array would look like. Except with mine I had 127 custom objects. Each object had a value like the above example.

The one thing that may seem a bit off if you noticed was the StartTime Value. If you noticed that this example showed the first object as being Monday with a UTC0 time, so why does my StartTime value have 19:00 and not 00:00? Great question and I will bring that up toward the end as an issue.

Now, to start my script out I asked for 4 required variables. $AutomationAccount, $ResourceGroupName, $TenantID, and $SubscriptionID. These will be used throughout the script to help make it more dynamic.

After the connection step we have the building of the array as mentioned above. We then go into the meat and potatoes of the this script.

The next 2 lines creates variables that can be used in the upcoming foreach loop.

$scope = "/subscriptions/$((Get-AzContext).subscription.id)"
$QueryScope = @($scope)

The first variable defines the scope that I will feed into a hash table that will be used by the New-AzAutomationUpdateManagementAzureQuery. The scope we are looking for in my example is everything within the subscription. I could get down to resource group and/or resource levels as well. However, that wasn’t needed and I wanted to catch all VM’s within this subscription.

The next two lines is what I used in my foreach loop. I would create two foreach loops based off the OS version of the schedule. There are a few different parameters between the two OS types, so we had to split these out.

$WindowsSchedules = $ScheduleConfig | Where-Object {$_.OS -eq "Windows"}
$LinuxSchedules = $ScheduleConfig | Where-Object {$_.OS -eq "Linux"}

The next step is the foreach loop for the Windows based deployment schedules.

foreach($WindowsSchedule in $WindowsSchedules){

$tag = @{$WindowsSchedule.TagName=$WindowsSchedule.TagValue}
$azq = New-AzAutomationUpdateManagementAzureQuery -ResourceGroupName $ResourceGroupName `
       -AutomationAccountName $AutomationAccount `
        -Scope $QueryScope `
          -Tag $tag

$AzureQueries = @($azq)

$date=((get-date).AddDays(1)).ToString("yyyy-MM-dd")
$time=$WindowsSchedule.Starttime
$datetime= $date + "t" + $time

$startTime = [DateTimeOffset]"$datetime"
$duration = New-TimeSpan -Hours 2




$schedule = New-AzAutomationSchedule -ResourceGroupName $ResourceGroupName `
         -AutomationAccountName $AutomationAccount `
   -Name $WindowsSchedule.ScheduleName `
   -StartTime $StartTime `
   -DayofWeek $WindowsSchedule.DayofWeek `
   -DayofWeekOccurrence $WindowsSchedule.DaysofWeekOccurrence `
   -MonthInterval 1 `
   -ForUpdateConfiguration

New-AzAutomationSoftwareUpdateConfiguration -ResourceGroupName $ResourceGroupName `
      -AutomationAccountName $AutomationAccount `
    -Schedule $schedule `
     -Windows `
    -Azurequery $AzureQueries `
   -IncludedUpdateClassification Critical,Security,Updates,UpdateRollup,Definition `
    -Duration $duration `
    -RebootSetting IfRequired
}

I start out by declaring a $tag variable that will be needed in building the azure query based groups. This data is pulled from the array I built earlier. You can see .TagName and .TagValue from the $WindowsSchedule variable.

$tag = @{$WindowsSchedule.TagName=$WindowsSchedule.TagValue}

The next step is creating an variable for the azure query we are building. This will use the New-AzAutomationUpdateManagementAzureQuery command. It will then use the $automationaccount variable, the recently created $queryscope variable, and our $tag variable to be defined as the $azq variable.

$azq = New-AzAutomationUpdateManagementAzureQuery -ResourceGroupName $ResourceGroupName `
        -AutomationAccountName $AutomationAccount `
        -Scope $QueryScope `
        -Tag $tag

Next, we take the $azq variable and create a newly defined hash table as part of a new variable called $AzureQueries.

$AzureQueries = @($azq)

We then grab the date using Get-Date, add a day to the current date, and then send it to a string to be formatted a certain way. We have to add a day to the current date in order to create the deployment schedules. When creating deployment schedules, they must be at least 5 minutes ahead of the current time.

$date=((get-date).AddDays(1)).ToString("yyyy-MM-dd")

I then created a $time variable that takes the StartTime value from the array I built. The next step is to create a variable that has the newly created date and the time into a format that the -StartTime parameter for the New-AzAutomationSchedule command to understand.

$time=$WindowsSchedule.Starttime
$datetime= $date + "t" + $time

Now we create a final two variables. The $startTime and the $duration.

$startTime = [DateTimeOffset]"$datetime"
$duration = New-TimeSpan -Hours 2

**** Note **** This is the one section that I am still having issues with.

The next step creates the actual schedule that will show under the automaton account. By using the command New-AzAutomationSchedule and including the -ForUpdateConfiguration flag, this will create a specific Schedule that will be linked to the software update configuration created next.

$schedule = New-AzAutomationSchedule -ResourceGroupName $ResourceGroupName `
           -AutomationAccountName $AutomationAccount `
           -Name $WindowsSchedule.ScheduleName `
           -StartTime $StartTime `
           -DayofWeek $WindowsSchedule.DayofWeek `
           -DayofWeekOccurrence $WindowsSchedule.DaysofWeekOccurrence `
           -MonthInterval 1 `
           -ForUpdateConfiguration
  • The -Name parameter of course is just the Display name of the Scheduled Deployment. This will be used in both the schedule you see in the automation account, as well as the name of the deployment schedule in update management.
  • -StartTime is the time the deployment should start. As noted, I have had issues with this.
  • -DayofWeek is the actual day of the week that the deployment will happen on. Values are Monday, Tuesday, etc.
  • DaysofWeekOccurrence is for what week that day will run on. Examples are First, Second, etc. So if it was going to run on the first Monday of the week, this would be First.
  • -MonthInterval is set to run each month. I am not sure if you can actually change it from 1 to 2? Maybe if you wanted to only update your system every 2 months?

All the information above is then defined in another variable called $schedule that will be used with the New-AzAutomationSoftwareUpdateConfiguration command.

The final step of the foreach loop uses the New-AzAutomationSoftwareUpdateConfiguration command.

New-AzAutomationSoftwareUpdateConfiguration -ResourceGroupName $ResourceGroupName `
  -AutomationAccountName $AutomationAccount `
  -Schedule $schedule `
  -Windows `
 -Azurequery $AzureQueries `
 -IncludedUpdateClassification Critical,Security,Updates,UpdateRollup,Definition `
    -Duration $duration `
    -RebootSetting IfRequired
  • The -Schedule parameter will use the recently created $schedule variable.
  • -Windows specifies that this deployment schedule will be Windows based.
  • -AzureQuery uses the recently created $AuzreQuery variable.
  • -IncludedUpdateClassification includes what updates you are going to deploy. The values are Critical, Security, etc.
  • -Duration is how long the update will run.
  • -RebootSettings has values of ifrequired, etc.

in the next section, everything is pretty much the same as in the first foreach. With some small changes for the Linux based deployment schedules.

New-AzAutomationSoftwareUpdateConfiguration -ResourceGroupName $ResourceGroupName `
                -AutomationAccountName $AutomationAccount `
                -Schedule $schedule `
                -Linux `
      -Azurequery $AzureQueries `
      -IncludedPackageClassification Critical,Security `
      -Duration $duration `
      -RebootSetting IfRequired

The only two differences are the -IncludePackageClassification and also the -Linux flag. Outside of these, everything is pretty much the same as in the Windows foreach.

Now, this will take some time depending on how many deployment schedules that are being creating. When all is done there will be 126 newly created deployment schedules with all the VM’s dynamically assigned to based off of the Tag created earlier.

Known Issues with my script

The -StartTime parameter in AzAutomationSchedule I think is my issue.  No matter how I format the data I give it when the actual schedule is created, the start time in the portal is off by 5 hours.  So I have a number of deployment schedules that need to be set for UTC0 (00:00 or 12:00 AM).  The data I feed that parameter is all correct.  I have tried it with the DateTimeOffset and without that flag.  Nothing works.  When I go to the portal, the start time show 5:00 AM UTC instead of 12:00 UTC.  The only way I can get it to set right is if I adjust the input from 00:00 to 19:00.  I have tried this on my clients laptop and if I run the script I have to adjust it to 18:00 in order to get the schedule set at 00:00.  Something is not allowing using the local time zone and adjusting the time I am giving to that parameter.  The workaround is good for me.  However, when we hand these over to people in different time zones, they will have to do some editing of the script to get around this.   I have seen a few GitHub issues raised around similar issues, they are older back from 2018 and no answers have been given to those questions.

It wouldn’t be a big issue but when you are automating the creation of 127 deployment schedules times 50 subscriptions you don’t want to go back into each one and edit them again.

What the Ops!!!

With anything there will be plenty of Ops’s when doing thing like this. I had a few. More related to the -StartTime then anything else. So when creating and testing I didn’t really noticed the times being off the first few times I tested. No, I didn’t test with the full array. I limited it to a few lines so I could make sure VM’s would show up correctly in the right dynamic groups, etc. It wasn’t until I ran the entire script the first time that I notice the start times not lining up correctly.

So now I had about 126 deployment groups created and there was no way I was going to go to the portal and delete them one by one.

So I took part of the first script which included asking for the required variables. Because I was lazy I included the existing array again. I only really needed one value from each object. The -ScheduleName value.

The last part of the script was a simple foreach loop.

$ScheduleNames=$ScheduleConfig.ScheduleName

foreach($ScheduleName in $ScheduleNames){

Remove-AzAutomationSoftwareUpdateConfiguration -ResourceGroupName $ResourceGroupName -AutomationAccountName $AutomationAccount -Name $ScheduleName

}

Now, this takes a few minutes and it just sits there. I guess I could add some fancy write-host or something to show which deployment schedule is being deleted. However, just like the script that created all the deployment schedules, it takes time.

Final Thoughts

So I hope this will help someone down the road. I try to write blogs based off of my experiences. The good and the bad ones. This one was a fun one and only a small part of the over all solution I am creating for this client. I may continue blogging about this project more and some of the cool things I am doing around Azure Governance.

One thought on “Azure Update Management – Creating Multiple Deployment Schedules with PowerShell

Leave a Reply

Please log in using one of these methods to post your comment:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s