Deploying #Azure ARM templates with #Powershell


In my previous post #Azure ARM lessons learned and more I shared my notes and tips for creating ARM templates. While you can directly deploy your ARM template form the new Azure Portal, this is not the way I want to go. Automation is everything and even if the portal is set to improve, there will always be area where it will fall short. A few example:
– What if your template needs a sas Token?
– I explained DSC resources and Scripts can only be specified once in ARM templates and then you cannot use anymore that feature
– What if I want to persist and document the parameters I used?
So I will always use powershell to deploy my ARM templates, and since we’re talking Automation it’s highly probable I want to persist my deployment workflows in Azure Automation.
So again here are my deployment notes. This is my scenario:
– I want to use both Azure Automation and standard Powershell
– My ARM templates are saved in Azure Storage as blobs. You can use azcopy or Visual Studio or whatever to upload your templates
– I plan to create my resource groups before deploying my templates

The basics

Import-Module Azure
add-azureaccount
select-azuresubscription ‘My SUbscription’
Switch-AzureMode AzureResourceManager 

#create an empty resource group
$deployName="QNDDemo1"
$RGName="QNDDemo1"
$locName="West Europe"
New-AzureResourceGroup -Name $RGName -Location $locName 

Deploy the templates passing the required parameters

$RGName='QNDDemo2'
$Location='West Europe'
if (! (Get-AzureResourceGroup -Name $RGName -ErrorAction "SilentlyContinue")) {
    New-AzureResourceGroup -Name $RGName -Location $Location
}
$assetsRG='Default-Storage-WestEurope'
$assetsStg=’mystorage’

#get temporary storage sas Token
$key = (Get-AzureStorageAccountKey -ResourceGroupName $assetsRG -Name $assetsStg).Key1
$ctx = New-AzureStorageContext -StorageAccountName $assetsStg -StorageAccountKey $key
$sasToken=New-AzureStorageContainerSASToken -Name protected -Permission rwdl -ExpiryTime ([DateTime]::Now.AddHours(1.0)) -Context $ctx
$sasToken=New-AzureStorageBlobSASToken -Container protected -Blob 'FirstConfig.ps1.zip' -Permission rw -Context $ctx

$vmTemplateUri="https://<storage>.blob.core.windows.net/templates/VM.json"

$parameters=@{
    infraResourceGroup='LabQNDInfra'
    location='West Europe'
    storageAccountName='qnd1'
    virtualNetworkName='EA-Lab-Reggio-ARM'
    subnetName='VMs'
    vmName='qndRecovery'
    adminPassword='<apassword>.'
    dnsNameForPublicIP='qnddemo2'
    vmCount=1
    dscSasToken=$($sasToken.ToString())
}
#check for template correctness
Test-AzureResourceGroupTemplate -ResourceGroupName $RGName -TemplateUri $vmTemplateUri -TemplateParameterObject $parameters –Verbose
#go and deploy
New-AzureResourceGroupDeployment -Name $RGName -ResourceGroupName $RGName -TemplateUri $vmTemplateURI -TemplateParameterObject $parameters -Verbose

Post deployment configuration

Now that the VMs have been provisioned let’s enable WinRM on https using a selfsigned certificate. We can even download or post the certificate somewhere, but this goes beyond the scope of this post. Enabling WinRM is the key for any further automated configuration, from here on we have complete control over the VMs. Enabling WinRM over https/ssl requires a certificate, we have several options:
– If the VM is domain joined and you have an internal PKI we can autoenroll the certificate. In that case is just a matter of selecting the proper certificate from store
– If the VM is not domain joined, but you have anyway an internal PKI you can automate certificate emission, load the certificate and all the CA chain into the VM. This requires the certificates to be funneled over a secure channel from a secure store: we can use Azure KeyVault, or a standard Azure storage account and use a temporary sas Token when we push the script. Let be clear during the VM provisioning process there secrets traveling anyway (for example the local admin password).
– If we don’t have a PKI (I assume we don’t want to pay for public CA certificates) we can create a self signed certificate. Using a self signed certificate implies that when you need to connect to the VM either ignore the certificate validation or you have a store of certificates and set them as trusted in the system used for automating the VMs configuration. In Azure Service Management interface we had a cmdlet to get the WinRM certificate, alas I’ve not been able to find the equivalent for ARM based Virtual Machines.
In this post I’m going to use the latter approach, ignoring certificates when connecting to the VMs.
The second challenge is to get the configuration script to the VM, again we have a couple of options based on the platform we created the VMs in. While this post referes to Azure ARM, after the VMs have been provisioned and WInRM is configured they are just VMs, the can be running on Azure, AWS, Hyper-v, VMWare, your cloud provider, it doesn’t matter.
So first share the snippet to configure WinRM with a self signed certificate

$hostname = [System.Net.Dns]::GetHostName()
$cert=New-SelfSignedCertificate -certstorelocation cert:localmachinemy -dnsname $hostname
winrm create winrm/config/listener?Address=*+Transport=HTTPS "@{Hostname=""$hostName"";CertificateThumbprint=""$($Cert.Thumbprint)"";port=""5986""}"
New-NetFirewallRule -Name WinRM-Https-In -DisplayName "Windows Remote Management (HTTPs-In)" -Direction Inbound –LocalPort 5986 -Protocol TCP -Action Allow

Secondly let’s see how to execute the script on the VMs based on the choosen platform
On Azure, assuming we uploaded the script on a protected storage account, we can use the Set-AzureVMCustomScriptExtension to run the script in a secure way. Obviously we could have set the script in our ARM template, but remember this would have been a one shot operation that couldn’t be repeated. I definitely prefer this approach.

$assetsRG='Default-Storage-WestEurope'
$assetsStg='qndinfralabre'
$assetsContainer='protected'
$key = (Get-AzureStorageAccountKey -ResourceGroupName Default-Storage-WestEurope -Name prginfralabre).Key1
Set-AzureVMCustomScriptExtension -ResourceGroupName $vmResourceGroup -VMName $VMName -Name 'Prereq' -Location $Location -TypeHandlerVersion 1.4 `
    -StorageAccountName $assetsStg -ContainerName protected -StorageAccountKey $key -Run 'ConfigureLocalWinRM.ps1' `
    -FileName 'ConfigureLocalWinRM.ps1' 

On AWS, we can use the user data string to execute anything we want after the VM has been provisioned. Similar to the insertion of the script in an ARM template

$userDataString = Get-Content -Path 'C:\scripts\ConfigureLocalWinRM.ps1' | Out-String
$userDataString = @"

$userDataString
"@ 
…
New-EC2Instance -ImageId $image.ImageId -MinCount 1 -MaxCount 1 –KeyName $keyName -SecurityGroupId $securityGroupId -InstanceType $InstanceType -SubnetId $subnetid –AssociatePublicIp $true -UserData $userData 

If the VMs are on premises, or if we have a secure channel with them (i.e. a VPN of some sort), adding the fact that no secrets are travelling with the script, we can use WinRM over http that is enabled by default from Windows Server 2012 and on.

$vmfqdn='myvm.domain.com'
$cred=Get-Credential
Invoke-Command -Verbose -ConnectionUri "http://$vmfqdn:5985" -Credential $cred `
     -ScriptBlock {
        $hostname = [System.Net.Dns]::GetHostName()
        $cert=New-SelfSignedCertificate -certstorelocation cert:\localmachine\my -dnsname $hostname
        winrm create winrm/config/listener?Address=*+Transport=HTTPS "@{Hostname=""$hostName"";CertificateThumbprint=""$($Cert.Thumbprint)"";port=""5986""}"
        New-NetFirewallRule -Name WinRM-Https-In -DisplayName "Windows Remote Management (HTTPs-In)" -Direction Inbound –LocalPort 5986 -Protocol TCP -Action Allow
} 

From now on we can take complet control of our VMs and configure them as we need to. I’m going to do this via Powershell Desired State Configuration addressing any VM on any platform, leveraging Azure Automation and Azure in general. But this is the topic for another post @AzureAutomation, DSC and Virtual Machines configuration.

Getting and setting Azure Diagnostics

Setting Azure diagnostics in an ARM template is a matter of getting the proper payload, in my previous post I added the easiest way to achieve this is to configure the diagnostics from the Azure Portal and then export and reuse them inside your ARM template. So here we are, remember to use exactly the resource name I set in the script, if not it won’t work:

$rgName='VMResourceGroup'
$vmName='myVM'

$settings = Get-AzureVMDiagnosticsExtension -ResourceGroupName $rgName -VMName $vmName -name Microsoft.Insights.VMDiagnosticsSettings
$wadcfg = [xml] [System.Text.Encoding]::ASCII.GetString([System.Convert]::FromBase64String( (convertfrom-json $settings.PublicSettings).xmlCfg));
#if you want to persist for future references and or to modify it, then you can relaod from file
$wadcfg.Save('c:\temp\wadcfg.xml')
#$wadconfig = [string] (Get-Content C:\temp\wadcfg.xml)
$wadconfig=$wadcfg
$wadpayload=[System.Convert]::ToBase64String( [Text.Encoding]::ASCII.GetBytes($wadconfig));
#now you can suer the content of $wadpayload in your ARM template OR Set it thoutgh powershell
$stgAccountRG='QNDReggioInfra'
$stgAccountName='qndlabrelrs1'
$key = (Get-AzureStorageAccountKey -ResourceGroupName $stgAccountRG -Name $stgAccountName).Key1
$ctx = New-AzureStorageContext -StorageAccountName $stgAccountRG -StorageAccountKey $key
Set-AzureVMDiagnosticsExtension -ResourceGroupName $rgName -VMName $vmName -Name Microsoft.Insights.VMDiagnosticsSettings -DiagnosticsConfigurationPath C:\temp\wadcfg.xml -StorageContext $ctx

Getting more info

Since there’s really poor documentation you may need to get some info directly from Azure

#Supported VM Sizes
$vmsize=Get-AzureVMSize -Location $location | Out-GridView -OutputMode Single
#Supported images
$publisher=Get-AzureVMImagePublisher -Location $location | Out-GridView -OutputMode Single
$offer=Get-AzureVMImageOffer -Location $location -Publisher $publisher.PublisherName | Out-GridView -OutputMode Single
$sku=Get-AzureVMImageSku -Location $location -Publisher $offer.PublisherName -Offer $offer.Offer |  Out-GridView -OutputMode Single
$image= Get-AzureVMImage -Location $location -PublisherName $sku.PublisherName -Offer $sku.Offer -Skus $sku.Skus  |  Out-GridView -OutputMode Single
#Supported extensions
$publishers=Get-AzureVMImagePublisher -Location $location | Out-GridView -OutputMode Multiple
$extlist=@()
foreach($p in $publishers) {
    $extlist += Get-AzureVMExtensionImageType -Location $location -PublisherName $p.PublisherName
}
$extensionType = $extList | out-GridView -OutputMode Multiple
$extensions=@()
foreach($e in $extensionType) {
    $extensions+=Get-AzureVMExtensionImage -Location $location -PublisherName $e.PublisherName -Type $e.Type
}
$extensions

Debugging and troubleshooting
Azure Powershell module is just a software layer on the azure REST API. Every software layer on one side makes life easier giving more abstraction, on the other introduces bugs. Troubleshooting Azure CmdLet is a twofold process:
1. Enable debugging in powershell: $VerbosePreference=’Continue’; $DebugPreference=’Continue’. From then on you can check the actual REST calls and the returned results
2. Try the calls using the REST interface directly
Last, but not least, check the Azure side Audit Logs, you can do this from the Azure portal and from powershell (Get-AzureResource*Log) (I prefer the first method.)
To try the REST interface you need some support in terms of authentication and you can currently use only Azure AD Accounts. The following snippet should be straightforward

Function Get-AADToken {

        [CmdletBinding()]
        PARAM (
        [Parameter(ParameterSetName='IndividualParameter',Mandatory=$true)][Alias('t')][String]$TenantADName,
        [Parameter(ParameterSetName='IndividualParameter',Mandatory=$true)][Alias('u')][String]$Username,
        [Parameter(ParameterSetName='IndividualParameter',Mandatory=$true)][Alias('p')][String]$Password,
        [Parameter(ParameterSetName='IndividualParameter',Mandatory=$true)][Alias('t')][String]$IdentityDLLPath='.'
        )
try {

    $arrDLLs = @()
    $arrDLLs += 'Microsoft.IdentityModel.Clients.ActiveDirectory.dll'

    Foreach ($DLL in $arrDLLs)
    {
        $AssemblyName = $DLL.TrimEnd('.dll')
        #weak match
        If (!([AppDomain]::CurrentDomain.GetAssemblies() |Where-Object { $_.FullName -match '($DLL).*'}))
        {
            Write-verbose 'Loading Assembly $AssemblyName...'
            Try {
                $DLLFilePath = Join-Path $IdentityDLLPath $DLL
                [Void][System.Reflection.Assembly]::LoadFrom($DLLFilePath)
            } Catch {
                throw "Unable to load $DLLFilePath. Please verify if the DLLs exist in this location!"                
            }
        }
    }

    # Set well-known client ID for Azure PowerShell
    $clientId = "1950a258-227b-4e31-a9cf-717495945fc2"

    # Set redirect URI for Azure PowerShell
    $redirectUri = "urn:ietf:wg:oauth:2.0:oob"

    # Set Resource URI to Azure Service Management API
    $resourceAppIdURI = "https://management.core.windows.net/"

    # Set Authority to Azure AD Tenant
    $authority = "https://login.windows.net/$TenantADName"

    $credential = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.UserCredential" -ArgumentList $Username,$Password
    # Create AuthenticationContext tied to Azure AD Tenant
    $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList $authority

    $authResult = $authContext.AcquireToken($resourceAppIdURI,$clientId,$credential)
    $Token = $authResult.CreateAuthorizationHeader()
    Return $Token
    }
catch {
        Write-Error "Get-AADToken error $($Error[0].Exception)"
        write-Verbose $("TRAPPED: " + $_.Exception.GetType().FullName); 
        Write-Verbose $("TRAPPED: " + $_.Exception.Message); 
        return $null
}

}
$adTenant = "<mydomain>.onmicrosoft.com" 

$token=Get-AADToken -TenantADName $adTenant -Username <user@domain> -Password <password> -IdentityDLLPath "${env:ProgramFiles(x86)}Microsoft SDKsAzurePowerShellServiceManagementAzureServices" 
    $headers = @{"Authorization"=$Token;"Accept"="application/json"}
    $headers.Add("Content-Type","application/json")
$uri=”<ARM formatted URI>”
invoke-webrequest -Method <Get|Put> -Uri $uri -Headers $headers -UseBasicParsing

Resources

Previous in the serie

-Daniele
This posting is provided “AS IS” with no warranties, and confers no rights.

Advertisements
  1. #Azure: ARM templates, Automation, DSC and more. Travel notes | Quae Nocent Docent
  2. #Azure ARM lessons learned and more | Quae Nocent Docent

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com 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 )

Google+ photo

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

Connecting to %s

%d bloggers like this: