Инфраструктура как код: Bicep и ARM

Учимся описывать облако текстовым файлом, который можно положить в git, отревьюить и развернуть одной командой — хоть в dev, хоть в прод.

Инфраструктура как код (IaC, Infrastructure as Code) — подход, при котором облачные ресурсы описываются в файлах-шаблонах и создаются автоматически, а не руками через веб-портал.

Почему кликанье в портале не масштабируется

Первое приложение в Azure почти все разворачивают одинаково: заходят в портал, нажимают «Create resource», заполняют десяток полей, ставят галочки. Это отличный способ познакомиться с сервисом. И совершенно тупиковый способ работать дальше.

Смотрите, что происходит через пару месяцев. Вам нужны три одинаковых окружения — dev, test, prod. Вы кликаете их по очереди и где-то на третьем забываете включить HTTPS-only. Прод внезапно отличается от теста, и «у меня локально работает» превращается в «у нас на тесте работает». Это называется дрейф конфигурации (configuration drift), и он всегда обнаруживается в худший момент.

Дальше хуже. Кто-то удалил правило файрвола — и никто не знает кто, когда и зачем. Новый разработчик спрашивает: «А как поднять окружение?» — и получает в ответ Confluence-страницу с 47 скриншотами, устаревшую на полгода. А ещё вы не можете сделать code review на инфраструктуру, потому что ревьюить нечего.

IaC решает всё это одним движением: инфраструктура становится обычным кодом в репозитории. У неё есть история изменений, автор каждой правки, pull request, откат к предыдущей версии и возможность развернуть точную копию продакшена за пять минут.

ARM-шаблоны и почему появился Bicep

Любое действие в Azure — из портала, из CLI, из SDK — в итоге превращается в вызов Azure Resource Manager (ARM). Это единый управляющий слой облака: он принимает описание желаемого состояния и приводит ресурсы к нему. Родной язык ARM — JSON-шаблоны. Выглядит это так:

{
  "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
  "contentVersion": "1.0.0.0",
  "parameters": {
    "storageName": { "type": "string" }
  },
  "resources": [
    {
      "type": "Microsoft.Storage/storageAccounts",
      "apiVersion": "2023-01-01",
      "name": "[parameters('storageName')]",
      "location": "[resourceGroup().location]",
      "sku": { "name": "Standard_LRS" },
      "kind": "StorageV2"
    }
  ]
}

Работоспособно — и абсолютно нечитаемо. Строковые функции в квадратных скобках, вложенность на пять уровней, никакой подсказки от редактора. Поэтому Microsoft сделал Bicep — компактный язык, который транспилируется в тот же самый ARM JSON. Bicep — это не отдельная система, а удобный синтаксис поверх ARM: никакого нового движка, никакого файла состояния, никаких дополнительных прав.

Первый шаблон Bicep

Тот же storage account, только по-человечески — файл main.bicep:

@description('Префикс имён ресурсов')
param prefix string

@allowed(['dev', 'test', 'prod'])
param env string = 'dev'

param location string = resourceGroup().location

var storageName = toLower('${prefix}${env}st${uniqueString(resourceGroup().id)}')

resource storage 'Microsoft.Storage/storageAccounts@2023-01-01' = {
  name: storageName
  location: location
  sku: {
    name: env == 'prod' ? 'Standard_GRS' : 'Standard_LRS'
  }
  kind: 'StorageV2'
  properties: {
    supportsHttpsTrafficOnly: true
    minimumTlsVersion: 'TLS1_2'
  }
  tags: {
    env: env
    owner: 'platform-team'
  }
}

output storageId string = storage.id
output blobEndpoint string = storage.properties.primaryEndpoints.blob

Здесь уже видно почти всё, что нужно для реальной работы. param — входные значения, var — вычисляемые внутри. Декоратор @allowed не даст передать prd вместо prod. Тернарный оператор задаёт георепликацию только продакшену — потому что Standard_GRS примерно вдвое дороже Standard_LRS, и платить за неё в dev-окружении смысла нет. Функция uniqueString() добавляет детерминированный хвост, потому что имя storage account должно быть уникальным на весь Azure. output отдаёт наружу значения, которые понадобятся дальше — например, пайплайну.

Деплой через CLI

Шаблон сам по себе ничего не делает. Его нужно отправить в ARM:

az group create --name rg-shop-dev --location westeurope

# Сначала СМОТРИМ, что изменится — и только потом деплоим
az deployment group what-if \
  --resource-group rg-shop-dev \
  --template-file main.bicep \
  --parameters prefix=shop env=dev

az deployment group create \
  --name deploy-$(date +%Y%m%d-%H%M) \
  --resource-group rg-shop-dev \
  --template-file main.bicep \
  --parameters prefix=shop env=dev

what-if — самая недооценённая команда во всём Azure. Она показывает дифф между тем, что есть, и тем, что будет: что создастся, что изменится, что удалится. Приучите себя запускать её перед каждым деплоем в прод, и вы сэкономите себе минимум один инцидент.

Результат:

Resource and property changes are indicated with these symbols:
  + Create
  ~ Modify

The deployment will update the following scope:

Scope: /subscriptions/.../resourceGroups/rg-shop-dev

  + Microsoft.Storage/storageAccounts/shopdevst4kj2mn7q
      sku.name: "Standard_LRS"
      kind:     "StorageV2"

Resource changes: 1 to create.

Параметры для разных сред удобно держать не в командной строке, а в отдельных файлах dev.bicepparam и prod.bicepparam — тогда команда деплоя отличается одной строкой, а всё различие сред лежит в git и видно на ревью.

Идемпотентность — главное свойство

Идемпотентность означает: запусти деплой один раз или десять раз подряд — результат будет одинаковым. Bicep описывает не «действия», а желаемое состояние. Если storage account с такими свойствами уже есть, ARM просто ничего не делает. Если кто-то руками выключил HTTPS-only — следующий деплой вернёт его обратно.

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

Но есть нюанс — режим деплоя:

РежимЧто делаетРиск
Incremental (по умолчанию)создаёт и обновляет ресурсы из шаблона, остальное не трогаетв группе копится «мусор», которого нет в коде
Completeприводит группу ресурсов к шаблону точь-в-точьудаляет всё, чего нет в шаблоне — включая прод-базу

Режим Complete звучит красиво и убил не одну базу данных. Если решились — только вместе с what-if и блокировками ресурсов (az lock create).

Модули: не пишите один гигантский файл

Когда ресурсов становится больше десятка, main.bicep распиливают на модули — переиспользуемые куски:

module db 'modules/postgres.bicep' = {
  name: 'db-deploy'
  params: {
    location: location
    env: env
    adminPassword: kv.getSecret('pgAdminPassword')
  }
}

module app 'modules/webapp.bicep' = {
  name: 'app-deploy'
  params: {
    location: location
    dbHost: db.outputs.fqdn
  }
}

Обратите внимание на две вещи. Первая: app ссылается на db.outputs.fqdn — и Bicep сам понимает, что базу надо развернуть раньше приложения. Никаких ручных dependsOn. Вторая: пароль не написан в файле, а берётся из Key Vault функцией getSecret() — секрет вообще не попадает ни в git, ни в историю деплоев.

Bicep или Terraform?

Вопрос, который обязательно зададут на собеседовании.

КритерийBicepTerraform
Облакатолько AzureAzure, AWS, GCP, Cloudflare и сотни провайдеров
Состояниехранит сам Azure (история деплоев)файл tfstate, который надо где-то хранить и блокировать
Новые фичи Azureв день релиза (API-версии доступны сразу)с задержкой — ждём обновления провайдера
Удаление ресурсовтолько в режиме Complete (грубо)аккуратно и по плану — terraform destroy
Экосистемаскромнееогромная: модули, инструменты, найм

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

Как это работает

Под капотом цепочка простая. Команда az deployment group create запускает компилятор Bicep, который превращает .bicep в обычный ARM JSON (можете посмотреть сами: az bicep build --file main.bicep). JSON уходит в Azure Resource Manager. ARM строит граф зависимостей, разбивает работу на части и раздаёт задачи resource provider'ам — отдельным службам, отвечающим за свой тип ресурсов: Microsoft.Storage, Microsoft.Web, Microsoft.Sql. Каждый провайдер приводит свои ресурсы к нужному состоянию и отчитывается.

Отсюда два практических следствия. Во-первых, деплой асинхронный: CLI просто опрашивает статус, и «зависший» деплой обычно означает, что провайдер долго создаёт что-то тяжёлое (кластер AKS, SQL Managed Instance — это десятки минут, и это нормально). Во-вторых, каждый деплой сохраняется в истории группы ресурсов: az deployment group list -g rg-shop-dev покажет, кто, когда и что развернул, включая упавшие попытки с текстом ошибки. Это ваш чёрный ящик при разборе полётов.

Про деньги приятная новость: сам ARM, Bicep и деплои бесплатны. Платите только за созданные ресурсы. Никакого лимита на количество деплоев нет.

Частые ошибки

  • Деплой в режиме Complete «чтобы навести порядок». Классика: инженер добавил флаг, шаблон описывал только веб-приложение — и ARM снёс из группы базу с данными. Самая дорогая ошибка в этом уроке. Всегда сначала what-if.
  • Секреты прямо в шаблоне. param adminPassword string = 'Qwerty123!' навсегда останется в истории git. Используйте @secure() и Key Vault.
  • Дорогой SKU по умолчанию. Скопировали пример из интернета, а там P1v3 или Premium — и dev-окружение внезапно жрёт сотню долларов в месяц. Проверяйте SKU у каждого ресурса и делайте их зависимыми от env.
  • Забытые теги. Без тегов env/owner/project через полгода никто не сможет объяснить, за что вы платите, и «осиротевшие» ресурсы будут крутиться годами.
  • Правки руками поверх шаблона. Быстренько поменяли настройку в портале — следующий деплой её откатит, и вы полдня будете искать «мистику». Правки только через код.
  • Жёстко зашитый регион. location: 'westeurope' в модуле вместо параметра — и однажды вы получите ресурсы, размазанные по трём регионам, с оплатой трафика между ними.

Итоги

  • IaC превращает инфраструктуру в код: ревью, история, откат, точная копия окружения одной командой.
  • Bicep — читаемая надстройка над ARM JSON; компилируется в него же, состояние хранит сам Azure.
  • Шаблон описывает желаемое состояние, поэтому деплой идемпотентен и лечит дрейф конфигурации.
  • az deployment group what-if перед каждым деплоем в прод — привычка, экономящая нервы и деньги.
  • Режим Complete удаляет всё, чего нет в шаблоне. Относитесь к нему как к rm -rf.
  • Bicep — если вы только в Azure; Terraform — если облаков несколько. Сам ARM и деплои не стоят ничего.
Проверьте себя
1. Чем режим деплоя Complete отличается от Incremental?
AComplete работает быстрее, потому что деплоит ресурсы параллельно
BComplete удаляет из группы ресурсов всё, чего нет в шаблоне, а Incremental — не трогает лишнее
CComplete можно применять только к новым, ещё пустым группам ресурсов
DComplete проверяет шаблон, но ничего не разворачивает — это режим предпросмотра
2. Что происходит с файлом .bicep при выполнении az deployment group create?
AОн компилируется в ARM-шаблон JSON, который отправляется в Azure Resource Manager
BОн исполняется на агенте как обычный скрипт, вызывая REST API по строкам
CОн загружается в облако и хранится там как файл состояния, аналог tfstate
DОн транслируется в набор команд az cli, которые выполняются по очереди