TL;DR

  • Azure предоставляет deployment what-if для предварительной валидации — используйте в CI перед каждым apply
  • Azurite эмулирует Storage, Queues и Tables локально — быстрее реального Azure для storage-тестов
  • Ошибка #1: пропуск тестирования Azure Policy до тех пор, пока деплой не упадёт в продакшене

Подходит для: Команд, деплоящих в Azure с Terraform, Bicep или ARM templates Пропустить если: Вы только на AWS/GCP или используете Azure PaaS без инфраструктурного кода Время чтения: 10 минут

Ваш Terraform plan выглядит чистым. Деплой в Azure начинается. Двадцать минут спустя падает: “Azure Policy evaluation failed.” Вы тратите час выясняя, какая policy заблокировала деплой, затем ещё час на рефакторинг для соответствия. Тем временем команда заблокирована.

Тестирование Azure инфраструктуры имеет уникальные вызовы. Применение Azure Policy происходит во время деплоя. Соглашения об именовании ресурсов различаются по регионам. Eventual consistency при распространении Azure AD вызывает периодические сбои. Понимание этих паттернов — разница между гладким CI и постоянным тушением пожаров.

Реальная проблема

Azure вводит вызовы тестирования, отличные от AWS:

Azure Policy: Корпоративные Azure-подписки имеют policies, блокирующие несоответствующие деплои. Вы не узнаете о нарушениях пока terraform apply или az deployment не упадёт.

Регистрация Resource Provider: Первое использование сервиса в подписке требует регистрации provider. Тесты неожиданно падают в чистых подписках.

Задержки распространения Azure AD: Service principals, managed identities и role assignments требуют времени для распространения. Тесты, работающие локально, падают в CI.

Ограничения имён: Именование ресурсов Azure имеет сложные правила — storage accounts должны быть глобально уникальными, 3-24 строчных буквенно-цифровых символа. Key Vaults имеют другие правила. VMs имеют другие правила опять.

Deployment What-If

Операция what-if Azure валидирует деплои перед выполнением:

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

# Деплой на уровне подписки
az deployment sub what-if \
  --location eastus \
  --template-file main.bicep

Для Terraform комбинируйте plan с Azure-специфичной валидацией:

# Генерировать plan
terraform plan -out=tfplan

# Конвертировать в JSON для анализа
terraform show -json tfplan > tfplan.json

# Проверить соответствие Azure Policy (требует Azure CLI)
az policy state trigger-scan --resource-group myResourceGroup

# Или использовать Checkov с Azure правилами
checkov -f tfplan.json --framework terraform_plan

Terratest для Azure

Terratest имеет 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)

    // Получить outputs
    resourceGroupName := terraform.Output(t, terraformOptions, "resource_group_name")
    storageAccountName := terraform.Output(t, terraformOptions, "storage_account_name")

    // Проверить что storage account существует и имеет правильные свойства
    exists := azure.StorageAccountExists(t, storageAccountName, resourceGroupName, subscriptionID)
    assert.True(t, exists)

    // Проверить свойства 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")

    // Проверить что VNet существует
    exists := azure.VirtualNetworkExists(t, vnetName, resourceGroupName, subscriptionID)
    assert.True(t, exists)

    // Проверить subnets
    subnets := azure.GetVirtualNetworkSubnets(t, vnetName, resourceGroupName, subscriptionID)
    assert.GreaterOrEqual(t, len(subnets), 1)
}

Azurite для локального тестирования Storage

Azurite эмулирует сервисы Azure Storage локально:

# Установить через npm
npm install -g azurite

# Запустить все сервисы
azurite --silent --location ./azurite-data --debug ./azurite-debug.log

# Или через Docker
docker run -d \
  -p 10000:10000 \
  -p 10001:10001 \
  -p 10002:10002 \
  -v azurite-data:/data \
  mcr.microsoft.com/azure-storage/azurite

Настроить Terraform для использования Azurite:

provider "azurerm" {
  features {}

  # Для Azurite переопределить storage endpoints
  # Примечание: Полный AzureRM не поддерживает Azurite напрямую
  # Используйте этот паттерн для тестирования кода приложения, не полного Terraform
}

# Для тестирования приложения со 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
  }
}

Тесты на Python с Azurite:

import os
from azure.storage.blob import BlobServiceClient

def test_blob_operations():
    # Строка подключения 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)

    # Создать container
    container_client = blob_service.create_container("test-container")

    # Загрузить blob
    blob_client = container_client.get_blob_client("test-blob.txt")
    blob_client.upload_blob("Hello, Azure!", overwrite=True)

    # Скачать и проверить
    downloaded = blob_client.download_blob().readall()
    assert downloaded == b"Hello, Azure!"

    # Очистка
    container_client.delete_container()

Тестирование Bicep с What-If

Для Bicep деплоев интегрируйте what-if в 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

Тестирование Azure Policy

Тестируйте соответствие Azure Policy перед деплоем:

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(),
            // Намеренно несоответствующий для тестирования
            "enable_https_only": false,
        },
    }

    // Не авто-удалять - хотим проверить состояние policy
    terraform.Init(t, terraformOptions)

    // Plan должен успешно выполниться
    terraform.Plan(t, terraformOptions)

    // Но apply должен упасть из-за policy
    _, err := terraform.ApplyE(t, terraformOptions)

    // Утверждаем что ошибка связана с policy
    assert.Error(t, err)
    assert.Contains(t, err.Error(), "PolicyViolation")

    // Очистить неудачный деплой
    terraform.Destroy(t, terraformOptions)
}

Запросить соответствие policy программно:

# Запустить оценку policy
az policy state trigger-scan --resource-group myResourceGroup

# Проверить состояние соответствия
az policy state list \
  --resource-group myResourceGroup \
  --filter "complianceState eq 'NonCompliant'" \
  --query "[].{Resource:resourceId, Policy:policyDefinitionName}"

Интеграция CI/CD

GitHub Actions для 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

Подходы с помощью ИИ

Azure имеет сложные правила именования и взаимодействия policy. ИИ-инструменты помогают навигировать.

Что ИИ делает хорошо:

  • Генерация соответствующих имён ресурсов для Azure naming conventions
  • Перевод определений Azure Policy в assertions тестов
  • Создание кода Terratest из спецификаций Azure ресурсов
  • Объяснение Azure-специфичных сообщений об ошибках и решений

Что всё ещё требует людей:

  • Понимание организационных требований Azure Policy
  • Проектирование архитектуры тестов для сложных Azure Landing Zones
  • Решение какие тесты нужны с реальным Azure vs локальная эмуляция
  • Отладка проблем timing распространения Azure AD

Полезный промпт:

У меня есть Azure Terraform модуль который создаёт:

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

Сгенерируй:

1. Код Terratest для валидации всех ресурсов
2. Проверки Azure Policy которые нужно включить
3. Типичные Azure-специфичные подводные камни для тестирования
4. Тесты Azurite для операций со storage

Когда это не работает

Тестирование Azure инфраструктуры имеет ограничения:

Timing Azure AD: Role assignments и распространение managed identity могут занимать минуты. Тестам нужна логика retry и задержки.

Региональные различия: Некоторые сервисы недоступны во всех регионах. Тесты работающие в eastus падают в других регионах.

Ресурсы уровня подписки: Management groups, подписки и некоторые policies требуют повышенных прав которых CI service principals могут не иметь.

Стоимость очистки: Неудачные Terraform destroys оставляют осиротевшие ресурсы. У Azure нет таких же инструментов очистки как у AWS.

Рассмотрите дополнительные подходы:

Framework принятия решений

Используйте Azurite когда:

  • Тестируете код приложения использующий Azure Storage
  • Скорость критична (Azurite мгновенный)
  • Требуется сетевая изоляция

Используйте what-if когда:

  • Валидация Bicep/ARM деплоев
  • Проверка соответствия Azure Policy
  • Предварительный просмотр изменений

Используйте Terratest с реальным Azure когда:

  • Тестируете полные модули инфраструктуры
  • Валидация cross-resource интеграций
  • Финальная валидация перед продакшеном

Измерение успеха

МетрикаДоПослеКак отслеживать
Сбои Azure Policy в CIЧастые0Логи деплоя
Время выполнения тестов20+ мин<10 минМетрики CI
Осиротевшие тестовые ресурсыНеизвестно0Azure Cost Management
Успешность первого деплоя60%95%+История деплоев

Предупреждающие знаки что не работает:

  • what-if проходит но deploy падает
  • Нестабильные тесты из-за timing Azure AD
  • Растущий список задач ручной очистки
  • Команды обходят CI для “быстрых” деплоев

Что дальше

Начните с валидации, затем расширяйтесь до интеграции:

  1. Добавьте az deployment what-if в каждый PR
  2. Внедрите Checkov для сканирования Azure policy
  3. Настройте Azurite для локального тестирования storage
  4. Добавьте Terratest для критичных модулей инфраструктуры
  5. Настройте автоматизацию очистки для упавших тестов
  6. Отслеживайте Azure-специфичные метрики тестирования

Цель — ловить Azure-специфичные проблемы до деплоя, а не после.


Связанные статьи:

Внешние ресурсы:

Официальные ресурсы

See Also