PowerShell Script to Deploy Subscription Level ARM Templates

5 minute read

Introduction

In my previous post, I demonstrated how to deploy Azure Policy definitions that require input parameters via ARM templates. as I mentioned in that post, at the time of writing, the tooling has not been updated to allow subscription level ARM template deployments. The only possible way to deploy such template right now is via the ARM REST API.

I have a requirement to deploy subscription level templates in VSTS pipelines. since I can’t use the native AzureRM PowerShell module or the Azure Resource Group Deployment VSTS task, I had to create a PowerShell script that can be used under the context of Azure PowerShell VSTS task.

Requirements

The script has the following requirements:

1. Being able to invoke ARM REST API within the Azure PowerShell VSTS task.

In order to invoke ARM REST APIs, an Azure AD oAuth token needs to be generated to access the resources in ARM endpoints (https://management.azure.com). Based on my own research, when running scripts in Azure PowerShell VSTS task, the oAuth token is not retrievable using Get-AzureRMContext cmdlet. To work around this limitation, I manually created the Azure AD Service Principal that the VSTS Service Endpoint uses and stored the key as a secret in an Azure Key Vault. Then created a variable group in the VSTS project and used it to retrieve the Service Principal key from the key vault. The key and the service principal name is passed into the script, the script looks up the Service Principal based on it’s name, and generates an oAuth token for ARM REST API calls.

Note: In case you ask why not use the oAuth token that can be made available in VSTS? Based on my research and testing, that token is generated for invoking VSTS REST APIs, not ARM APIs. So the answer is no, I can’t use that token.

2. Being able to perform ARM template validation

Part of the build (CI) pipeline would be validating the ARM template and input parameters, make sure it template is valid before kicking off the release (CD) task. so the same script can also be used to perform the validation using an input parameter.

PowerShell Script Deploy-SubscriptionLevelTemplate.ps1

In order to execute the script, you will firstly need to create a service principal and also pass its key to the script.

image

This service principal must have sufficient privilege in the subscription that you are deploying the template to. In my case, since I’m deploying policy definitions, the Service Principal needs to have the subscription Owner role because the Contributor role does not have required rights in Microsoft.Authorization resource provider.

To manually execute the script outside of the VSTS pipeline, you can do something like this:

$TenantId = '74edd9d1-c33c-4890-bbfe-53d8eea27fad'
$SubscriptionId = '3fe4fa99-78ff-44a2-ad57-4d2cba88790a'
$ServicePrincipalAppNamePrefix = 'vsts'
$ServicePrincipalKey = 'ENTER YOUR SP KEY HERE'

$location = 'australiasoutheast'
Add-AzureRMAccount -Subscription $SubscriptionId

$TemplateFilePath = 'C:\temp\allowedRoleDefinitionDemo.azuredeploy.json'
$ParameterFilePath = "C:\temp\allowedRoleDefinitionDemo.azuredeploy.parameters.json"
.\Deploy-SubscriptionLevelTemplate.ps1 -TenantId $tenantId -SubscriptionId $SubscriptionId -location $location -ServicePrincipalAppNamePrefix $ServicePrincipalAppNamePrefix -ServicePrincipalKey $ServicePrincipalKey -TemplateFilePath $TemplateFilePath -ParameterFilePath $ParameterFilePath  -Verbose

Few things to be aware of:

  • By default, the script parameter –validate is set to $false, to perform template validation instead of deployment, use –validate $true when executing the script.
  • The location field is still required even when deploying the template to a subscription (instead of a resource group)
  • the –ParameterFilePath parameter is optional. Only use it if you need to pass parameters into the template
  • You still need to login to Azure using Add-AzureRMAccount, because if you execute this script within the Azure PowerShell task in VSTS, it is already logged in using the service endpoint you have specified. This is to simulate the same context, and it is required to lookup the Azure AD Service Principal.
  • The function that generates oAuth token for the Service Principal is copied from my PowerShell module AzureServicePrincipalAccount (GitHub, PSGallery). I took it out instead of using the module so we don’t have to worry about installing the module to the VSTS agents.
  • I have put a lot of information in the verbose stream. Use –verbose to view them

i.e. the output when I deployed the sample template from my previous post:

image

Using it in VSTS

To demonstrate this, I created a very simple project and pipelines in VSTS, in real life, you’d probably want to have additional tasks in your pipelines (i.e. running pester tests, etc.).

Creating a variable group to retrieve the Service Principal key from key vault (you will need to store it in KV manually first)

image

Create Build Pipeline

image

Firstly, associate the variable group to the build:

image

The first Azure PowerShell task “Get Azure Subscription Details” retrieves the tenant and subscription Id from AzureRM context and store them as variables. here’s the inline script:

$Context = Get-AzureRMContext
$TenantId = $Context.Tenant.Id
$SubId = $Context.subscription.Id
Write-Output ("##vso[task.setvariable variable=TenantId]$TenantId")
Write-Output ("##vso[task.setvariable variable=SubscriptionId]$SubId")

image

The second Azure PowerShell task executes the Deploy-SubscriptionLevelTemplate.ps1 script with the following parameters:

-TenantId $(TenantId) -SubscriptionId $(SubscriptionId) -location ‘australiasoutheast’ -ServicePrincipalAppNamePrefix ‘vsts’ -ServicePrincipalKey $(vsts) -TemplateFilePath “$(Build.SourcesDirectory)/ARMDeploymentDemo/SubLevelTemplateDemo/tagDemo.azuredeploy.json” -ParameterFilePath “$(Build.SourcesDirectory)/ARMDeploymentDemo/SubLevelTemplateDemo/tagDemo.azuredeploy.parameters.json” -validate $true

image

Note: modify the parameters to suit your own needs. Lastly, add a MSBuild step and a Publish Build Artifact step so the release task can access the output of the build pipeline.

Create Release Pipeline

firstly, create one or more environments according to your requirement. In this demo, I only have one:

image

Link the variable group either to an environment or to the release

image

Create two Azure PowerShell tasks. first task is to get the tenant Id and subscription Id, same as the build pipeline. the second task is executing the Deploy-SubscriptionLevelTemplate.ps1 script. note the input parameters are slightly different (different VSTS variables are used)

image

Input parameters (change it according to your requirements):

-TenantId $(TenantId) -SubscriptionId $(SubscriptionId) -location ‘australiasoutheast’ -ServicePrincipalAppNamePrefix ‘vsts’ -ServicePrincipalKey $(vsts) -TemplateFilePath “$(System.DefaultWorkingDirectory)/_ARMDeploymentDemo-CI/drop/SubLevelTemplateDemo/tagDemo.azuredeploy.json” -ParameterFilePath “$(System.DefaultWorkingDirectory)/_ARMDeploymentDemo-CI/drop/SubLevelTemplateDemo/tagDemo.azuredeploy.parameters.json” –verbose

Build Pipeline Execution output

image

Release Pipeline Execution output

image

If the template or parameter files contain errors, the build would fail:

image

Summary

This script is designed primarily for VSTS Azure PowerShell task, therefore I’m using an AAD Service Principal.

It does not support specifying ARM input parameters as script input or specifying ARM template and parameters files in a URI format (like what AzureRM module does), because I don’t have such a requirement. Feel free to modify this script to suit your requirements. Suggestions are always welcome. just leave me a message in the comment area.

Leave a comment