Когда серверы bare-metal, гипервизоры, облачные решения и десятки Kubernetes-кластеров живут вместе, навести порядок в ИТ-инфраструктуре становится задачей со звёздочкой. К тому же к гибридной инфраструктуре добавляется организационный слой: десятки автономных команд — backend, UI, data engineering, ML, platform — и у каждой свой бэкграунд, уровень зрелости и разный подход к описанию инфраструктуры через код (IaC).

Кто-то всегда пишет на HCL, кто-то предпочитает Python и JS, а кто-то привык работать с docker compose up –d. Задача инженера платформы в такой обстановке не в том, чтобы навязать «серебряную пулю», а в том, чтобы найти инструмент, который обеспечит контроль над состоянием инфраструктуры, позволит стандартизировать базовые паттерны, предсказуемо реагировать на изменения, которые внесли вручную, а еще не будет ломать уже существующие процессы.

Привет, Хабр! Меня зовут Вячеслав Швецов, я архитектор в команде MWS B2B Store. Это первый материал из цикла о построении инженерной платформы в гетерогенной среде. Мы будем разбирать инструменты, антипаттерны и ограничения при эксплуатации. В этом выпуске сравним подходы Terraform и Pulumi, а также рассмотрим управление состоянием, детекцию дрейфа инфраструктуры и практику управления инфраструктурой как кодом.

Terraform (HCL)

История Terraform фактически началась в 2011 году, когда AWS анонсировала CloudFormation: Митчелл Хашимото, сооснователь HashiCorp, опубликовал пост, в котором высоко оценил идею управления инфраструктурой с помощью кода, но отметил критический пробел — отсутствие open source облачно-независимого инструмента, который бы предоставлял единый рабочий процесс для любого провайдера. 

Несколько лет идея оставалась нереализованной, пока растущая сложность мультиоблачных сред не сделала проблему острой для самой HashiCorp. В июле 2014 года компания выпустила Terraform 0.1 с поддержкой AWS и DigitalOcean, распространяющегося под открытой лицензией MPL 2.0. 

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

Пример декларативного описания на HCL для создания виртуальной машины через libvirt:

terraform {
  required_providers {
    libvirt = {
      source  = "dmacvicar/libvirt"
      version = "~> 0.7"
    }
  }
}

provider "libvirt" {
  uri = "qemu:///system"
}

resource "libvirt_volume" "disk" {
  ...
}

resource "libvirt_network" "net" {
  ...
}

resource "libvirt_cloudinit_disk" "commoninit" {
  ...
}

resource "libvirt_domain" "vm" {
  name   = "test-vm"
  memory = 2048
  vcpu   = 2

  disk {
    volume_id = libvirt_volume.disk.id
  }

  network_interface {
    network_id = libvirt_network.net.id
    hostname   = "test-vm"
  }

  cloudinit = libvirt_cloudinit_disk.commoninit.id
}

В августе 2023 года HashiCorp сменила лицензию Terraform с открытой (MPL 2.0) на Business Source License (BSL) 1.1 по экономическим причинам. Это решение вызвало раскол в сообществе и привело к созданию OpenTofu — полностью открытого аналога, развивающегося при поддержке Linux Foundation.

Pulumi (Code, YAML)

Pulumi появился в 2017 году как ответ на фундаментальное ограничение существующих IaC-инструментов, использовавших DSL вместо полноценных языков программирования (ЯП).

Сооснователи проекта — Джо Даффи (бывший архитектор .NET и Azure в Microsoft) и Эрик Раддер (экс-CTO Microsoft) — поставили цель объединить декларативную модель инфраструктуры с выразительностью и экосистемой современных ЯП. 

Публичный запуск Pulumi состоялся в начале 2018 года. Инструмент вышел с поддержкой Python, TypeScript и Go, что позволило разработчикам управлять облачными ресурсами с помощью привычных паттернов (тесты, абстракции, пакеты). В отличие от конкурентов, Pulumi сделал ставку на «инфраструктуру как программный код», а не «инфраструктуру как конфигурацию», что привлекло команды с сильной инженерной культурой.

Бизнес-модель Pulumi, в отличие от HashiCorp, строится на сочетании полностью открытого исходного кода (Apache 2.0) и облачной SaaS-платформы. Монетизируется не сам инструмент, а удобство управления им в команде.

Ниже — пример кода на Python, создающий виртуальную машину через libvirt. Но писать код не обязательно. У Pulumi есть декларативный режим на базе YAML, похожий на привычный HCL. Это снижает порог входа для команд, которые предпочитают «описывать инфраструктуру», а не «программировать её».

Пример кода на Python:

import pulumi
import pulumi_libvirt as libvirt

net = libvirt.Network("net", mode="nat", domain="test.local", addresses=["192.168.100.0/24"])
disk = libvirt.Volume("disk", pool="default", format="qcow2", size=20 * 1024**3)
cloudinit = libvirt.CloudInitDisk("init", pool="default", user_data="#cloud-config\nhostname: test-vm\npassword: changeme\nchpasswd: { expire: False }")

vm = libvirt.Domain("vm", memory=2048, vcpu=2,
    disks=[{"volume_id": disk.id}],
    network_interfaces=[{"network_id": net.id, "hostname": "test-vm"}],
    cloudinit=cloudinit.id)

Пример конфигурации на Pulumi YAML:

name: kvm-vm
runtime: yaml

resources:
  vm:
    type: libvirt:index/domain:Domain
    properties:
      name: test-vm
      memory: 2048
      vcpu: 2
      ...

Pulumi YAML: декларативный подход вроде есть, а вроде нет

У Pulumi есть альтернатива коду — декларативный режим. Но на практике этот режим быстро упирается в жёсткие рамки и подходит только для решения простых задач: 

  • Нет циклов и полноценных условий: нельзя использовать привычные for, if/else внутри YAML. Чтобы динамически создавать однотипные ресурсы, обычно используют несколько стеков или внешние генераторы.

  • Ограниченная логика вычислений: вычисления сильно урезаны, доступны только базовые встроенные строковые/массивные функции. Сложные преобразования данных не поддерживаются.

Управление конфигурацией и работа со state

Основные команды

После того как конфигурация описана, нужно ею воспользоваться. Оба инструмента следуют единому циклу: 

  • инициализация окружения; 

  • расчёт плана изменений;

  • применение конфигурации к реальной инфраструктуре. 

В таблице собраны ключевые команды для создания, обновления и управления локальным состоянием инфраструктуры:

Действие

Terraform

Pulumi

Инициализация

terraform init

pulumi login –local

pulumi stack init dev

Просмотр плана

terraform plan

pulumi preview

Применение конфигурации

terraform apply

pulumi up

Обновление (idempotent) конфигурации

terraform apply 

pulumi up 

Локальный state

Файл ./terraform.tfstate

~/.pulumi/stacks/<project>/<stack>.json

Экспорт state

terraform state pull > state.json

pulumi stack export > stack.json

На первый взгляд Terraform и Pulumi решают идентичные задачи и даже позволяют выстраивать работу практически в одинаковом стиле. Однако за этой внешней схожестью скрываются принципиально разные архитектурные решения, модели управления состоянием, философии расширяемости и подходы к интеграции с процессами доставки. 

Чтобы сделать взвешенный выбор для разнородной среды, недостаточно сравнить синтаксис или базовые команды. Необходимо заглянуть под капот и разобрать, как каждый инструмент обрабатывает граф зависимостей, реагирует на дрейф инфраструктуры, масштабируется при росте числа команд и вписывается в строгие инженерные практики. Именно эти особенности мы и разберём далее.

State и куда же обойтись без drift

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

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

Хранение состояния

Состояние инфраструктуры хранится в специальном state-файле. По нему инструмент понимает, какие изменения нужно внести при следующем запуске. 

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

Terraform и Pulumi подходят к безопасности и хранению state-файла по-разному:

Параметр

Terraform

Pulumi

Формат

JSON-файл terraform.tfstate

Encrypted checkpoint (JSON-структура)

Локальное хранение

Файл в рабочей папке. Блокировка только через file или внешний сервис

Команда pulumi login –local сохраняет всё в скрытую домашнюю папку ~/.pulumi/stacks/

Сетевое хранилище

Любой бэкенд (S3, GCS, Consul, Terraform Cloud)

Pulumi Cloud, S3, GCS, Azure Blob, Consul, Kubernetes

Секреты

Хранятся в открытом виде (рекомендуется внешний бэкенд с шифрованием)

Шифруются на лету (по умолчанию через Pulumi passphrase или cloud KMS)

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

Здесь Pulumi выигрывает из коробки за счёт встроенного шифрования секретов, но требует явного управления passphrase/KMS.

Обработка drift (расхождения desired vs actual)

Оба инструмента выявляют расхождение реального и желаемого состояния инфраструктуры (drift инфраструктуры) через чтение API провайдера. Terraform делает это в рамках plan, Pulumi — в preview/refresh. Разница кроется в подходе: 

  • Для Terraform единственный источник правды — state-файл. Любое ручное изменение воспринимается как аномалия, ошибка, которую нужно устранить.

  • Pulumi к ручным правкам относится более гибко. У него есть штатный инструмент синхронизации — refresh. Он позволяет обновить state-файл под реальность, что удобнее в средах, где ручные правки неизбежны.

Механизм

Terraform

Pulumi

Поиск изменений

terraform plan сравнивает state с реальным миром через API провайдера

pulumi preview (или pulumi up –diff) сравнивает state с реальным миром через API провайдера

Синхронизация

terraform apply применяет изменения к инфраструктуре. 

terraform apply -refresh-only обновляет state под реальность

pulumi refresh обновляет state под реальность

Мутация состояния: когда план не спасает

Идеология IaC предполагает, что state — зеркало реальности. На практике бывают ситуации, когда зеркало «трескается»: ресурс удалён вручную, модуль рефакторили или провайдер сломал идемпотентность. В таких случаях приходится вручную править state, чтобы избежать внезапного пересоздания живых ресурсов (destroy + create).

Задача

Terraform

Pulumi

Убрать ресурс из state, не трогая реальную инфраструктуру

terraform state rm <address>

pulumi state delete <urn>

Взять существующий ресурс под управление

terraform import <address> <id>

pulumi import <type> <name> <id>

Переименовать/перенести ресурс в state (после рефакторинга)

terraform state mv <old> <new>

Нет прямой команды. Экспорт → правка urn в JSON → импорт

Принудительно пересоздать ресурс (без полного destroy)

terraform apply -replace="<address>"

pulumi up --target <urn> --target-replace

Тонкая ручная правка state

terraform state pull > state.json → ручное редактирование → terraform state push

pulumi stack export > state.json → ручное редактирование → pulumi stack import state.json

Защита от случайного удаления

lifecycle { prevent_destroy = true }

options: { protect: true } (снимается через pulumi state unprotect)

Правила работы с мутациями состояний:

  1. Всегда делайте бэкапы перед мутацией: terraform state pull / pulumi stack export.

  2. Проверяйте state после изменений: terraform plan / pulumi preview должны показать No changes или ожидаемые минимальные различия.

  3. Checkpoint-система Pulumi автоматически сохраняет предыдущие версии состояния, что даёт откат «одной командой». В Terraform history состояние нужно хранить вручную (например, через бэкенд с версионированием в S3).

Мутация state — крайняя мера. Используйте её для миграции legacy, восстановления после CI-сбоев или обхода багов провайдеров. Не заменяйте ею нормальный рабочий процесс.

Переиспользование кода: как не дублировать описание инфраструктуры

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

Без модулей даже самый продвинутый IaC-инструмент быстро деградирует в «скриптовый хаос», где каждое изменение требует героических усилий.

Terraform: модули + обёртки

  • Модули (modules/) — базовый механизм. Параметризуются через variable, возвращают output. Поддерживают for_each, count, depends_on.

  • Registry — реестр с готовыми провайдерами от HashiCorp и комьюнити. Удобно, но требует аудита версий.

  • Terragrunt — стандарт для DRY (Don’t Repeat Yourself) в Terraform. Позволяет один раз настроить общие параметры (например, backend, provider, remote_state) и использовать их во всех проектах одной строкой кода через include и dependency. Особенно полезно, когда команды разные, но много однотипных окружений (stage/prod/dev).

Ограничения: HCL — это DSL, а не язык программирования. Поэтому нет поддержки наследования, unit-тестов, сложная отладка.

Pulumi: компоненты + пакеты 

  • Компоненты (ComponentResource) — классы/функции, инкапсулирующие группу ресурсов. Можно писать на Python, TS, Go, C#. Это полноценное ООП/функциональное программирование с наследованием, dependency injection, mock-тестами.

  • Пакеты — публикация компонентов в приватном реестре. Команды подключают компоненты как зависимости, не видя внутренней реализации.

  • YAML — декларативный подход. Поддерживает ${config.*}, ${resources.*}, но не даёт логики. Для переиспользования кода используются внешние конфигурации, CI-генерация или переход к языковому стеку при росте сложности.

Ограничения: требуются знания ЯП от авторов компонентов. YAML-стеку не хватает гибкости для сложных паттернов.

Сравнение подходов к анализу кода инфраструктуры

По мере накопления кода IaC он неизбежно перестаёт быть просто набором конфигурационных файлов и превращается в корпоративный актив, требующий управления. 

Рост объёмов, масштабирование команд и увеличение количества сред делают ручное сопровождение и ad-hoc-проверки невозможными. Это закономерно подталкивает организации к внедрению централизованного анализа: статической валидации, контроля соответствия политикам безопасности и стандартам. Terraform и Pulumi подходят к этой задаче совершенно по-разному:

Параметр

Terraform

Pulumi

Простота анализа кода

Высокая

Код написан в простом текстовом формате. 

Сканеры безопасности легко читают его без запуска самой программы

Средняя/низкая.  

Чтобы полностью проверить инфраструктуру, код сначала нужно запустить.

Сложнее статический парсинг. 

Из-за гибкости языков сканеры часто ошибаются или пропускают скрытые баги 

Инструменты проверки

Богатый выбор готовых инструментов для автоматической проверки

Можно использовать стандартные тесты для языков программирования (unit-тесты)

Почему HCL предпочтителен для нас 

Главные причины, почему мы выбрали Terraform и HCL:

  • Предсказуемая структура и готовность к AST-анализу:

    • HCL имеет фиксированный синтаксис, строгую иерархию (resource, module, variable, data, locals) и отсутствие произвольной логики.

    • Парсеры легко строят абстрактное синтаксическое дерево (AST), что упрощает написание линтеров, валидаторов и анализаторов.

    • В отличие от языков общего назначения (Python, TypeScript в Pulumi), здесь нет динамического импорта, рефлексии или побочных эффектов при парсинге.

  • Детерминированный plan в машиночитаемом формате:

    • terraform plan –json и terraform show –json выдают структурированный снимок будущих изменений: создаваемые/удаляемые ресурсы, изменённые атрибуты, зависимости, метаданные провайдеров.

    • Платформы могут парсить этот JSON для:

      • оценки стоимости или проверки квотирования;

      • детекции дрейфа инфраструктуры.

  • Зрелая экосистема Policy as Code и линтеров:

    • Checkov, tfsec, Terrascan, KICS, TFLint — все нативно поддерживают HCL.

    • Правила пишутся декларативно, покрывают CIS, NIST, PCI-DSS, GDPR, внутренние стандарты компаний.

    • Интеграция с реестром провайдеров: схемы ресурсов публикуются как JSON, что позволяет легко валидировать атрибуты.

  • Модель зависимостей и граф ресурсов:

    • HCL автоматически строит DAG (directed acyclic graph) на основе явных и косвенных зависимостей.

    • Terraform graph и визуализаторы (Rover, graphviz) дают прозрачную карту архитектуры.

  • Модульность и контракты:

    • variables с типами/описаниями/дефолтами, outputs, required_providers, terraform {} создают предсказуемость.

    • Анализаторы могут принудительно проверять:

      • версионирование провайдеров и модулей;

      • обязательные теги/метки;

      • переменные, которые используются в качестве контракта;

      • запрет на использование определённых ресурсов. 

Заключение

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

На практике вы всё равно будете строить собственную инженерную платформу поверх этих инструментов: писать обёртки, настраивать CI/CD-пайплайны для валидации state, внедрять policy-as-code и выстраивать культуру работы с инфраструктурой. Ни Terraform, ни Pulumi не решают проблему «снежинок» из коробки — они лишь предоставляют инструменты, которыми эту проблему можно решить при наличии дисциплины и правильной архитектуры.

В следующей статье цикла мы разберём, как закрыть этот пробел. Рассмотрим atmos.tools, Terramate и другие решения, работающие поверх Terraform и Pulumi. Покажем, как они обходят проблемы на уровне платформы, превращая разрозненные скрипты в управляемую, версионируемую и предсказуемую инфраструктуру.

Более того, можно вообще отказаться от ручной сборки платформы из десятков разномастных инструментов и обратить внимание на среды, где типовые B2B-решения для IaC уже унифицированы. В MWS мы применили такой подход при создании собственного маркетплейса B2B-решений: предлагаем развёртывание по клику на инфраструктуре клиента независимо от её архитектуры. Вы можете разместить свой B2B-продукт в нашей экосистеме.