Azure Application Gateway learnings

I’ve been fine-tuning some Terraform config for Azure Application Gateway lately, and have thus been fine-tuning my understanding of its components. This Microsoft Doc about the App Gateway configuration was quite helpful because of it’s diagram.

Here’s a few items I’ve learned:

  • Terraform: You must separate out private IP and public IP into different front-end configurations. If you wish to utilize both to be associated with listeners, you’d have two config blocks:
    • frontend_ip_configuration {
          name                 = "${local.frontend_ip_configuration_name}"
          public_ip_address_id = "${azurerm_public_ip.test.id}"
        }
    • frontend_ip_configuration {
          name                 = "${local.frontend_ip_configuration_name}"
          subnet_id = "${azurerm_subnet.test.id}"
          private_ip_address_id = "${azurerm_subnet.testsub.id}"
          private_ip_address_allocation = Static
        }
  • General: A listener can only be associated with 1 front end IP (either private or public). Originally I thought that I could have both a private and public front-end that were associated with the same listener, and thus the same rule with a backend. This isn’t possible, and instead you must have unique listeners to each front end configuration.
  • Terraform: Terraform seems to have a problem adding multiple rules touching the same backend. Even though these were unique rules associated with unique listeners, Terraform gave this error: ApplicationGatewayBackendAddressPoolCannotHaveDuplicateAddress
    • This was seen while trying to add an additional rule after an original was created. I haven’t yet tried to perform a ‘terraform apply’ from a fresh start with two rules referencing the same backend.
  • General: You can’t have multiple listeners on the same front-end port across two different front-end configurations. If you do, you receive the following error:
    • For example, if I have port 80 on my private front-end config, I can add multi-site listeners based on hostname here to target multiple rules and backends.  But this means I cannot use port 80 on the public front-end configuration anymore; a different port would be required.
    • Error: Two Http Listeners of Application Gateway are using the same Frontend Port and HostName (ApplicationGatewayFrontendPortsUsingSamePortNumber)

 

Azure Monitor Alert rule not stopping

I discovered something interesting while working with Azure Monitor alert rules recently.

If you have an alert that is firing based on a condition, it will continue to fire until the condition is cleared, even if the alert rule itself is modified, disabled, or even deleted.

Here’s the example:

I have a B-Series VM, and want to alert on the “CPU Credits Remaining” metric, to pro-actively catch intervals when CPU usage is causing credit exhaustion and thus reduced compute capacity.

I created an Alert Rule to fire when the “CPU Credits Remaining” to fire when the value is 100 or less. It was configured with a frequency of 1 minute and a period of 5 minutes (because I wasn’t thinking of the implications at the time).

This worked just great for a while! And then a rogue Windows Update process got stuck consuming 60% of the CPU for a period of time, and the credit count dropped all the way to zero.

The alert began to fire as expected, once per minute, which quickly became excessive and drowned a bunch of mailboxes in alert-overload.

“Ok, lets just disable the alert” – nope, it continued to fire. I modified the rule so that the frequency and period were much greater. However, the email alerts received continued to reflect the original values:

Even deleting the rule did not stop the alerts from triggering.

Interestingly, if I modified the action group to use a different target email address, that was immediately effective. This allowed me to black-hole the emails until I had resolved the CPU utilization problem and waiting until the credits built back up.

 

Azure update configuration – dynamic group workaround

I put my Azure Update Management into full testing recently, using the deployment script I shared last week.

I quickly encountered a problem where despite the VMs communicating with Azure Automation properly, and showing as “ready” for Update Management, they would no longer appear as selected when using Dynamic Groups.

I suspect this may have something to do with the proxy setup in this particular environment, but I didn’t have time to troubleshoot and needed to have my scheduled update configurations statically select my VMs instead of relying upon dynamic groups (they are after all still in preview).

However, I still wanted to use Tags in order to select my VMs. In order to do this, I removed the “Targets” object from my JSON body entirely, and used the “azureVirtualMachines” object instead. I needed to put the logic of VM selection based on tags into this object.

I recognize that this removes the “dynamic” nature, in that it would only be updated when I re-run the PowerShell script to update the scheduled update configuration, but since this is going to be run once-per-month in order to change the date (Microsoft, add day offset support!) that isn’t a large problem.

The only real change is that selection of the VMs by tag can be done like this:

$selectedvms = Get-AzureRmVM | Where-Object {$_.Tags['MaintenanceWindow'] -eq $MaintenanceWindow} | select-object id
$virtualmachines = $selectedvms.id | ConvertTo-JSON

Multiple Tag definitions can be added into the “Where-Object” logic of that command.

Full Example

 
# API Reference: https://docs.microsoft.com/en-us/rest/api/automation/softwareupdateconfigurations/create#updateconfiguration
 
### Monthly Parameters ###
  $deploymentName = "SUG_Thurs-2am-MST-4hours"
  $MaintenanceWindow = "Thurs-2am-MST-4hours"
  $starttime = "2018-11-15T02:00:00+00:00"
 
# Scope Parameters
  $clientsubscription = "<subscription_id_to_target>"
  Select-AzureRmSubscription -Subscription $clientsubscription
  ## Schedule Parameters
  # Populate an array with the full ID of all VMs to apply this schedule to:
  $selectedvms = Get-AzureRmVM | Where-Object {$_.Tags['MaintenanceWindow'] -eq $MaintenanceWindow} | select-object id
  $virtualmachines = $selectedvms.id | ConvertTo-JSON
 
# Static Schedule Parameters
  $AutomationRG = "test-rg" # Resource Group the Automation Account resides in
  $automationAccountName = "test-automation"
  $automationSubscription = "<subscriptionId>" # Subscription that the Automation Account resides in
 
  $duration = "PT4H0M" # This equals maintenance window - Put in the format PT2H0M, changing the numbers for hours and minutes
  $rebootSetting = "IfRequired" # Options are Never, IfRequired
  $includedUpdateClassifications = "Critical,UpdateRollup,Security,Updates" # List of options here: https://docs.microsoft.com/en-us/rest/api/automation/softwareupdateconfigurations/create#windowsupdateclasses
  $timeZone = "America/Edmonton" # List from ??
  $frequency = "OneTime" # Valid values: https://docs.microsoft.com/en-us/rest/api/automation/softwareupdateconfigurations/create#schedulefrequency
  #$interval = "1" # How often to recur based on the frequency (i.e. if frequency = hourly, and interval = 2, then its every 2 hours)
 
 
### These values below shouldn't need to change
  Select-AzureRmSubscription -Subscription "$automationSubscription"
  # Get the access token from a cached PowerShell session
  . .\Get-AzureRmCachedAccessToken.ps1 # Source = https://gallery.technet.microsoft.com/scriptcenter/Easily-obtain-AccessToken-3ba6e593
  $BearerToken = ('Bearer {0}' -f (Get-AzureRmCachedAccessToken))
  $RequestHeader = @{
    "Content-Type" = "application/json";
    "Authorization" = "$BearerToken"
  }
 
# JSON formatting to define our required settings
$Body = @"
{
  "properties": {
    "updateConfiguration": {
	  "operatingSystem": "Windows",
      "duration": "$duration",
      "windows": {
        "excludedKbNumbers": [],
        "includedUpdateClassifications": "$includedUpdateClassifications",
        "rebootSetting": "$rebootSetting"
      },
      "azureVirtualMachines": $virtualmachines,
    },
    "scheduleInfo": {
      "frequency": "$frequency",
      "startTime": "$starttime",
      "timeZone": "$timeZone",
      "interval": $interval,
	  "isEnabled": true
    }
  }
}
"@
 
# Build the URI string to call with a PUT
$URI = "https://management.azure.com/subscriptions/$($automationSubscription)/" `
     +"resourceGroups/$($AutomationRG)/providers/Microsoft.Automation/" `
     +"automationAccounts/$($automationaccountname)/softwareUpdateConfigurations/$($deploymentName)?api-version=2017-05-15-preview"
 
# use the API to add the deployment
$Response = Invoke-RestMethod -Uri $URI -Method Put -body $body -header $RequestHeader

Parameterized Update Configuration deployment

After getting a functional script working for deploying an Update Configuration in my last post, I began working on making it worthwhile to use in the future.

I wanted to parameterize the JSON, so that it could be re-used in a much more efficient manner without having to scroll through the file to the JSON body and manually edit sections. This would be needed for both the Subscriptions/ResourceGroups (in the Scope object) and the Tags that I wanted to use.

In this example, I wanted a scheduled deployment that ran once (so I could control the date for the first Wednesday after the second Tuesday of the month), with a maintenance window of 4 hours, with dynamic grouping applied against multiple subscriptions and virtual machines with a defined “MaintenanceWindow” tag.

For the Scope definition, I created a PowerShell array containing the resource groups resource IDs. The format of these can be found in the Portal under “Properties” of the resource group.

# Populate this array with the subscription IDs and resource group names that it should apply to
$scopeDefinition = @(
  "/subscriptions/$subscriptionId/resourceGroups/managementVMs"
  ,"/subscriptions/$subscriptionId/resourceGroups/webVMs"
)

The Tag definition was created as a Hashtable, noting the tag value that I wanted to include:

# Populate this Hashtable with the tags and tag values that should it should be applied to
$tagdefinition = @{
  MaintenanceWindow = @("Thurs-2am-MST-4hours")
}

* side note, since Update Management doesn’t operate on the basis of local time of the VM (as SCCM is capable of) I need to build my maintenance windows around discrete time zones, and have multiple update configurations for these time zones even if the local time that I’m targeting is always the same (i.e. 2am).

I can now convert these Powershell variables into JSON:

$applyResourceGroup = $scopeDefinition | ConvertTo-JSON
$applyTags = $tagdefinition | ConvertTo-JSON

and reference it within my JSON body like this:

"targets": 
      {
        "azureQueries": [{
                  "scope": $applyResourceGroup,
                  "tagSettings": {
                      "tags": $applyTags,
                      "filterOperator": "Any"
                  },
                  "locations": null
              }]
      }

Full Example

Here’s a full example that makes parameters of the full contents of the JSON body:

 
# API Reference: https://docs.microsoft.com/en-us/rest/api/automation/softwareupdateconfigurations/create
### Monthly Parameters ###
$deploymentName = "SUG_Thurs-2am-MST-4hours"
$starttime = "2018-11-15T02:00:00+00:00" 
 
# Scope Parameters
$clientsubscription = "<subscription_id_to_target>"
# Populate this array with the subscription IDs and resource group names that it should apply to
$scopeDefinition = @(
  "/subscriptions/$clientsubscription/resourceGroups/managementVMs"
  ,"/subscriptions/$clientsubscription/resourceGroups/webVMs"
)
 
# Static Schedule Parameters
$AutomationRG = "test-rg" # Resource Group the Automation Account resides in
$automationAccountName = "test-automation"
$automationSubscription = "<subscriptionId>" # Subscription that the Automation Account resides in
# Populate this Hashtable with the tags and tag values that should it should be applied to
$tagdefinition = @{
  MaintenanceWindow = @("Thurs-2am-MST-4hours")
}
$duration = "PT4H0M" # This equals maintenance window - Put in the format PT2H0M, changing the numbers for hours and minutes
$rebootSetting = "IfRequired" # Options are Never, IfRequired
$includedUpdateClassifications = "Critical,UpdateRollup,Security,Updates" # List of options here: https://docs.microsoft.com/en-us/rest/api/automation/softwareupdateconfigurations/create#windowsupdateclasses
$timeZone = "America/Edmonton" # List from ??
$frequency = "OneTime" # Valid values: https://docs.microsoft.com/en-us/rest/api/automation/softwareupdateconfigurations/create#schedulefrequency
#$interval = "1" # How often to recur based on the frequency (i.e. if frequency = hourly, and interval = 2, then its every 2 hours)
 
 
### These values below shouldn't need to change
Select-AzureRmSubscription -Subscription "$automationSubscription" # Ensure PowerShell context is targeting the correct subscription
$applyResourceGroup = $scopeDefinition | ConvertTo-JSON
$applyTags = $tagdefinition | ConvertTo-JSON
# Get the access token from a cached PowerShell session
. .\Get-AzureRmCachedAccessToken.ps1 # Source = https://gallery.technet.microsoft.com/scriptcenter/Easily-obtain-AccessToken-3ba6e593
$BearerToken = ('Bearer {0}' -f (Get-AzureRmCachedAccessToken))
$RequestHeader = @{
  "Content-Type" = "application/json";
  "Authorization" = "$BearerToken"
}
 
# JSON formatting to define our required settings
$Body = @"
{
"properties": {
  "updateConfiguration": {
    "operatingSystem": "Windows",
    "duration": "$duration",
    "windows": {
      "excludedKbNumbers": [],
      "includedUpdateClassifications": "$includedUpdateClassifications",
      "rebootSetting": "$rebootSetting"
    },
    "azureVirtualMachines": [],
    "targets": 
      {
        "azureQueries": [{
                  "scope": $applyResourceGroup,
                  "tagSettings": {
                      "tags": $applyTags,
                      "filterOperator": "Any"
                  },
                  "locations": null
              }]
      }
  },
  "scheduleInfo": {
    "frequency": "$frequency",
    "startTime": "$starttime",
    "timeZone": "$timeZone",
    "interval": $interval,
    "isEnabled": true
  }
}
}
"@
 
# Build the URI string to call with a PUT
$URI = "https://management.azure.com/subscriptions/$($automationSubscription)/" `
   +"resourceGroups/$($AutomationRG)/providers/Microsoft.Automation/" `
   +"automationAccounts/$($automationaccountname)/softwareUpdateConfigurations/$($deploymentName)?api-version=2017-05-15-preview"
 
# use the API to add the deployment
$Response = Invoke-RestMethod -Uri $URI -Method Put -body $body -header $RequestHeader

 

Azure Update Management deployments – programmatically

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

 

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}

Fantastic! This can be turned into read-able JSON by piping your $GetResponse like this:

 $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

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
                    }
                ]
            }

However, this is what the API reference uses as it’s structure:

"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 = "&lt;subscription_id&gt;"
$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