Despliegues anidados de plantillas ARM automatizados con PowerShell y Azure Blob Storage

A la hora de desplegar una plantilla de Azure Resource Manager, el proceso es bastante sencillo tanto desde la línea de comandos multiplataforma como desde PowerShell. Se resume en un comando al que se le pasa el grupo de recursos y la plantilla que se quiere desplegar. Sin embargo, cuando en lugar de una única plantilla queremos hacer un despliegue de una solución más compleja compuesta por varias plantillas anidadas, esto requiere incluir algún elemento intermedio para lograrlo.

Desde una plantilla de ARM es posible invocar el despliegue de otras plantillas ARM con una única condición: deben de estar accesibles de forma pública al orquestador que las interpreta en Azure para que pueda descargársela e implementar los recursos que hemos definido. El uso de plantillas anidadas puede darse por diversos motivos, generalmente los principales suelen ser: evitar plantillas de varios miles de líneas y facilitar el trabajo de la persona o el equipo de personas que escriben dicha plantilla.

Una solución bastante común al problema es hacer uso de un repositorio público de código como lo que ofrece GitHub. Este modo es el que emplea el propio repositorio público de plantillas que Azure pone a vuestra disposición para implementar directamente o para tomar como ejemplo a la hora de crear nuestras propias plantillas. Sin embargo, en muchas ocasiones no nos interesa que nuestras plantillas estén accesibles por terceros dentro de Github. Es ahí donde entra en juego la solución que empleo yo. Probablemente no sea la mejor solución o la más completa pero es la que me facilita el trabajo con las plantillas anidadas en varios despliegues preparados para diferentes clientes. El sistema funciona de la siguiente manera.

Lo primero es crear un grupo de recursos destinado únicamente a alojar los ficheros asociados a nuestro despliegue y dentro de él definir una cuenta de almacenamiento.

New-AzureRmResourceGroup -Name $artifactsResourceGroupName -Location $artifactsResourceGroupLocation 
New-AzureRmStorageAccount -ResourceGroupName $artifactsResourceGroupName -Location $artifactsResourceGroupLocation -Name $artifactsResourceGroupStorageAccountName -SkuName Standard_GRS -Kind Storage

Tras ello, seleccionar los ficheros que se quieren subir. Yo por defecto subo todos los ficheros relacionados con las plantillas, scripts de PowerShell utilizados en la extensión de CustomScript y otros ficheros comprimidos como extensiones de DSC o datos que se descargarán posteriormente.

$filesToUpload = Get-ChildItem ".\*" -Include *.json, *.zip, *.ps1

Una vez obtenidos los ficheros es necesario interactuar con la cuenta de almacenamiento obteniendo las claves de acceso y generando el contexto. Aquí es importante tener en cuenta que estamos mezclando módulos de ARM con módulos de ASM ya que la subida de ficheros únicamente está disponible a través del módulo de almacenamiento del modo clásico.

$artifactsResourceGroupStorageAccountKey = Get-AzureRmStorageAccountKey -ResourceGroupName $artifactsResourceGroupName -Name $artifactsResourceGroupStorageAccountName
$storageContext = New-AzureStorageContext -StorageAccountName $artifactsResourceGroupStorageAccountName -StorageAccountKey $artifactsResourceGroupStorageAccountKey[0].Value
New-AzureStorageContainer -Name "artifacts" -Permission Blob -Context $storageContext
 
foreach ( $file in $filesToUpload) {
    Set-AzureStorageBlobContent -File $file.Name -Container "artifacts" -Blob $file.Name -Context $storageContext -Force 
}

Lo último es construir la URL base para nuestros ficheros con el extremo más el nombre del contenedor.

$artifactsBaseUri = $storageContext.BlobEndPoint + "artifacts"

Tras ello, definiremos un nuevo grupo de recursos dónde se realizará el despliegue de los recursos y realizamos el despliegue. Como parámetro estamos pasando la url base de los ficheros. El objetivo es que esté disponible dentro de la plantilla para construir la URL completa.

New-AzureRmResourceGroup -Name $clusterResourceGroupName -Location $clusterResourceGroupLocation -Force 
New-AzureRmResourceGroupDeployment -Name "deployment-$((Get-Date).Ticks)" -ResourceGroupName $clusterResourceGroupName -Mode Incremental -TemplateUri "$artifactsBaseUri/cluster.main.json" -ArtifactsBaseUri $artifactsBaseUri -Verbose

El acceso al blob es posible debido a que se ha dado el permiso correspondiente a la hora de crear el contenedor. Una alternativa más segura sería generar un token SAS por cada uno de los ficheros e inyectarlo como un parámetro del tipo objeto a la plantilla; sin embargo, dado que una vez desplegada la plantilla elimino el grupo de recursos temporal no es crítico el utilizar la URL del blob o utilizar el token SAS

Hasta este punto se ha cubierto los puntos básicos del funcionamiento de cómo subirlo y desplegarlo. Sin embargo, en el script completo que uso hay otros pasos que facilitan el realizar los despliegues. Entre ellos se encuentra:

  • Definir parámetros del script
  • Facilitar la selección de la suscripción sobre la que hacer el despliegue
  • Chequeo de la existencia de recursos para evitar mensajes de error por pantalla
  • Trazas por pantalla de los pasos que se van realizando. Una mejora sería sustituir el Write-Output por Write-Verbose

Este sería el script completo

Param(
    [parameter(Mandatory=$false,
    HelpMessage="Azure region where resources would be provisioned")]
    [string] $location = "North Europe",
)
 
$ErrorActionPreference = "Stop"
 
## Deployment artifacts configuration variables
$artifactsResourceGroupStorageAccountName = "exartifactssa"
$artifactsResourceGroupLocation = $location
$artifactsResourceGroupName = "example-artifacts"
 
# Add subscription name to avoid prompt message for selecting subscription
$subscription = ""
 
if (-not $subscription) {
    try {
        Write-Output "|-->; Obtaining Azure subscriptions available"
        $subscription = Get-AzureRmSubscription
    }
    catch {
        Write-Output "You need to login to Azure before continuing. Please, introduce your credentials in the windows that has appeared"
        Login-AzureRmAccount | Out-Null
        $subscription = Get-AzureRmSubscription
    }
 
    Write-Output "|--->; Obtained"
    $workingSubscription = ($subscription |  Out-GridView -Title "Select the subscription to deploy" -OutputMode Single)
 
    Select-AzureRmSubscription -SubscriptionName $workingSubscription.Name | Out-Null
}
else {
    Write-Output "|-->; Configuring subscription $subscription"
    Select-AzureRmSubscription -SubscriptionName $subscription
    Write-Output "|--->; Configured"
}
 
if ( -Not (Get-AzureRmResourceGroup -Name $artifactsResourceGroupName -Location $artifactsResourceGroupLocation -ErrorAction SilentlyContinue) ) {
    Write-Output "|-->; Creating $artifactsResourceGroupName artifacts resource group"
    New-AzureRmResourceGroup -Name $artifactsResourceGroupName -Location $artifactsResourceGroupLocation | Out-Null
    Write-Output "|--->; Created"
}
else {
    Write-Output "|-->; An artifacts resource group already exists. Skipping"
}
 
if ( -Not (Get-AzureRmStorageAccount -ResourceGroupName $artifactsResourceGroupName -ErrorAction SilentlyContinue | Where-Object { $_.StorageAccountName -like "$artifactsResourceGroupStorageAccountName*" } ) ) {
    Write-Output "|-->; Creating $artifactsResourceGroupStorageAccountName artifacts storage account"
    New-AzureRmStorageAccount -ResourceGroupName $artifactsResourceGroupName -Location $artifactsResourceGroupLocation `
        -Name $artifactsResourceGroupStorageAccountName -SkuName Standard_GRS -Kind Storage | Out-Null
    Write-Output "|--->; Created"
}
else {
    Write-Output "|-->; An artifacts storage account already exists. Skipping"
    $artifactsResourceGroupStorageAccountName = $(Get-AzureRmStorageAccount -ResourceGroupName $artifactsResourceGroupName -ErrorAction SilentlyContinue | Where-Object { $_.StorageAccountName -like "$artifactsResourceGroupStorageAccountNameP*" } | Select-Object StorageAccountName).StorageAccountName
}
 
# Obtaining files
$filesToUpload = Get-ChildItem ".\*" -Include *.json, *.zip, *.ps1
$artifactsResourceGroupStorageAccountKey = Get-AzureRmStorageAccountKey -ResourceGroupName $artifactsResourceGroupName -Name $artifactsResourceGroupStorageAccountName
$storageContext = New-AzureStorageContext -StorageAccountName $artifactsResourceGroupStorageAccountName -StorageAccountKey $artifactsResourceGroupStorageAccountKey[0].Value
 
if ( -Not (Get-AzureStorageContainer -Name "artifacts" -Context $storageContext -ErrorAction SilentlyContinue) ) {
    Write-Output "|-->; Creating artifacts container"
    New-AzureStorageContainer -Name "artifacts" -Permission Blob -Context $storageContext | Out-Null
    Write-Output "|--->; Created"
}
else {
    Write-Output "|-->; An artifacts container already exists. Skipping"
}
 
foreach ( $file in $filesToUpload) {
    Write-Output "|-->; Uploading file $($file.Name)"
    Set-AzureStorageBlobContent -File $file.Name -Container "artifacts" -Blob $file.Name -Context $storageContext -Force | Out-Null
    Write-Output "|--->t; Uploaded"
}
 
$artifactsBaseUri = $storageContext.BlobEndPoint + "artifacts"
 
if ( -Not (Get-AzureRmResourceGroup -Name $clusterResourceGroupName -Location $clusterResourceGroupLocation -ErrorAction SilentlyContinue) ) {
    Write-Output "|-->; Creating resource group"
    New-AzureRmResourceGroup -Name $clusterResourceGroupName -Location $clusterResourceGroupLocation -Force | Out-Null
    Write-Output "|--->; Created"
}
else {
    Write-Output "|--->; A resource group already exists. Skipping"
}
 
Write-Output "|-->; Starting deployment of the cluster"
New-AzureRmResourceGroupDeployment -Name "deployment-$((Get-Date).Ticks)" -ResourceGroupName $clusterResourceGroupName  `
    -Mode Incremental -TemplateUri "$artifactsBaseUri/your.template.file.json" `
    -ArtifactsBaseUri $artifactsBaseUri `
    -Verbose

Leave a Reply

Your email address will not be published. Required fields are marked *