Создавать ресурсы кликами в портале удобно ровно до второго раза. Дальше начинаются вопросы: как воспроизвести то же самое в другой подписке? как понять, что вообще создано? как откатить изменение? Ответ — инфраструктура как код (IaC): инфраструктуру описывают текстовыми файлами, которые хранят в git, ревьюят и применяют. В Azure для этого есть три инструмента. Общие принципы IaC, не привязанные к облаку, разбирает статья про основы infrastructure as code; здесь — как это устроено в Azure.
Обе дороги декларативные, а расходятся на состоянии: 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 на группы, а не на подписку.
- Секреты в шаблоне это ссылка на хранилище секретов с включённой отдачей в развёртывания; секретные параметры помечают, сгенерированные пароли сразу кладут в хранилище.
Что почитать дальше
- Основы инфраструктуры как кода — state, идемпотентность и дрейф без привязки к провайдеру.
- Принципы конвейера доставки — как устроено применение из конвейера.
- Безопасность и наблюдаемость в Azure — хранилище секретов, на которое ссылаются шаблоны.
- Terraform — тот же инструмент подробно, с провайдерами и модулями.