Создавать ресурсы кликами в портале удобно ровно до второго раза. Дальше начинаются вопросы: как воспроизвести то же самое в другой подписке? как понять, что вообще создано? как откатить изменение? Ответ — инфраструктура как код (IaC): инфраструктуру описывают текстовыми файлами, которые хранят в git, ревьюят и применяют. В Azure для этого есть три инструмента. Общие принципы IaC, не привязанные к облаку, разбирает статья про основы infrastructure as code; здесь — как это устроено в Azure.

Bicep шаблон и параметры what-if развёртывание лишнее остаётся, если не стек Terraform код и состояние plan apply выпавшее удаляется

Обе дороги декларативные, а расходятся на состоянии: Terraform помнит, что создавал, и удалит выпавшее из описания; Bicep сравнивает шаблон с облаком и в инкрементальном режиме не трогает то, чего в шаблоне нет.

Обязательно

ARM-шаблоны: родной формат

ARM-шаблоны (Azure Resource Manager) — «родной» способ описания ресурсов в Azure, документы в формате JSON. Всё, что вы создаёте в Azure, в итоге проходит через Resource Manager, поэтому ARM — фундамент. Минус в том, что JSON-шаблоны многословны и тяжело читаются: на реальную инфраструктуру получаются сотни строк со скобками.

Bicep: тот же ARM, но по-человечески

Bicep — это язык поверх ARM: он компилируется в те же ARM-шаблоны, но пишется в разы компактнее и читаемее. Это рекомендованный Microsoft способ для проектов, живущих только в Azure.

resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: 'mystorage${uniqueString(resourceGroup().id)}'
  location: resourceGroup().location
  sku: { name: 'Standard_LRS' }
  kind: 'StorageV2'
}

Один короткий блок Bicep разворачивается в объёмный ARM-шаблон, но вам работать с читаемой версией. Ресурсы могут ссылаться друг на друга, и Bicep сам вычисляет порядок создания.

Terraform: когда не только Azure

Terraform — независимый от вендора инструмент, который управляет ресурсами десятков платформ на одном языке (HCL) через провайдеры; для Azure это провайдер azurerm. Его берут, когда инфраструктура не только в Azure (мультиоблако) или в компании уже стандартизирован Terraform. Знание переносимо: тот же Terraform описывает и AWS, и Kubernetes.

Правило выбора простое: только Azure — берите Bicep (нативно, без лишних слоёв); несколько облаков или уже есть Terraform — берите Terraform. ARM в чистом виде сегодня пишут редко — обычно через Bicep.

Модули и повторное использование

То, ради чего инфраструктуру как код заводят на втором проекте: описать один раз и переиспользовать.

В Bicep любой файл — это модуль, и подключается он одной строкой:

module network 'modules/network.bicep' = {
  name: 'network'
  params: {
    addressPrefix: '10.10.0.0/16'
    env: env
  }
}

output subnetId string = network.outputs.appSubnetId

Модуль объявляет свои параметры (param) и выходные значения (output), и вызывающий видит только их — внутренности скрыты. Для модулей, которые нужны нескольким проектам, есть реестр модулей: модуль публикуется в реестр образов и подключается по адресу с версией.

module network 'br:acme.azurecr.io/bicep/modules/network:2.4.1' = { ... }

Версия здесь обязательна по той же причине, что и везде: без неё обновление модуля автором меняет поведение у всех сразу.

В Terraform модули устроены так же (папка с файлами плюс параметры и выходные значения), и источник у них бывает локальный, из реестра или из репозитория с указанием тега. Про версии и файл блокировки — в статье про Terraform: механика одинакова независимо от облака.

Когда модуль оправдан. Когда его вызывают минимум дважды или когда он скрывает заметную сложность (сеть с подсетями, группами и таблицами маршрутов — хороший кандидат). Модуль на один ресурс, вызываемый один раз, только добавляет уровень вложенности.

Несколько сред одним описанием

Вопрос, с которого начинается интро («как воспроизвести то же самое в другой подписке»), решается параметрами — но по-разному в двух инструментах.

В Bicep значения для среды держат в файлах параметров:

// main.bicep
param env string
param appSku string = 'B1'
param minReplicas int = 1

Параметры окружения:

// prod.bicepparam
using './main.bicep'
param env = 'prod'
param appSku = 'P1v3'
param minReplicas = 3
az deployment group create -g rg-orders-prod -f main.bicep -p prod.bicepparam

Один шаблон, два файла параметров, две группы ресурсов — и среды физически не могут задеть друг друга, потому что развёртывание идёт в разные группы (а лучше и в разные подписки).

В Terraform тот же результат получают отдельными каталогами со своим состоянием и общим модулем — это надёжнее рабочих областей, где легко перепутать активную. Разбор — в статье про состояние и доставку.

Что общего в обоих подходах и что важнее инструмента: отличия сред — это значения, а не разные описания. Как только у боевого контура появляется свой отдельный шаблон, среды начинают расходиться, и «в тесте работало» перестаёт что-либо значить.

И граница, которую стоит держать: имена ресурсов включают среду (rg-orders-prod, stordersprod), потому что часть имён в Azure глобально уникальна, а остальное — чтобы в портале было видно, куда вы смотрите.

Состояние и как это применяют

У Terraform есть состояние (state) — карта соответствия «описание ↔ реальные ресурсы». Его нельзя терять и нельзя держать локально при командной работе, иначе двое применят изменения поверх друг друга. Хранят его в общем месте — например, в Blob Storage. Блокировка там встроенная: бэкенд azurerm берёт аренду (lease) на сам файл-блоб, так что отдельную таблицу под замок, как это делают в AWS, заводить не надо. У Bicep/ARM отдельного файла состояния нет: Resource Manager сам знает, что развёрнуто, и сравнивает с шаблоном.

Идея у всех одна — декларативная: вы пишете, что должно существовать. А дальше инструменты расходятся, и это самая частая неожиданность при переезде с Terraform.

У Terraform есть состояние, поэтому он видит, что ресурс раньше был, а теперь его в описании нет, — и удаляет. У Bicep и ARM по умолчанию не так: развёртывание идёт в инкрементальном режиме, где шаблон только добавляет и меняет. Ресурс, убранный из шаблона, спокойно продолжает жить в группе ресурсов и попадать в счёт. Вычищает лишнее только полный режим (--mode Complete), который включают явно и с осторожностью: он снесёт из группы всё, чего в шаблоне нет. То есть обещание «чего нет в коде, того нет в облаке» у Bicep из коробки не выполняется — и это прямое следствие того самого различия «есть состояние / нет состояния».

Эту дыру закрывают стеки развёртывания (deployment stacks): стек ведёт учёт ресурсов, которыми управляет, и при следующем развёртывании умеет удалять выпавшие из шаблона. Если нужно поведение «как в Terraform» — смотреть надо на них.

Перед применением всегда смотрят предпросмотр изменений (what-if у Bicep, plan у Terraform), чтобы случайно не снести лишнее. Доверие к ним, правда, разное: plan опирается на состояние и довольно точен, а what-if по части свойств честно пишет «неизвестно» и иногда показывает изменения там, где их не будет. Смотреть — обязательно, принимать как приговор — нет.

Дрейф: кто-то поменял ресурс в портале

Ручная правка в портале — это обычное дело, и инструменты реагируют на неё по-разному.

В Terraform это классика: состояние помнит, каким ресурс был, и план покажет расхождение — значит, у вас есть выбор, вернуть реальность к коду или принять изменение в код. Разбор обоих вариантов — в статье про состояние и доставку.

В Bicep и шаблонах ARM своего состояния нет, и это меняет картину. Инструмент сравнивает шаблон с текущим состоянием ресурсов в Azure — то есть «дрейф» обнаруживается в момент предпросмотра:

az deployment group what-if -g rg-orders-prod -f main.bicep -p prod.bicepparam

Предпросмотр покажет, что изменится, и ручная правка проявится как изменение, которое развёртывание собирается вернуть назад.

И здесь важна особенность инкрементального режима, которая одновременно и плюс, и минус: ресурс, созданный руками и не описанный в шаблоне, развёртывание не тронет. То есть лишние ресурсы так не обнаруживаются: шаблон описывает то, что описывает, а всё остальное продолжает жить. Полный режим (complete), который удаляет всё лишнее в группе ресурсов, существует — и пользуются им редко и осторожно, потому что цена ошибки в нём высока.

Практическая раскладка. Регулярный предпросмотр по расписанию на боевых группах (он не меняет ничего, только показывает) плюс оповещение, если он показал непустой результат, — так ручные правки становятся видны. Как реагировать: те же три варианта, что в любом облаке — вернуть к коду, принять в код или признать чужое владение и исключить свойство из управления.

И вторая половина той же задачи — не давать править руками: на боевые группы ресурсов не выдают права изменения людям, а выдают их конвейеру; человеку остаётся доступ на чтение и временная роль на случай аварии. Это работает лучше любых договорённостей.

Где это применяется

IaC — стандартный способ управлять инфраструктурой Azure в любом серьёзном проекте: инфраструктура лежит в git рядом с кодом, изменения проходят ревью в pull request, а применяет их конвейер доставки, а не человек руками в портале. Это убирает «а кто и что там накликал» и делает инфраструктуру воспроизводимой.

Типичные ошибки новичков:

  • пишут сырой ARM-JSON руками вместо Bicep — тонут в многословии;
  • держат Terraform state локально в командной работе — рассинхрон и затёртые изменения;
  • применяют изменения не глядя в предпросмотр — и удаляют лишнее;
  • кладут секреты прямо в шаблоны и коммитят в git — секреты берут из Key Vault, а не из кода.
Дополнительно: при первом чтении можно пропустить

Глубже: захват существующего: import и existingрасширенное

В живом проекте всё уже накликано, и IaC начинается не с чистого листа.

В Terraform существующий ресурс заводят под управление импортом — блоком import в коде или командой; критерий успеха один: план показывает «изменений нет». Подробно — в статье про основы инфраструктуры как кода, механика одинакова для всех облаков.

В Bicep есть другой приём, и он проще: ключевое слово existing. Оно означает «этот ресурс уже есть, я не создаю его, я только ссылаюсь»:

resource kv 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
  name: 'kv-orders-prod'
}

resource app 'Microsoft.App/containerApps@2024-03-01' = {
  name: 'orders'
  properties: {
    configuration: {
      secrets: [ { name: 'db-password', keyVaultUrl: '${kv.properties.vaultUri}secrets/db-password', identity: 'system' } ]
    }
  }
}

Разница принципиальная: existing не берёт ресурс под управление — он просто читает его свойства (это аналог источника данных в Terraform). Значит, хранилище секретов, созданное другой командой, можно использовать, не претендуя на него.

А вот если ресурс надо именно взять под управление, в Azure работает свойство самого механизма развёртывания: в инкрементальном режиме (он используется по умолчанию) развёртывание не удаляет то, чего нет в шаблоне, и приводит описанные ресурсы к описанному состоянию. То есть достаточно описать существующий ресурс в шаблоне с теми же свойствами — и следующее развёртывание просто подтвердит их, а дальше он ваш. Критерий тот же: предпросмотр не показывает изменений.

Практический порядок для живого проекта: начать с одной группы ресурсов, описать её содержимое в шаблоне, довести предпросмотр до пустого результата, и только потом двигаться к следующей. Всё новое сразу создавать кодом.

Глубже: как конвейер получает праварасширенное

Про «применяет конвейер, а не человек» осталось сказать главное: чем конвейер входит в Azure.

Чего делать не надо. Создать приложение-регистрацию, сгенерировать ему секрет и положить в настройки конвейера. Такой секрет долгоживущий (обычно год-два), лежит в системе сборки, и его утечка означает полный доступ к подписке на весь срок. Плюс его надо помнить и вовремя менять — а забытый просроченный секрет останавливает выкаты в самый неудачный момент.

Что делают. Федерация учётных данных (workload identity federation): в Azure заводится приложение с доверием к вашей системе сборки, и конвейер получает короткоживущий токен на одну задачу. Никакого секрета нигде не хранится.

az ad app federated-credential create --id <app-id> --parameters '{
  "name": "github-main",
  "issuer": "https://token.actions.githubusercontent.com",
  "subject": "repo:acme/infra:ref:refs/heads/main",
  "audiences": ["api://AzureADTokenExchange"]
}'

Ключевая строка здесь — subject: она ограничивает доверие конкретным репозиторием и ветвью. Без такого ограничения (или с шаблоном, охватывающим лишнее) токен от чужого репозитория на той же платформе тоже подойдёт — и это самая опасная ошибка настройки, потому что снаружи всё выглядит работающим. Для запросов на изменение заводят отдельное доверие с правами только на предпросмотр, а применение разрешают только из основной ветви.

Дальше в конвейере вход выглядит так — без единого секрета:

permissions:
  id-token: write
  contents: read
steps:
  - uses: azure/login@v2
    with:
      client-id: ${{ vars.AZURE_CLIENT_ID }}
      tenant-id: ${{ vars.AZURE_TENANT_ID }}
      subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }}

И права самого приложения: Contributor на нужные группы ресурсов, а не на подписку, и отдельно — право раздавать доступ только если конвейер действительно создаёт назначения ролей. Общий принцип тот же, что в статье про доступ: у автоматизации ровно те права, которые ей нужны, и ни одним больше.

Глубже: секреты в шаблонах: как правильнорасширенное

Сказано, что секреты в шаблонах — ошибка. Правильный способ выглядит так: в файле параметров лежит ссылка на хранилище секретов, а не значение.

{
  "parameters": {
    "dbPassword": {
      "reference": {
        "keyVault": {
          "id": "/subscriptions/<sub>/resourceGroups/rg-sec/providers/Microsoft.KeyVault/vaults/kv-orders-prod"
        },
        "secretName": "db-password"
      }
    }
  }
}

Во время развёртывания механизм сам достаёт значение из хранилища и подставляет его в шаблон. В репозитории остаётся только путь к секрету; в журнале развёртывания значение не появляется, если параметр помечен как секретный (@secure() в Bicep).

Что нужно, чтобы это работало: у хранилища включена возможность отдавать значения в развёртывания (enabledForTemplateDeployment), и у того, кто запускает развёртывание, есть право читать этот секрет.

Три правила рядом с этим. Параметры с секретами всегда помечают как секретные — иначе значение попадёт в историю развёртываний, которую видно в портале. Пароли, которые генерируются при развёртывании, лучше сразу писать в хранилище секретов, а не отдавать выходным значением (выходные значения развёртывания тоже сохраняются). И для приложений правильнее вообще не передавать секрет через развёртывание: приложение читает его из хранилища само по управляемому удостоверению — тогда секрет не проходит через инфраструктурный код вовсе.

Коротко

  • ARM это родной JSON, через который проходит всё; Bicep компилируется в него и читается в разы лучше; Terraform берут, когда облаков несколько или он уже есть.
  • Модуль в Bicep это любой файл с параметрами и выходами, общие модули берут из реестра с версией; модуль оправдан от второго вызова или для скрытия сложности.
  • Среды различаются значениями, а не описаниями: файлы параметров Bicep или отдельные каталоги Terraform, разные группы ресурсов или подписки, среда в именах ресурсов.
  • Существующее в Terraform берут импортом до пустого плана, в Bicep на него ссылаются через existing, не беря под управление; инкрементальный режим не удаляет то, чего нет в шаблоне.
  • У Terraform есть состояние, и он удаляет выпавшее из описания; у Bicep состояния нет, лишние ресурсы остаются, пока не включить стеки развёртывания или полный режим.
  • Перед применением всегда предпросмотр: what-if у Bicep и plan у Terraform, и доверие к ним разное.
  • Дрейф в Terraform виден по состоянию, в Bicep по предпросмотру; ресурс, созданный руками и не описанный, никто не тронет. Права на изменение боевых групп у конвейера, у людей чтение и временная роль.
  • Конвейер входит федерацией учётных данных с subject, ограниченным репозиторием и ветвью, без секрета; права Contributor на группы, а не на подписку.
  • Секреты в шаблоне это ссылка на хранилище секретов с включённой отдачей в развёртывания; секретные параметры помечают, сгенерированные пароли сразу кладут в хранилище.

Что почитать дальше