Есть две стадии взросления инфраструктурного инженера. И если вы достаточно долго работаете в эксплуатации, разработке платформ или DevOps, велика вероятность, что через обе вы уже проходили. Первая стадия выглядит удивительно безобидно. Человек получает доступ к серверу, открывает терминал и думает: "Сейчас я быстро всё настрою через SSH и пару shell-скриптов. Тут работы минут на двадцать". В этот момент он искренне верит, что инфраструктура - это набор машин, несколько конфигурационных файлов и немного Bash-магии. Скрипты складываются в папку scripts, пароли временно живут в заметках, а документация существует преимущественно в голове человека, который всё это написал.

Вторая стадия наступает значительно позже и обычно сопровождается куда менее жизнерадостными формулировками. Она начинается где-то между фразами "кто удалил production VPC?" и "почему этим человеком оказался я?". Именно тогда выясняется, что инфраструктура уже давно перестала быть несколькими виртуальными машинами. Она превратилась в сложную систему, состоящую из облачных сетей, Kubernetes-кластеров, очередей сообщений, баз данных, балансировщиков нагрузки и нескольких десятков микросервисов. Каждый из этих компонентов зависит от других, а любое изменение потенциально способно затронуть критически важные бизнес-процессы. В этот момент становится очевидно, что инфраструктура требует таких же инженерных подходов, как и обычная разработка программного обеспечения.

Именно поэтому индустрия постепенно пришла к Infrastructure as Code - инфраструктуре как коду. Сначала появились решения вроде CloudFormation. Затем появился Terraform и буквально изменил отрасль. Потом инженеры начали писать Ansible-плейбуки объёмом с "Войну и мир", строить многослойные Helm-чарты и генерировать YAML при помощи Jinja-шаблонов. А затем внезапно появился Pulumi и задал очень неудобный вопрос:

А что если инфраструктуру можно описывать обычным языком программирования?

И вот тут многие инфраструктурщики впервые за долгое время действительно подняли брови.

Что вообще такое Pulumi

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

Pulumi поддерживает:

  • Python;

  • Go;

  • TypeScript;

  • JavaScript;

  • C#;

  • Java;

  • YAML.

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

Однако главный смысл Pulumi заключается совсем не в количестве поддерживаемых языков. Он состоит в том, что вместо специализированного декларативного DSL вы получаете настоящий язык программирования со всеми его возможностями:

  • циклами;

  • условиями;

  • функциями;

  • классами;

  • библиотеками;

  • тестированием;

  • повторным использованием кода;

  • пакетной структурой проекта.

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

Главная проблема Terraform, о которой не любят говорить

Прежде чем продолжить, важно сразу обозначить одну вещь. Terraform - великолепный инструмент. Без всякой иронии. Он изменил индустрию сильнее, чем большинство решений последних десяти лет. Он сделал Infrastructure as Code массовым явлением, научил тысячи компаний относиться к инфраструктуре как к версии контролируемому артефакту и фактически создал новую инженерную культуру.

Но у Terraform существует фундаментальное ограничение.

Terraform не является языком программирования.

Его HCL - это декларативный DSL, специализированный язык описания инфраструктуры. И пока инфраструктура остаётся относительно простой, подобный подход работает прекрасно.

Например:

AWS
├── VPC
├── 2 Subnet
├── Security Group
└── EC2

Для подобных сценариев Terraform остаётся одним из лучших решений на рынке.

Проблемы начинают проявляться позже.

Сначала появляется несколько окружений:

dev
stage
prod

Потом возникает второй Kubernetes-кластер.

Затем приходит multi-region.

Через некоторое время появляется необходимость автоматически генерировать ресурсы для десятков сервисов.

После этого возникают динамические зависимости.

И внезапно инфраструктурный код начинает выглядеть примерно так:

locals {
  services = {
    billing   = 3
    payments  = 5
    analytics = 2
  }
}

resource "..." "..." {
  for_each = local.services

  dynamic "..." {
    ...
  }
}

Затем появляются:

  • locals;

  • count;

  • for_each;

  • dynamic;

  • шаблоны;

  • модули внутри модулей;

  • Terragrunt;

  • генераторы Terraform;

  • Python-скрипты, генерирующие Terraform;

  • оболочки поверх Terraform.

И в какой-то момент инженер ловит себя на очень странной мысли:

Подождите. Я написал собственный язык программирования поверх Terraform, чтобы не писать код. Но зачем?

Именно здесь Pulumi начинает выглядеть не как очередной IaC-инструмент, а как попытка переосмыслить сам подход к работе с инфраструктурой.

Почему Pulumi ощущается иначе

Представим вполне обычную задачу.

Необходимо создать namespace для нескольких приложений в Kubernetes.

Terraform:

resource "kubernetes_namespace" "apps" {
  count = length(var.apps)

  metadata {
    name = var.apps[count.index]
  }
}

Работает ли это?

Безусловно.

Красиво ли это?

Это уже вопрос вкуса.

Тот же самый пример на Pulumi с использованием Python выглядит так:

import pulumi
import pulumi_kubernetes as k8s

apps = [
    "billing",
    "payments",
    "analytics",
    "monitoring",
]

for app in apps:
    k8s.core.v1.Namespace(
        app,
        metadata={
            "name": app
        }
    )

Именно здесь у многих возникает ощущение лёгкого когнитивного диссонанса.

Нет:

  • count.index;

  • специальных конструкций;

  • внутренней магии DSL;

  • дополнительных шаблонизаторов.

Есть обычный цикл Python.

И вроде бы это мелочь.

Но если инфраструктура насчитывает сотни ресурсов, десятки окружений и сложную бизнес-логику, подобные "мелочи" начинают радикально влиять на удобство сопровождения.

Потому что инфраструктура постепенно перестаёт быть набором деклараций.

Она начинает превращаться в программу.

И именно это изменение мышления оказывается самым важным отличием Pulumi от Terraform.

Очень быстро после знакомства с Pulumi большинство инженеров проходит через одинаковую стадию. Сначала возникает ощущение, что вся разница между ним и Terraform сводится к синтаксису. Кто-то пишет HCL, кто-то предпочитает Python или Go. Кажется, что речь идёт исключительно о вкусовых предпочтениях, примерно как спор о том, какие скобки красивее или какой редактор кода считать единственно правильным. Но затем появляется первая по-настоящему сложная задача, и именно в этот момент становится понятно, что речь идёт вовсе не о синтаксисе. Меняется сама модель мышления.

Представим вполне реалистичную ситуацию. Компания развивает внутреннюю платформу для нескольких продуктовых команд. Каждый Pull Request должен автоматически поднимать полноценное окружение для тестирования. Разработчик открывает запрос на слияние изменений, после чего инфраструктура создаёт отдельный namespace, разворачивает сервис, поднимает Redis, PostgreSQL, настраивает ingress, выпускает TLS-сертификаты и публикует временный домен. После завершения проверки всё это должно быть автоматически уничтожено.

Архитектурно такой процесс выглядит следующим образом:

GitHub Pull Request
        ↓
GitHub Actions
        ↓
Pulumi Automation API
        ↓
Kubernetes Namespace
        ↓
Deployment
        ↓
Redis
        ↓
PostgreSQL
        ↓
Ingress
        ↓
ExternalDNS
        ↓
CertManager
        ↓
Grafana Dashboards

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

Попробуем представить, как выглядел бы код создания временного окружения в Pulumi на Python.

Сначала определим параметры Pull Request:

import pulumi
import pulumi_kubernetes as k8s

pr_number = 482

namespace_name = f"pr-{pr_number}"

image = f"ghcr.io/company/app:pr-{pr_number}"

Создадим namespace:

namespace = k8s.core.v1.Namespace(
    namespace_name,
    metadata={
        "name": namespace_name,
        "labels": {
            "environment": "preview",
            "pr": str(pr_number),
        }
    }
)

Развернём приложение:

deployment = k8s.apps.v1.Deployment(
    f"app-{pr_number}",
    metadata={
        "namespace": namespace.metadata["name"]
    },
    spec={
        "replicas": 1,
        "selector": {
            "matchLabels": {
                "app": "demo"
            }
        },
        "template": {
            "metadata": {
                "labels": {
                    "app": "demo"
                }
            },
            "spec": {
                "containers": [{
                    "name": "app",
                    "image": image,
                    "ports": [{
                        "containerPort": 8000
                    }]
                }]
            }
        }
    }
)

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

Например, добавить Redis можно буквально несколькими строками:

redis = k8s.apps.v1.Deployment(
    f"redis-{pr_number}",
    metadata={
        "namespace": namespace.metadata["name"]
    },
    spec={
        "replicas": 1,
        "selector": {
            "matchLabels": {
                "app": "redis"
            }
        },
        "template": {
            "metadata": {
                "labels": {
                    "app": "redis"
                }
            },
            "spec": {
                "containers": [{
                    "name": "redis",
                    "image": "redis:8"
                }]
            }
        }
    }
)

А затем опубликовать приложение наружу:

ingress = k8s.networking.v1.Ingress(
    f"app-{pr_number}",
    metadata={
        "namespace": namespace.metadata["name"],
        "annotations": {
            "cert-manager.io/cluster-issuer":
                "letsencrypt",

            "external-dns.alpha.kubernetes.io/hostname":
                f"pr-{pr_number}.dev.company.io"
        }
    },
    spec={
        "ingressClassName": "nginx",
        "rules": [{
            "host": f"pr-{pr_number}.dev.company.io",
            "http": {
                "paths": [{
                    "path": "/",
                    "pathType": "Prefix",
                    "backend": {
                        "service": {
                            "name": "app",
                            "port": {
                                "number": 80
                            }
                        }
                    }
                }]
            }
        }]
    }
)

В результате через несколько минут после открытия Pull Request разработчик получает готовую ссылку:

https://pr-482.dev.company.io

А после закрытия Pull Request всё окружение уничтожается одной командой:

pulumi destroy --yes

На первый взгляд может показаться, что ничего принципиально нового здесь нет. Kubernetes остаётся Kubernetes. Redis остаётся Redis. Ingress по-прежнему публикует приложение наружу. Однако если посмотреть на происходящее внимательнее, становится заметно важное отличие. В Terraform подобный сценарий неизбежно начинает обрастать дополнительными слоями абстракций. Появляются шаблонизаторы, генераторы конфигураций, Terragrunt и вспомогательные скрипты. В Pulumi сама инфраструктура становится программой, внутри которой подобная логика выглядит естественно.

Но именно здесь возникает следующий очень важный вопрос. Если инфраструктура теперь является полноценным кодом, означает ли это, что её можно тестировать так же, как тестируется обычное программное обеспечение? И именно в этот момент Pulumi начинает раскрывать ещё одну возможность, которая постепенно меняет представление о том, чем вообще должен заниматься современный Platform Engineering.

Разговор о тестировании инфраструктуры ещё несколько лет назад звучал для многих инженеров как своеобразная шутка. Обычно он выглядел примерно так:

  • разработчик пишет unit-тесты;

  • QA пишет интеграционные сценарии;

  • инфраструктурщик делает terraform apply и внимательно смотрит, не загорелось ли ничего вокруг.

Если после применения конфигурации кластер не исчез, VPC не удалилась, а production каким-то чудом продолжил обслуживать пользователей, эксперимент считался успешным. Ирония заключается в том, что именно так работало огромное количество вполне серьёзных компаний. Инфраструктурный код существовал, хранился в Git и проходил code review, но при этом практически никогда не тестировался как полноценное программное обеспечение.

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

Почему мы не можем писать для неё тесты?

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

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

Создание бакета выглядит привычно:

import pulumi_aws as aws

bucket = aws.s3.Bucket(
    "documents",
    acl="private"
)

Но теперь можно проверить ожидаемое поведение автоматически.

Например:

import pulumi
from pulumi.runtime import set_mocks
from pulumi.runtime import Mocks

Создадим мок-реализацию облачного провайдера:

class MyMocks(Mocks):

    def new_resource(
        self,
        type_,
        name,
        inputs,
        provider,
        resource_id
    ):

        return (
            f"{name}_id",
            inputs
        )

    def call(
        self,
        token,
        args,
        provider
    ):

        return {}

Подключим её:

set_mocks(MyMocks())

А затем проверим результат:

def test_bucket_private():

    assert bucket.acl == "private"

На первый взгляд пример кажется примитивным. Но именно подобные проверки постепенно перерастают в полноценную инфраструктурную политику.

Например, можно гарантировать, что:

  • все Kubernetes-кластеры используют шифрование;

  • все базы данных разворачиваются внутри приватных сетей;

  • все балансировщики нагрузки поддерживают TLS;

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

  • ни один разработчик случайно не создаст публичный S3-бакет.

Именно в этот момент инфраструктурный код перестаёт быть набором деклараций и начинает вести себя как обычное приложение.

Разумеется, на этом разговор не заканчивается. Потому что как только появляется тестирование, возникает следующий уровень зрелости - повторное использование кода.

Component Resources - когда инфраструктура становится библиотекой

Практически каждая команда Platform Engineering рано или поздно замечает одну и ту же закономерность. Большая часть инфраструктуры повторяется.

Каждому сервису нужны:

  • Deployment;

  • Service;

  • Ingress;

  • ServiceAccount;

  • ConfigMap;

  • Dashboard;

  • Alertmanager Rule;

  • NetworkPolicy.

Сначала всё копируется вручную.

Затем появляются Helm-чарты.

Потом появляются Helm-чарты внутри Helm-чартов.

После этого инженеры начинают писать генераторы YAML.

А затем наступает момент, когда кто-то произносит:

Может быть, это просто класс?

И вот здесь Pulumi начинает чувствовать себя как дома.

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

Вместо копирования одинакового кода можно создать собственный компонент:

import pulumi
import pulumi_kubernetes as k8s

class Application(
    pulumi.ComponentResource
):

    def __init__(
        self,
        name,
        image,
        namespace,
        opts=None
    ):

        super().__init__(
            "company:Application",
            name,
            {},
            opts
        )

Создадим Deployment:

        self.deployment = (
            k8s.apps.v1.Deployment(
                f"{name}-deployment",

                metadata={
                    "namespace": namespace
                },

                spec={
                    "replicas": 2,

                    "selector": {
                        "matchLabels": {
                            "app": name
                        }
                    },

                    "template": {

                        "metadata": {
                            "labels": {
                                "app": name
                            }
                        },

                        "spec": {
                            "containers": [{
                                "name": name,
                                "image": image
                            }]
                        }
                    }
                },

                opts=pulumi.ResourceOptions(
                    parent=self
                )
            )
        )

И Service:

        self.service = (
            k8s.core.v1.Service(

                f"{name}-service",

                metadata={
                    "namespace": namespace
                },

                spec={
                    "selector": {
                        "app": name
                    },

                    "ports": [{
                        "port": 80,
                        "targetPort": 8000
                    }]
                },

                opts=pulumi.ResourceOptions(
                    parent=self
                )
            )
        )

        self.register_outputs({})

Теперь использование выглядит удивительно просто:

Application(
    name="billing",
    image="ghcr.io/company/billing:v1",
    namespace="prod"
)

Application(
    name="payments",
    image="ghcr.io/company/payments:v1",
    namespace="prod"
)

Application(
    name="analytics",
    image="ghcr.io/company/analytics:v1",
    namespace="prod"
)

Именно в этот момент становится понятно, почему Pulumi особенно полюбился людям, занимающимся Platform Engineering. Потому что появляется возможность создавать не просто инфраструктуру, а внутренние платформенные примитивы.

Разработчик больше не думает о Deployment, Service и Ingress отдельно.

Он получает готовую сущность:

Application(...)

И знает, что внутри уже реализованы:

  • корпоративные стандарты;

  • политики безопасности;

  • мониторинг;

  • логирование;

  • соглашения по именованию;

  • лучшие практики команды.

Фактически инфраструктурная команда начинает поставлять не YAML, а программные библиотеки.

Automation API - момент, когда Pulumi перестаёт быть просто IaC

Именно здесь начинается самая интересная часть истории.

Большинство инженеров воспринимает Pulumi как "Terraform на Python".

Это очень понятное заблуждение.

Первые недели именно так всё и выглядит.

Есть команда:

pulumi up

Есть состояние:

pulumi stack ls

Есть удаление ресурсов:

pulumi destroy

Кажется, что разница исключительно в языке.

Но затем появляется Automation API.

И внезапно выясняется, что Pulumi можно встроить внутрь обычного приложения.

Например:

from pulumi import automation as auto

Создадим стек программно:

stack = auto.create_or_select_stack(
    stack_name="pr-482",
    project_name="preview-env",
    program=my_program
)

Обновим инфраструктуру:

stack.up(
    on_output=print
)

Удалим её после завершения работы:

stack.destroy(
    on_output=print
)

На первый взгляд ничего особенного.

Но теперь представим внутренний портал компании.

Разработчик нажимает кнопку:

Create Environment

Под капотом происходит следующее:

Web UI
    ↓
FastAPI
    ↓
Automation API
    ↓
Pulumi Program
    ↓
Kubernetes
    ↓
Cloud Resources

Именно в этот момент Pulumi перестаёт быть инструментом командной строки.

Он превращается в движок платформы.

Разработчики получают self-service.

Платформенная команда сохраняет контроль.

Инфраструктурные стандарты продолжают соблюдаться автоматически.

Именно поэтому всё больше Platform Engineers смотрят на Pulumi не как на очередную замену Terraform, а как на конструктор внутренних платформ. Потому что Infrastructure as Code постепенно эволюционирует в Platform as a Product, где инфраструктура становится не набором файлов в Git-репозитории, а полноценным программным продуктом со своим API, библиотеками, тестами и пользовательским опытом.

Именно на этом этапе у многих читателей возникает вполне закономерный вопрос. Если Pulumi настолько хорош, почему Terraform до сих пор остаётся стандартом де-факто во многих компаниях? Почему HashiCorp продолжает доминировать на рынке, а тысячи инженеров по всему миру ежедневно запускают terraform plan и terraform apply, даже имея возможность использовать полноценные языки программирования?

Ответ, как это обычно бывает в инженерии, гораздо сложнее, чем лозунги в стиле "старое против нового". Потому что зрелость технологии определяется не только её техническими возможностями, но и экосистемой, накопленным опытом и стоимостью изменений.

Почему Terraform никуда не исчезнет

За последние годы вокруг Terraform сформировалась огромная инфраструктурная вселенная. Появились тысячи готовых модулей, сотни корпоративных стандартов, зрелые процессы сопровождения и огромный рынок специалистов. Практически любой облачный сервис имеет готовые примеры. Любая проблема уже обсуждалась на форумах, в блогах и GitHub Issues. Практически любой инженер эксплуатации способен прочитать HCL и понять, что происходит.

Именно поэтому разговор о Pulumi не должен превращаться в попытку доказать, что Terraform внезапно стал плохим инструментом. Это было бы не только несправедливо, но и просто неправильно.

Terraform великолепен, когда инфраструктура хорошо описывается декларативной моделью.

Например:

AWS Account
    ↓
VPC
    ↓
Subnets
    ↓
Security Groups
    ↓
RDS
    ↓
EKS

Подобные сценарии составляют огромную часть корпоративного мира.

Типичный Terraform-проект выглядит предсказуемо:

terraform/
├── modules/
├── environments/
│   ├── dev/
│   ├── stage/
│   └── prod/
├── backend.tf
├── providers.tf
└── variables.tf

И миллионы инфраструктурных ресурсов по всему миру успешно управляются именно таким образом.

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

Например:

  • создание сотен окружений по событиям из GitHub;

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

  • сложные процессы согласования;

  • программное управление жизненным циклом ресурсов;

  • реализация self-service платформ;

  • тесная интеграция с бизнес-логикой компании.

Именно здесь Pulumi начинает предлагать принципиально иной подход.

State - проблема, от которой никто не убежал

Есть темы, которые способны объединить инженеров вне зависимости от того, используют они Terraform, Pulumi или пишут всё вручную через API облачных провайдеров.

Одна из таких тем называется State.

Состояние инфраструктуры является фундаментальной проблемой любого IaC-инструмента. Кто-то должен помнить, какие ресурсы были созданы, какие атрибуты изменились и каким образом текущее состояние отличается от желаемого.

Terraform использует привычную модель:

Code
    ↓
Terraform State
    ↓
Cloud Provider

Чаще всего состояние хранится в S3:

terraform {
  backend "s3" {

    bucket = "company-tf-state"

    key = "prod/network.tfstate"

    region = "eu-central-1"

    dynamodb_table = "terraform-locks"
  }
}

Pulumi использует очень похожую концепцию:

Code
    ↓
Pulumi State
    ↓
Cloud Provider

Однако варианты хранения оказываются более гибкими.

Например, можно использовать облачный сервис Pulumi:

pulumi login

Либо хранить состояние в объектном хранилище:

pulumi login \
s3://company-pulumi-state

Или использовать Azure Blob Storage:

pulumi login \
azblob://pulumi-state

Либо Google Cloud Storage:

pulumi login \
gs://pulumi-state

Это очень важный момент. Некоторые инженеры ошибочно считают, что использование Pulumi автоматически означает зависимость от SaaS-платформы Pulumi Cloud.

На практике это не так.

Например:

pulumi stack init prod

pulumi up

будет прекрасно работать с состоянием в S3.

Тем не менее принципиальная проблема остаётся общей.

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

Именно поэтому зрелость процессов вокруг State зачастую оказывается важнее выбора конкретного IaC-решения.

А как быть с миграцией?

Пожалуй, самый частый вопрос, который возникает после знакомства с Pulumi, звучит следующим образом:

Неужели теперь придётся переписывать всю инфраструктуру?

К счастью, ответ отрицательный.

Во-первых, далеко не каждая инфраструктура нуждается в миграции.

Во-вторых, Pulumi умеет импортировать существующие ресурсы.

Например, если у вас уже существует S3-бакет:

pulumi import aws:s3/bucket:Bucket \
documents \
company-documents

После импорта Pulumi создаст соответствующее описание ресурса.

Например:

bucket = aws.s3.Bucket(
    "documents",

    bucket="company-documents",

    opts=pulumi.ResourceOptions(
        import_="company-documents"
    )
)

Аналогичным образом можно работать с уже существующими Kubernetes-ресурсами.

Например:

pulumi import \
kubernetes:core/v1:Namespace \
monitoring \
monitoring

На практике большинство компаний выбирает гораздо более прагматичный путь.

Старая инфраструктура продолжает жить в Terraform.

Новые платформенные сервисы создаются на Pulumi.

Схематично это выглядит следующим образом:

Terraform
├── Networking
├── Shared Infrastructure
├── IAM
└── Legacy Services

Pulumi
├── Developer Platform
├── Preview Environments
├── Internal Portals
├── Self-Service APIs
└── Platform Automation

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

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

Настоящие недостатки Pulumi

После нескольких десятков страниц кода очень легко влюбиться в идею "инфраструктуры как обычного приложения". Однако именно здесь особенно важно сохранить профессиональную честность. У Pulumi существуют вполне реальные недостатки, и игнорировать их было бы неправильно.

Во-первых, порог входа оказывается выше. Если Terraform требует понимания HCL и модели ресурсов, то Pulumi предполагает знание языка программирования. Для команд, состоящих преимущественно из системных администраторов, это может стать серьёзным препятствием.

Во-вторых, свобода порождает соблазн. Получив полноценный язык программирования, некоторые инженеры начинают строить внутри Pulumi архитектуры, достойные отдельного микросервиса.

Например:

Pulumi Program
    ↓
Factory
    ↓
Strategy
    ↓
Repository
    ↓
Abstract Factory
    ↓
Builder
    ↓
Singleton
    ↓
Создание одного Security Group

Именно поэтому опытные команды постепенно приходят к очень простому правилу.

Инфраструктурный код - это всё ещё инфраструктурный код.

Тот факт, что можно написать двести строк абстракций ради создания Kubernetes Namespace, ещё не означает, что так действительно стоит поступать.

В-третьих, экосистема Terraform остаётся значительно богаче. Количество готовых модулей, накопленных практик и специалистов пока объективно больше.

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

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

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

Pulumi пытается сделать следующий шаг и предлагает относиться к инфраструктуре как к программному продукту.

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

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

Какой инструмент позволит нашей платформе развиваться быстрее, безопаснее и удобнее для тех людей, которые будут пользоваться ею каждый день?

Именно с этого вопроса обычно и начинается настоящее Platform Engineering.