Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Deploying Azure Subscription level ARM template via ARM REST API
#requires -Modules AzureRM.Profile, AzureRM.Resources
<#
========================================================================================================================================
AUTHOR: Tao Yang
DATE: 23/05/2018
Version: 1.0
Comment: - Script used in VSTS pipelines to deploy Azure Policy and Initiative ARM template
- This script is required because subscription-level deployment not available via the ARM SDK and VSTS ARM deployment task yet
========================================================================================================================================
#>
[CmdLetBinding()]
param(
[Parameter(Mandatory = $true)]
[ValidateScript({
try {
[System.Guid]::Parse($_) | Out-Null
$true
} catch {
$false
}
})]
[string]$TenantId,
[Parameter(Mandatory = $true)]
[ValidateScript({
try {
[System.Guid]::Parse($_) | Out-Null
$true
} catch {
$false
}
})]
[string]$SubscriptionId,
[parameter(Mandatory=$false)][ValidateNotNullOrEmpty()][string]$location='australiasoutheast',
[parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$ServicePrincipalAppNamePrefix,
[parameter(Mandatory=$true)][ValidateNotNullOrEmpty()][string]$ServicePrincipalKey,
[Parameter(Mandatory=$true)][validatescript({test-path $_ -PathType Leaf})][string]$TemplateFilePath,
[Parameter(Mandatory=$false)][validatescript({test-path $_ -PathType Leaf})][string]$ParameterFilePath,
[parameter(Mandatory=$false)][ValidateRange(1,120)][int]$MaximumWaitMinutes = 30,
[parameter(Mandatory=$false)][boolean]$Validate = $false
)
#region functions
Function Get-AzureADTokenForServicePrincipal
{
[CmdletBinding()]
[OutputType([string])]
PARAM (
[Parameter(Mandatory=$true)]
[ValidateScript({
try
{
[System.Guid]::Parse($_) | Out-Null
$true
}
catch
{
$false
}
})]
[Alias('tID')]
[String]$TenantID,
[Parameter(Mandatory = $true,HelpMessage = 'Please specify the Azure AD credential')]
[Alias('cred')]
[ValidateNotNullOrEmpty()]
[PSCredential]$Credential,
[Parameter(Mandatory = $false)]
[String][ValidateNotNullOrEmpty()]$OAuthURI,
[Parameter(Mandatory = $false)]
[String][ValidateNotNullOrEmpty()]$ResourceURI ='https://management.azure.com/'
)
$ClientId = $Credential.UserName
$ClientSecret = $Credential.GetNetworkCredential().Password
#URI to get oAuth Access Token
If (!$PSBoundParameters.ContainsKey('oAuthURI'))
{
$oAuthURI = "https://login.microsoftonline.com/$TenantId/oauth2/token"
}
#oAuth token request
$body = 'grant_type=client_credentials'
$body += '&client_id=' + $ClientId
$body += '&client_secret=' + [Uri]::EscapeDataString($ClientSecret)
$body += '&resource=' + [Uri]::EscapeDataString($ResourceURI)
$response = Invoke-RestMethod -Method POST -Uri $oAuthURI -Headers @{} -Body $body
$Token = "Bearer $($response.access_token)"
$Token
}
Function ValidateSubscriptionLevelARMTemplate
{
Param (
[Parameter(Mandatory = $true)][Hashtable]$RequestHeaders,
[Parameter(Mandatory = $true)]
[ValidateScript({
try
{
[System.Guid]::Parse($_) | Out-Null
$true
}
catch
{
$false
}
})]
[String]$SubscriptionId,
[Parameter(Mandatory = $true)][validatescript({test-path $_ -PathType Leaf})][String]$TemplateFilePath,
[Parameter(Mandatory = $false)][validatescript({test-path $_ -PathType Leaf})][string]$ParameterFilePath,
[Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][String]$location
)
$UTCNow = (get-date).ToUniversalTime().tostring('yyyyMMddHHmmss')
$DeploymentName = "SubLevelDeployment-$UTCNow"
$URI = "https://management.azure.com/subscriptions/$SubscriptionId/providers/microsoft.resources/deployments/$DeploymentName/validate?api-version=2018-05-01"
$TemplateContent = Get-Content -Path $TemplateFilePath -Raw
If ($PSBoundParameters.ContainsKey('ParameterFilePath'))
{
$ParametersFileJSONContent = ConvertFrom-JSON $(Get-Content -Path $ParameterFilePath -Raw)
$ParametersContent = ConvertTo-JSON $ParametersFileJSONContent.parameters
$RequestBody = @"
{
"properties": {
"template":
$TemplateContent
,
"parameters":
$ParametersContent
,
"mode": "Incremental"
},
"location": "$location"
}
"@
} else {
$RequestBody = @"
{
"properties": {
"template":
$TemplateContent
,
"mode": "Incremental"
},
"location": "$location"
}
"@
}
Write-Verbose "Request body:"
Write-Verbose $RequestBody
Try {
$ValidateTemplateRequest = Invoke-WebRequest -UseBasicParsing -Uri $URI -Method POST -Headers $RequestHeaders -Body $RequestBody -ContentType 'application/json'
$return = $DeploymentName
If ($ValidateTemplateRequest.statuscode -ge 200 -and $ValidateTemplateRequest.statuscode -le 299)
{
Write-verbose "Template validation succeeded."
$return = ConvertFrom-Json $ValidateTemplateRequest.Content
} else {
#Write-Error $DeployTemplateRequest.rawContent
Write-Error "The template validation for deployment '$DeploymentName' has failed. Error Code: $($ValidateTemplateRequest.properties.error.code), Error Message: $($ValidateTemplateRequest.properties.error.message)"
Foreach ($Detail in $ValidateTemplateRequest.properties.error.details)
{
Write-Error "$Detail.message"
}
}
} Catch {
Write-Error $_.ErrorDetails
$return = $_
}
$return
}
Function DeploySubscriptionLevelARMTemplate
{
Param (
[Parameter(Mandatory = $true)][Hashtable]$RequestHeaders,
[Parameter(Mandatory = $true)]
[ValidateScript({
try
{
[System.Guid]::Parse($_) | Out-Null
$true
}
catch
{
$false
}
})]
[String]$SubscriptionId,
[Parameter(Mandatory = $true)][validatescript({test-path $_ -PathType Leaf})][String]$TemplateFilePath,
[Parameter(Mandatory = $false)][validatescript({test-path $_ -PathType Leaf})][string]$ParameterFilePath,
[Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][String]$location
)
$UTCNow = (get-date).ToUniversalTime().tostring('yyyyMMddHHmmss')
$DeploymentName = "SubLevelDeployment-$UTCNow"
$URI = "https://management.azure.com/subscriptions/$SubscriptionId/providers/microsoft.resources/deployments/$DeploymentName`?api-version=2018-05-01"
$TemplateContent = Get-Content -Path $TemplateFilePath -Raw
If ($PSBoundParameters.ContainsKey('ParameterFilePath'))
{
$ParametersFileJSONContent = ConvertFrom-JSON $(Get-Content -Path $ParameterFilePath -Raw)
$ParametersContent = ConvertTo-JSON $ParametersFileJSONContent.parameters
$RequestBody = @"
{
"properties": {
"template":
$TemplateContent
,
"parameters":
$ParametersContent
,
"mode": "Incremental"
},
"location": "$location"
}
"@
} else {
$RequestBody = @"
{
"properties": {
"template":
$TemplateContent
,
"mode": "Incremental"
},
"location": "$location"
}
"@
}
Write-Verbose "Request body:"
Write-Verbose $RequestBody
Try {
$DeployTemplateRequest = Invoke-WebRequest -UseBasicParsing -Uri $URI -Method PUT -Headers $RequestHeaders -Body $RequestBody -ContentType 'application/json'
$return = $DeploymentName
If ($DeployTemplateRequest.statuscode -ge 200 -and $DeployTemplateRequest.statuscode -le 299)
{
Write-verbose "Template deployment request successfully submitted."
} else {
#Write-Error $DeployTemplateRequest.rawContent
Write-Error "The deployment '$DeploymentName' has failed. Error Code: $($DeployTemplateRequest.properties.error.code), Error Message: $($DeployTemplateRequest.properties.error.message)"
Foreach ($Detail in $DeployTemplateRequest.properties.error.details)
{
Write-Error "$Detail.message"
}
}
} Catch {
$ExceptionDetails = $_.Exception.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($ExceptionDetails)
$ExceptionResponse = $reader.ReadToEnd();
Write-Error $ExceptionResponse
Write-Error $_.ErrorDetails
throw $_.Exception
$return = $null
}
$return
}
Function GetSubscriptionLevelDeployment
{
Param (
[Parameter(Mandatory = $true)][Hashtable]$RequestHeaders,
[Parameter(Mandatory = $true)]
[ValidateScript({
try
{
[System.Guid]::Parse($_) | Out-Null
$true
}
catch
{
$false
}
})]
[String]$SubscriptionId,
[Parameter(Mandatory = $true)][ValidateNotNullOrEmpty()][String]$DeploymentName
)
$URI = "https://management.azure.com/subscriptions/$SubscriptionId/providers/microsoft.resources/deployments/$DeploymentName`?api-version=2018-05-01"
Try {
$GetDeploymentRequest = Invoke-WebRequest -UseBasicParsing -Uri $URI -Method GET -Headers $RequestHeaders
If ($GetDeploymentRequest.statuscode -ge 200 -and $GetDeploymentRequest.statuscode -le 299)
{
Write-verbose "Template deployment $DeploymentName is found."
$return = ConvertFrom-Json $GetDeploymentRequest.content
} else {
Write-Error $GetDeploymentRequest.rawContent
}
} Catch {
$ExceptionDetails = $_.Exception.Response.GetResponseStream()
$reader = New-Object System.IO.StreamReader($ExceptionDetails)
$ExceptionResponse = $reader.ReadToEnd();
Write-Error $ExceptionResponse
Write-Error $_.ErrorDetails
throw $_.Exception
$return = $null
}
$return
}
#endregion
#region main
#Generate oAuth token for ARM REST API calls
Write-Output "Looking up Application with name starts with '$ServicePrincipalAppNamePrefix'"
[string]$AppId = (Get-AzureRmADApplication -DisplayNameStartWith $ServicePrincipalAppNamePrefix).ApplicationId.guid
Write-output "AAD Application App Id: '$AppId'."
$SecServicePrincipalKey = ConvertTo-SecureString -String $ServicePrincipalKey -AsPlainText -Force
$SPCred = New-Object System.Management.Automation.PSCredential($AppId, $SecServicePrincipalKey)
Write-Output "Generating Azure AD oAuth token for ARM REST API calls..."
$Token = Get-AzureADTokenForServicePrincipal -TenantID $TenantId -Credential $SPCred
$RequestHeaders = @{'Authorization' = $Token}
#Deploy the template
$DeployParam = @{
RequestHeaders = $RequestHeaders
SubscriptionId = $SubscriptionId
TemplateFilePath = $TemplateFilePath
location = $location
}
If ($PSBoundParameters.ContainsKey('ParameterFilePath'))
{
$DeployParam.Add('ParameterFilePath', $ParameterFilePath)
if ($Validate)
{
Write-Output "Validating template '$TemplateFIlePath with Parameter file '$ParameterFilePath'..."
} else {
Write-Output "Deploying template '$TemplateFIlePath with Parameter file '$ParameterFilePath'..."
}
} else {
if ($Validate)
{
Write-Output "Validating template '$TemplateFilePath'..."
} else {
Write-Output "Deploying template '$TemplateFilePath'..."
}
}
if ($Validate)
{
Write-Verbose "Template validation mode"
$ValidationResult = ValidateSubscriptionLevelARMTemplate @DeployParam
If ($ValidationResult -is [System.Management.Automation.ErrorRecord])
{
Write-Error "Template Validation failed."
} elseif ($ValidationResult.properties.provisioningState -ieq 'succeeded')
{
Write-Output "Template Validation passed."
}
} else {
$DeploymentName = DeploySubscriptionLevelARMTemplate @DeployParam
#Wait for Deployment to finish
Write-Output "Waiting for deployment '$DeploymentName' to finish..."
$WaitStartTime = (Get-Date).ToUniversalTime()
$WaitFinishTime = $WaitStartTime.AddMinutes($MaximumWaitMinutes)
Write-Output "Maximum wait time - up to $WaitFinishTime (UTC)"
Start-Sleep -Seconds 5
$bWait = $true
Do {
$GetDeployment = GetSubscriptionLevelDeployment -RequestHeaders $RequestHeaders -SubscriptionId $SubscriptionId -DeploymentName $DeploymentName
$provisioningState = $GetDeployment.properties.provisioningState.tolower()
$now = (Get-Date).ToUniversalTime()
Write-Verbose "Current Time $now (UTC)"
if ($provisioningState -ine 'running' -and $provisioningState -ine 'accepted')
{
$bWait = $false
Write-Verbose "Current provisioning state: '$provisioningState'. The wait is over."
} else {
if ($now -ge $WaitFinishTime)
{
$bWait = $false
Write-Verbose "Current provisioning state: '$provisioningState'. The wait is over."
} else {
Write-Verbose "Current provisioning state: '$provisioningState'. Sleep 15 seconds..."
}
Start-Sleep -Seconds 15
}
} While ($bWait -eq $true)
#Output provisioning state
Switch ($provisioningState)
{
'running' {Write-Warining "The deployment '$DeploymentName' is still running. please manually monitor the state since it has passed the maximum wait time of $MaximumWaitMinutes minutes."}
'succeeded' {Write-Output "The deployment '$DeploymentName' has finished successfully."}
'failed' {
Write-Error "The deployment '$DeploymentName' has failed. Error Code: $($GetDeployment.properties.error.code), Error Message: $($GetDeployment.properties.error.message)"
Foreach ($Detail in $GetDeployment.properties.error.details)
{
Write-Error "$Detail.message"
}
}
Default {Write-Output "The deployment '$DeploymentName' provisioning state: '$provisioningState'"}
}
}
Write-Output "Done."
#endregion
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.