TL;DR

  • Azure proporciona deployment what-if para validación pre-despliegue — úsalo en CI antes de cada apply
  • Azurite emula Storage, Queues y Tables localmente — más rápido que Azure real para tests de storage
  • El error #1: saltarse testing de Azure Policy hasta que el despliegue falla en producción

Ideal para: Equipos desplegando en Azure con Terraform, Bicep o plantillas ARM Omitir si: Estás solo en AWS/GCP o usas Azure PaaS sin código de infraestructura Tiempo de lectura: 10 minutos

Tu plan de Terraform se ve limpio. El despliegue a Azure comienza. Veinte minutos después, falla: “Azure Policy evaluation failed.” Pasas una hora averiguando qué policy bloqueó el despliegue, luego otra hora refactorizando para cumplir. Mientras tanto, el equipo está bloqueado.

El testing de infraestructura Azure tiene desafíos únicos. La aplicación de Azure Policy ocurre en tiempo de despliegue. Las convenciones de nombres de recursos varían por región. La consistencia eventual en la propagación de Azure AD causa fallos intermitentes. Entender estos patrones marca la diferencia entre CI fluido y constante apagar incendios.

El Problema Real

Azure introduce desafíos de testing diferentes a AWS:

Azure Policy: Las suscripciones Azure empresariales tienen policies que bloquean despliegues no conformes. No sabes sobre violaciones hasta que terraform apply o az deployment falla.

Registro de Resource Provider: El primer uso de un servicio en una suscripción requiere registro del provider. Los tests fallan inesperadamente en suscripciones limpias.

Retrasos de propagación Azure AD: Service principals, managed identities y role assignments toman tiempo en propagarse. Tests que funcionan localmente fallan en CI.

Restricciones de nombres: Los nombres de recursos Azure tienen reglas complejas — storage accounts deben ser globalmente únicos, 3-24 caracteres alfanuméricos en minúscula. Key Vaults tienen reglas diferentes. VMs tienen reglas diferentes otra vez.

Deployment What-If

La operación what-if de Azure valida despliegues antes de ejecutarlos:

# ARM/Bicep what-if
az deployment group what-if \
  --resource-group myResourceGroup \
  --template-file main.bicep \
  --parameters @params.json

# Despliegue a nivel de suscripción
az deployment sub what-if \
  --location eastus \
  --template-file main.bicep

Para Terraform, combina plan con validación específica de Azure:

# Generar plan
terraform plan -out=tfplan

# Convertir a JSON para análisis
terraform show -json tfplan > tfplan.json

# Verificar cumplimiento de Azure Policy (requiere Azure CLI)
az policy state trigger-scan --resource-group myResourceGroup

# O usar Checkov con reglas Azure
checkov -f tfplan.json --framework terraform_plan

Terratest para Azure

Terratest tiene módulos específicos para Azure:

package test

import (
    "testing"

    "github.com/gruntwork-io/terratest/modules/azure"
    "github.com/gruntwork-io/terratest/modules/terraform"
    "github.com/stretchr/testify/assert"
)

func TestAzureStorageAccount(t *testing.T) {
    t.Parallel()

    subscriptionID := azure.GetSubscriptionID()
    uniqueID := random.UniqueId()

    terraformOptions := &terraform.Options{
        TerraformDir: "../modules/storage-account",
        Vars: map[string]interface{}{
            "resource_group_name":  "rg-test-" + uniqueID,
            "storage_account_name": "sttest" + uniqueID,
            "location":             "eastus",
        },
    }

    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)

    // Obtener outputs
    resourceGroupName := terraform.Output(t, terraformOptions, "resource_group_name")
    storageAccountName := terraform.Output(t, terraformOptions, "storage_account_name")

    // Verificar que storage account existe y tiene propiedades correctas
    exists := azure.StorageAccountExists(t, storageAccountName, resourceGroupName, subscriptionID)
    assert.True(t, exists)

    // Verificar propiedades de storage account
    storageAccount := azure.GetStorageAccount(t, storageAccountName, resourceGroupName, subscriptionID)
    assert.Equal(t, "Standard_LRS", string(storageAccount.Sku.Name))
    assert.True(t, *storageAccount.EnableHTTPSTrafficOnly)
}

func TestAzureVirtualNetwork(t *testing.T) {
    t.Parallel()

    subscriptionID := azure.GetSubscriptionID()
    uniqueID := random.UniqueId()

    terraformOptions := &terraform.Options{
        TerraformDir: "../modules/virtual-network",
        Vars: map[string]interface{}{
            "resource_group_name": "rg-test-" + uniqueID,
            "vnet_name":          "vnet-test-" + uniqueID,
            "address_space":      []string{"10.0.0.0/16"},
            "location":           "eastus",
        },
    }

    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)

    vnetName := terraform.Output(t, terraformOptions, "vnet_name")
    resourceGroupName := terraform.Output(t, terraformOptions, "resource_group_name")

    // Verificar que VNet existe
    exists := azure.VirtualNetworkExists(t, vnetName, resourceGroupName, subscriptionID)
    assert.True(t, exists)

    // Verificar subnets
    subnets := azure.GetVirtualNetworkSubnets(t, vnetName, resourceGroupName, subscriptionID)
    assert.GreaterOrEqual(t, len(subnets), 1)
}

Azurite para Testing Local de Storage

Azurite emula servicios de Azure Storage localmente:

# Instalar via npm
npm install -g azurite

# Iniciar todos los servicios
azurite --silent --location ./azurite-data --debug ./azurite-debug.log

# O via Docker
docker run -d \
  -p 10000:10000 \
  -p 10001:10001 \
  -p 10002:10002 \
  -v azurite-data:/data \
  mcr.microsoft.com/azure-storage/azurite

Configurar Terraform para usar Azurite:

provider "azurerm" {
  features {}

  # Para Azurite, sobrescribir endpoints de storage
  # Nota: AzureRM completo no soporta Azurite directamente
  # Usa este patrón para testing de código de app, no Terraform completo
}

# Para testing de aplicación con storage
resource "null_resource" "test_storage" {
  provisioner "local-exec" {
    command = <<-EOT
      export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1"
      python test_storage_operations.py
    EOT
  }
}

Tests en Python con Azurite:

import os
from azure.storage.blob import BlobServiceClient

def test_blob_operations():
    # String de conexión Azurite
    connection_string = os.environ.get(
        "AZURE_STORAGE_CONNECTION_STRING",
        "DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1"
    )

    blob_service = BlobServiceClient.from_connection_string(connection_string)

    # Crear container
    container_client = blob_service.create_container("test-container")

    # Subir blob
    blob_client = container_client.get_blob_client("test-blob.txt")
    blob_client.upload_blob("Hello, Azure!", overwrite=True)

    # Descargar y verificar
    downloaded = blob_client.download_blob().readall()
    assert downloaded == b"Hello, Azure!"

    # Limpieza
    container_client.delete_container()

Testing de Bicep con What-If

Para despliegues Bicep, integra what-if en CI:

# azure-pipelines.yml
trigger:
  paths:
    include:

      - infra/**

stages:

  - stage: Validate
    jobs:

      - job: BicepValidation
        pool:
          vmImage: ubuntu-latest
        steps:

          - task: AzureCLI@2
            displayName: 'Bicep Lint'
            inputs:
              azureSubscription: 'MyServiceConnection'
              scriptType: bash
              scriptLocation: inlineScript
              inlineScript: |
                az bicep build --file infra/main.bicep --stdout > /dev/null

          - task: AzureCLI@2
            displayName: 'What-If Analysis'
            inputs:
              azureSubscription: 'MyServiceConnection'
              scriptType: bash
              scriptLocation: inlineScript
              inlineScript: |
                az deployment group what-if \
                  --resource-group $(ResourceGroup) \
                  --template-file infra/main.bicep \
                  --parameters infra/params.$(Environment).json

  - stage: Deploy
    dependsOn: Validate
    condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
    jobs:

      - deployment: DeployInfra
        environment: production
        strategy:
          runOnce:
            deploy:
              steps:

                - task: AzureCLI@2
                  inputs:
                    azureSubscription: 'MyServiceConnection'
                    scriptType: bash
                    scriptLocation: inlineScript
                    inlineScript: |
                      az deployment group create \
                        --resource-group $(ResourceGroup) \
                        --template-file infra/main.bicep \
                        --parameters infra/params.$(Environment).json

Testing de Azure Policy

Prueba cumplimiento de Azure Policy antes del despliegue:

func TestAzurePolicyCompliance(t *testing.T) {
    t.Parallel()

    subscriptionID := azure.GetSubscriptionID()

    terraformOptions := &terraform.Options{
        TerraformDir: "../modules/storage-account",
        Vars: map[string]interface{}{
            "resource_group_name":  "rg-policy-test",
            "storage_account_name": "stpolicytest" + random.UniqueId(),
            // Intencionalmente no conforme para testing
            "enable_https_only": false,
        },
    }

    // No auto-destruir - queremos verificar estado de policy
    terraform.Init(t, terraformOptions)

    // Plan debería tener éxito
    terraform.Plan(t, terraformOptions)

    // Pero apply debería fallar por policy
    _, err := terraform.ApplyE(t, terraformOptions)

    // Afirmar que el error es relacionado con policy
    assert.Error(t, err)
    assert.Contains(t, err.Error(), "PolicyViolation")

    // Limpiar el despliegue fallido
    terraform.Destroy(t, terraformOptions)
}

Consultar cumplimiento de policy programáticamente:

# Disparar evaluación de policy
az policy state trigger-scan --resource-group myResourceGroup

# Verificar estado de cumplimiento
az policy state list \
  --resource-group myResourceGroup \
  --filter "complianceState eq 'NonCompliant'" \
  --query "[].{Resource:resourceId, Policy:policyDefinitionName}"

Integración CI/CD

GitHub Actions para infraestructura Azure:

name: Azure Infrastructure

on:
  pull_request:
    paths:

      - 'terraform/**'

permissions:
  id-token: write
  contents: read
  pull-requests: write

jobs:
  validate:
    runs-on: ubuntu-latest
    steps:

      - uses: actions/checkout@v4

      - name: Azure Login (OIDC)
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3

      - name: Terraform Init
        run: terraform init
        working-directory: terraform

      - name: Terraform Validate
        run: terraform validate
        working-directory: terraform

      - name: Terraform Plan
        id: plan
        run: terraform plan -out=tfplan -no-color
        working-directory: terraform
        continue-on-error: true

      - name: Run Checkov
        uses: bridgecrewio/checkov-action@v12
        with:
          directory: terraform/
          framework: terraform

      - name: Comment PR
        uses: actions/github-script@v7
        with:
          script: |
            const output = `#### Terraform Plan 📖
            \`\`\`
            ${{ steps.plan.outputs.stdout }}
            \`\`\`
            `;
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            })

  terratest:
    runs-on: ubuntu-latest
    needs: validate
    steps:

      - uses: actions/checkout@v4

      - name: Azure Login
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Setup Go
        uses: actions/setup-go@v5
        with:
          go-version: '1.22'

      - name: Run Terratest
        run: go test -v -timeout 30m ./tests/...
        env:
          ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
          ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
          ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
          ARM_USE_OIDC: true

Enfoques Asistidos por IA

Azure tiene reglas de nombres complejas e interacciones de policy. Las herramientas de IA ayudan a navegar esto.

Lo que la IA hace bien:

  • Generar nombres de recursos conformes para convenciones de nombres Azure
  • Traducir definiciones de Azure Policy en aserciones de test
  • Crear código Terratest desde especificaciones de recursos Azure
  • Explicar mensajes de error específicos de Azure y soluciones

Lo que todavía necesita humanos:

  • Entender requisitos organizacionales de Azure Policy
  • Diseñar arquitectura de tests para Azure Landing Zones complejas
  • Decidir qué tests necesitan Azure real vs emulación local
  • Depurar problemas de timing de propagación Azure AD

Prompt útil:

Tengo un módulo Terraform de Azure que crea:

- Resource Group
- Storage Account con blob containers
- Key Vault con access policies
- Azure Functions con managed identity

Genera:

1. Código Terratest para validar todos los recursos
2. Verificaciones de Azure Policy que debería incluir
3. Errores comunes específicos de Azure a probar
4. Tests de Azurite para operaciones de storage

Cuándo Esto Falla

El testing de infraestructura Azure tiene limitaciones:

Timing de Azure AD: Role assignments y propagación de managed identity pueden tomar minutos. Los tests necesitan lógica de retry y delays.

Diferencias regionales: Algunos servicios no están disponibles en todas las regiones. Tests que funcionan en eastus fallan en otras regiones.

Recursos a nivel de suscripción: Management groups, suscripciones y algunas policies requieren permisos elevados que service principals de CI pueden no tener.

Costo de limpieza: Terraform destroys fallidos dejan recursos huérfanos. Azure no tiene las mismas herramientas de limpieza que AWS.

Considera enfoques complementarios:

Framework de Decisión

Usa Azurite cuando:

  • Probando código de aplicación que usa Azure Storage
  • La velocidad es crítica (Azurite es instantáneo)
  • Se requiere aislamiento de red

Usa what-if cuando:

  • Validando despliegues Bicep/ARM
  • Verificando cumplimiento de Azure Policy
  • Revisión de cambios pre-despliegue

Usa Terratest con Azure real cuando:

  • Probando módulos de infraestructura completos
  • Validando integraciones cross-resource
  • Validación final antes de producción

Midiendo el Éxito

MétricaAntesDespuésCómo Rastrear
Fallos de Azure Policy en CIFrecuentes0Logs de despliegue
Tiempo de ejecución de tests20+ min<10 minMétricas de CI
Recursos de test huérfanosDesconocido0Azure Cost Management
Tasa de éxito primer despliegue60%95%+Historial de despliegues

Señales de advertencia de que no está funcionando:

  • what-if pasa pero deploy falla
  • Tests inestables por timing de Azure AD
  • Lista creciente de tareas de limpieza manual
  • Equipos evitando CI para despliegues “rápidos”

Qué Sigue

Comienza con validación, luego expande a integración:

  1. Agrega az deployment what-if a cada PR
  2. Implementa Checkov para escaneo de policy Azure
  3. Configura Azurite para testing local de storage
  4. Agrega Terratest para módulos de infraestructura críticos
  5. Configura automatización de limpieza para tests fallidos
  6. Rastrea métricas de test específicas de Azure

El objetivo es capturar problemas específicos de Azure antes del despliegue, no después.


Artículos relacionados:

Recursos externos:

Recursos Oficiales

See Also