Pass multi value parameters to Azure Runbook

I learned something recently about calling an Azure Automation Runbook through PowerShell, particularly around how to pass multi-value entries in a single parameter.

The original concept was that I have a runbook which takes input parameters of an Azure resource group, network security group, and DNS name. It takes this information, resolves the DNS name to an IP address, and then creates a rule inside the network security group for it.

Here’s a simplified example of what I started with:

param(
  [parameter(Mandatory=$false)]
  [string]$nsggroup = "*",
  [parameter(Mandatory=$false)]
  [string]$rulename = "source_dest_port",
  [parameter(Mandatory=$false)]
  [string]$rgname = "*-srv-rg",
  [parameter(Mandatory=$false)]
  [string]$endpointfqdn = "endpoint.vendor.com"
)
$dnstoip = [System.Net.Dns]::GetHostAddresses("$endpointfqdn")
$subscriptions = Get-AzSubscription
 
$nsgs = Get-AzNetworkSecurityGroup -ResourceGroupName "$rgname" -Name "$nsggroup"
ForEach($nsg in $nsgs) {
    $nsgname = $nsg.name
    $nsg | Add-AzNetworkSecurityRuleConfig -Access Allow -DestinationAddressPrefix $dnstoip.IPAddressToString -DestinationPortRange 443 -Direction Outbound -name $rulename -Priority $priority -SourceAddressPrefix * -SourcePortRange * -Protocol * | Set-AzNetworkSecurityGroup | out-null
}

The problem with this is once it took it to a production environment, I needed more flexibility on where the rule got applied, without having to create many instances of the scheduled runbook linked to parameters.

The key was that I wanted to pass in a list of network security group names, and have the runbook do a ForEach on that list.

The primary change in parameters was to make $nsggroup an [array] type like this:

[array]$nsggroup
Now, if calling the runbook manually in the Portal GUI, I would populate that parameter with this syntax:
['nsg1','nsg2']

This syntax and learning originally came from this doc link which states:

  • If a runbook parameter takes an [array] or [object] type, then these must be passed in JSON format in the start runbook dialog.  For example:
  • A parameter of type [object] that expects a property bag can be passed in the UI with a JSON string formatted like this: {“StringParam”:”Joe”,”IntParam”:42,”BoolParam”:true}.
  • A parameter of type [array] can be input with a JSON string formatted like this: [“Joe”,42,true]

 

In order to programmatically set up the runbook along with linked schedules, I put my input into an array containing PowerShell objects like this (note the “nsggroup” value on each object):

$inputrules= @(
    [pscustomobject]@{  name = "dnsresolve1"; rulename = "source_dest_port"; ports = @(443); endpointfqdn = "endpoint.vendor.com"; nsggroup = @('nsg1', 'nsg2', 'nsg5', 'nsg6') },
    [ps[pscustomobject]@{  name = "dnsresolve2"; rulename = "source2_dest_port"; ports = @(443); endpointfqdn = "endpoint2.vendor.com"; nsggroup = @('nsg3', 'nsg4') }
)

In my script that does the programmatic setup, I use this array of objects to do something like this (vastly simplified):

foreach ($object in $inputrules) {
Register-AzAutomationScheduledRunbook -Name $runbookname -ScheduleName "6Hours-$($object.name)" -AutomationAccountName $automationAccountName -ResourceGroupName $resourceGroupName `
        -Parameters @{"nsggroup" = $object.nsggroup; "rulename" = $object.rulename; "endpointfqdn" = $object.endpointfqdn; "ports" = $object.ports; "subscriptionid" = $clientsubscriptionid }
}

You can see by using the syntax in my custom object of an array, I can pass it into the scheduled runbook as a simple parameter, and Azure Automation will take that and apply it successfully.

 

Terraform refactor to modules deletes resources

I’ve finally got a use-case for using Modules in Terraform, and so I’m beginning to dip into testing with them. I’ve got a bunch of Azure resources (VNET, subnet, NSG, etc) that already exist in my Terraform config, and I’m looking to basically duplicate them for a disaster recovery purpose.

In reality, I don’t want to duplicate the Terraform config, because if it ever changes or improves, it would not be efficient to track those changes in multiple spots. So instead I can move the resources that build my VNET into a module, add some variables, and then call the module twice from my main Terraform config – just need to pass in different values for the variables.

module "vnet" {
  source = "./network"
  env = "test1"
  location = "CanadaCentral"
}
 
module "dr-vnet" {
  source = "./network"
  env = "test2"
  location = "CanadaEast"
}

While reading up on modules though, I noticed a note in the docs:

When refactoring an existing configuration to introduce modules, moving resource blocks between modules causes Terraform to see the new location as an entirely separate resource to the old. Always check the execution plan after performing such actions to ensure that no resources are surprisingly deleted.

Yikes! Even for a simple implementation of a VNET, deleting it means deleting a bunch of dependent resources like virtual machines.

However, there does appear to be a (very manual) way to work around this problem when refactoring into Modules: use the terraform mv command to move location of a resource in the state file.

This relies upon the syntax of referencing resources beginning with module.”module name”. For example, lets say I have this original resource:

resource "azurerm_resource_group" "test-rg" {
  name     = "test-rg"
  location = "CanadaCentral"
}

Assuming I call the module just like my example above, I would use the following terraform mv command:

terraform state mv azurerm_resource_group.test-rg module.vnet.azurerm_resource_group.test-rg

Note the sytax of my destination, being module.”module name”.resourcetype.”resource name”

The downside is that I would need to do this for each original resource that is moving into a Module – perhaps a small price to pay in order to have better managed code, and at least I don’t need to go and find the Azure resource ID like you do with the terraform import command.

This resource group is actually a bad example, because terraform apply will fail when it tries to create two different resource groups with the same name in the same subscription. This could be avoided if you pass in an alternate AzureRM Provider to your Module, using a different Azure subscription.

 

Azure NSG Update issue with PowerShell

Today I’m working on creating and updating Azure Network Security Group rules from the Az PowerShell module. However I hit a bit of a bump.

I’m trying to do something simple like add 1 rule to an existing NSG that has other rules:

# Find the security group where it matches a variable
$nsgs = Get-AzNetworkSecurityGroup -ResourceGroupName $rgname | where-object { $_.Name -like "$nsgsuffix" }
# For the selected security group, add a rule, and then apply it
$nsgs | add-aznetworksecurityruleconfig -Access Allow -DestinationAddressPrefix $ips -DestinationPortRange 443 `
 -Direction Outbound -name allowOut-To3rdParty_443 -Priority 400 `
-SourceAddressPrefix * -SourcePortRange * -Protocol * | Set-AzNetworkSecurityGroup

This NSG and other rules were previously deployed through TerraForm, and I want to add an “out-of-band” rule that isn’t tracked in the Terraform state.

When I run these commands, I get this error:

Required security rule parameters are missing for security rule with Id:
Security rule must specify either DestinationPortRange or DestinationPortRanges.

That’s super strange; when I look at that rule in the Portal, it has ports listed:

So I check my $nsgs variable that was previously populated, looking specifically for the SecurityRules property:

$nsgs.SecurityRules

What I find is that for any rule that the portal displays having multiple ports, it lists them just fine (see Destination Port Range property below):

Name                                 : test_test.test
Id                                   : /subscriptions/1/resourceGroups/rg/providers/Microsoft.Network
/networkSecurityGroups/db-nsg/securityRules/test_test.test
Etag                                 : W/"3a98e199-3bf6-4308-806c-8d84bc03723e"
ProvisioningState                    : Succeeded
Description                          :
Protocol                             : *
SourcePortRange                      : {*}
DestinationPortRange                 : {123, 456, 789}
SourceAddressPrefix                  : {10.8.1.}
DestinationAddressPrefix             : {10.8.1.}
SourceApplicationSecurityGroups      : []
DestinationApplicationSecurityGroups : []
Access                               : Allow
Priority                             : 109
Direction                            : Inbound

But for any rule that only has a SINGLE port shown in the portal, it is blank for that property:

Name                                 : web_.db
Id                                   : /subscriptions/1/resourceGroups/rg/providers/Microsoft.Network
/networkSecurityGroups/db-nsg/securityRules/web_.db
Etag                                 : W/"3a98e199-3bf6-4308-806c-8d84bc03723e"
ProvisioningState                    : Succeeded
Description                          :
Protocol                             : *
SourcePortRange                      : {*}
DestinationPortRange                 : {}
SourceAddressPrefix                  : {10.0.0.1, 10.0.0.2}
DestinationAddressPrefix             : {10.1.1.100}
SourceApplicationSecurityGroups      : []
DestinationApplicationSecurityGroups : []
Access                               : Allow
Priority                             : 107
Direction                            : Inbound

My first thought was “how is this possible”?

I think this is because for these single port rules, we’re using the following Terraform syntax:

destination_port_ranges    = ["${var.port-https}"]

Terraform is expecting a list, but we’re only passing a single value. Terraform doesn’t consider this invalid, and it is applied to Azure successfully. However the flaw is revealed when trying to use the “Set-AzNetworkSecurityGroup” cmdlet because that attempts to re-validate ALL rules on the NSG, not just the one that I added or modified.

If I change that Terraform property to destination_port_range (note the singular) then everything appears to work properly when using Az PowerShell afterwards.

Get Azure VM Uptime – sorta

Let’s say you have a few virtual machines in Azure, that should only be running for a limited period of time. You want to ensure that they are stopped after that period of time, but you aren’t the one responsible for the service running on the VMs, and as such, want to empower the owner(s) with both the responsibility and capability to determine when that period of time is finished; perhaps with just a bit of nudging…

Thinking about this scenario had me determine, “what if I could find out when a VM uptime exceeds a set value (say 14 days)” and notify someone that “hey, maybe you forgot to turn this off”.

Turns out there isn’t an accessible “uptime” property for an Azure VM. However, there is a time stamp property from the last state change, and that’s what we can use to enable this logic.

The key is to use “Get-AzVM” with the -Status switch. When you run Get-AzVM, you can either return the “model view” or the “instance view” of a collection of virtual machines, or an individual VM. The Microsoft Doc page for this cmdlet describes using the -Status switch to provide the “instance view” which is where we get the properties we’re after.

If you run it for a collection of VMs you can retrieve the PowerState property:

For an individual VM, you get the “Statuses” property, which contains a collection of items:

The first item within the “Statuses” collection is the “ProvisioningState”, which displays (from what I can gather) the most recent provisioning action taken on the VM (starting it, stopping/deallocating it), along with a Time property. The second item is the current PowerState of the VM.

Using this information, I’ve built the PowerShell below to grab all VMs within a subscription, and for each of them evaluate the status against a point-in-time, outputting results for those which are running and where the successful provisioning exceeds the age of that point-in-time.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Select-AzSubscription 
$comparedate = (get-date).AddDays(-14)
$rg = "resourcegroup"
#Get the Instance view of a collection of virtual machines (returns the PowerState property)
$vms = get-azvm -status -resourcegroup $rg
 
#Iterate through the collection
foreach ($vm in $vms)
{
	# only check if the VM is running, because if it's off we don't care
	if ($vm.powerstate -ceq "VM running")
	{
		# Get the instance view of a single virtual machine (returns the "statuses" object)
		$foundvm = get-azvm -resourcegroup $vm.ResourceGroupName -name $vm.Name -status
		    #$foundvm.Statuses.Time
                # check if time since it was provisioned (in Statuses[0]) is greater than a value
		if ($foundvm.Statuses.Time -le $comparedate)
		{
			write-output "$($foundvm.name) : running longer than 14 days"
		}
	}
}

Using this PowerShell, you could modify the results to populate an array, and email it to a recipient. This could be placed into an Azure Automation runbook and ran on a schedule.

Run script inside Azure VM from PowerShell

Today I was working on a way to initialize and format disks inside an Azure VM from PowerShell, specifically outside the VM itself.

The two most common ways to accomplish this are:

One of my key requirements was to not have the VM itself go and grab the script file to run; I wanted to pass a scriptblock to my VM from PowerShell instead. This immediately ruled out Set-AzVMCustomScriptExtension, as it needs a file that is accessible to the VM in order to run.

I had originally thought the same about Invoke-AzVmRunCommand. The example I saw for the ‘-ScriptPath’ parameter were all from a storage account or something similar. However after testing, I found that this ScriptPath could be local to MY computer, where I was running the PowerShell, not the VM itself. The command will inject the contents of the script file into the VM.

In order to simulate the “scriptblock” effect without having too many files, I put my code into a script file at runtime (so that it would be wherever the user was running the prompt), referenced it, and then removed it afterwards, like this:

# Build a command that will be run inside the VM.
$remoteCommand =
@"
#Get first disk that is raw with the lowest disk number (because we may not know what number it will be)
# F volume
Get-Disk | Where-Object partitionstyle -eq 'raw' | Sort-Object Number | Select-Object -first 1 | Initialize-Disk -PartitionStyle GPT  -confirm:$false -PassThru |
New-Partition -DriveLetter 'F' -UseMaximumSize |
Format-Volume -FileSystem NTFS -NewFileSystemLabel "F volume" -Confirm:$false
"@
# Save the command to a local file
Set-Content -Path .\DriveCommand.ps1 -Value $remoteCommand
# Invoke the command on the VM, using the local file
Invoke-AzureRmVMRunCommand -Name $vm.name -ResourceGroupName $vm.ResourceGroupName -CommandId 'RunPowerShellScript' -ScriptPath .\DriveCommand.ps1
# Clean-up the local file
Remove-Item .\DriveCommand.ps1