Перейти к основному содержимому

8.04. Pulumi

Разработчику Архитектору Инженеру

Pulumi

Что такое Pulumi

Pulumi — это инструмент управления облачной инфраструктурой, позволяющий описывать, развертывать и обновлять ресурсы с помощью общих языков программирования. В отличие от традиционных подходов, основанных на декларативных шаблонах в формате YAML или JSON, Pulumi использует полноценные языки, такие как TypeScript, Python, Go, C# и Java. Это позволяет применять привычные конструкции: переменные, функции, циклы, типы, модули и объектно-ориентированное проектирование.

Основная цель Pulumi — обеспечить единый способ описания инфраструктуры, который сочетает выразительность кода с надежностью и воспроизводимостью, присущими инфраструктуре как коду (Infrastructure as Code, IaC). Pulumi поддерживает все крупные облачные платформы: AWS, Azure, Google Cloud, а также Kubernetes, OpenStack, VMware и другие через провайдеры.

Архитектура Pulumi

Ядро и движок

Ядро Pulumi состоит из клиентского CLI-инструмента и движка, отвечающего за выполнение программ. При запуске команды pulumi up движок загружает пользовательский код, анализирует его и строит граф зависимостей между ресурсами. Этот граф определяет порядок создания, обновления или удаления ресурсов.

Движок не выполняет операции напрямую. Он взаимодействует с внешними провайдерами — специализированными компонентами, реализующими логику работы с конкретными API облачных платформ. Например, провайдер AWS преобразует вызовы в Pulumi-коде в соответствующие вызовы AWS SDK.

Провайдеры

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

Провайдеры могут быть:

  • Облачными — для AWS, Azure, GCP.
  • Контейнерными — для Kubernetes.
  • Специализированными — для Datadog, Snowflake, Auth0 и других SaaS-сервисов.
  • Универсальными — для Terraform-провайдеров через мост Pulumi-Terraform.

Каждый ресурс в Pulumi связан с определённым провайдером. Например, при создании экземпляра EC2 в AWS используется провайдер AWS, а при создании Deployment в Kubernetes — провайдер Kubernetes.

Состояние (State)

Pulumi сохраняет состояние инфраструктуры в специальном хранилище, называемом бэкендом состояния. Это состояние содержит полную информацию о текущих ресурсах: их идентификаторы, входные параметры, выходные данные и зависимости. Без этого состояния Pulumi не смог бы определить, какие изменения нужно применить при следующем запуске.

Поддерживаются следующие бэкенды:

  • Pulumi Service — облачный сервис от создателей Pulumi с веб-интерфейсом, историей изменений и совместной работой.
  • Локальный файл — для экспериментов и обучения.
  • Объектные хранилища — Amazon S3, Azure Blob Storage, Google Cloud Storage.
  • Хранилища с поддержкой блокировок — например, HashiCorp Consul или PostgreSQL.

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

Модель программирования

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

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

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

Ресурсы

В Pulumi всё, что создаётся в облаке, является ресурсом. Ресурс — это объект, представляющий сущность в целевой системе: виртуальная машина, база данных, сетевая группа безопасности, DNS-запись и так далее.

Каждый ресурс имеет:

  • Тип — например, aws.ec2.Instance.
  • Имя — логическое имя внутри программы.
  • Входные параметры — конфигурация ресурса.
  • Выходные данные — атрибуты, доступные после создания (например, IP-адрес).

Пример на Python:

import pulumi
import pulumi_aws as aws

# Создание VPC
vpc = aws.ec2.Vpc("my-vpc",
cidr_block="10.0.0.0/16")

# Создание подсети
subnet = aws.ec2.Subnet("my-subnet",
vpc_id=vpc.id,
cidr_block="10.0.1.0/24")

# Создание EC2-инстанса
instance = aws.ec2.Instance("web-server",
instance_type="t3.micro",
ami="ami-0abcdef1234567890",
subnet_id=subnet.id)

Здесь vpc.id — это выходное значение ресурса VPC, которое автоматически становится входным для подсети. Pulumi отслеживает такие зависимости и гарантирует правильный порядок создания.

Компоненты

Компоненты — это пользовательские абстракции, объединяющие несколько ресурсов в логическую единицу. Они позволяют повторно использовать сложные паттерны инфраструктуры.

Пример компонента на TypeScript:

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

class WebApp extends pulumi.ComponentResource {
public readonly bucket: aws.s3.Bucket;
public readonly cdn: aws.cloudfront.Distribution;

constructor(name: string, opts?: pulumi.ResourceOptions) {
super("custom:WebApp", name, {}, opts);

this.bucket = new aws.s3.Bucket(`${name}-bucket`, {
website: {
indexDocument: "index.html",
},
}, { parent: this });

this.cdn = new aws.cloudfront.Distribution(`${name}-cdn`, {
origins: [{
domainName: this.bucket.websiteEndpoint,
originId: this.bucket.id,
}],
defaultCacheBehavior: {
targetOriginId: this.bucket.id,
viewerProtocolPolicy: "redirect-to-https",
allowedMethods: ["GET", "HEAD"],
cachedMethods: ["GET", "HEAD"],
forwardedValues: {
cookies: { forward: "none" },
queryString: false,
},
},
enabled: true,
isIpv6Enabled: true,
comment: "CDN for static website",
}, { parent: this });
}
}

const app = new WebApp("my-app");

Компонент WebApp инкапсулирует создание S3-бакета и CloudFront-дистрибуции. Все ресурсы внутри помечены как дочерние (parent: this), что позволяет Pulumi корректно отображать иерархию в состоянии и веб-интерфейсе.

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

Конфигурационные переменные

Pulumi предоставляет механизм для хранения конфигурационных значений, специфичных для окружения (dev, staging, prod). Эти значения задаются через CLI или файл Pulumi.<stack>.yaml.

Пример:

# Pulumi.dev.yaml
config:
aws:region: us-west-2
my-app:instanceType: t3.micro
my-app:enableMonitoring: true

В коде эти значения читаются так:

config = pulumi.Config()
instance_type = config.get("instanceType") or "t3.micro"
enable_monitoring = config.get_bool("enableMonitoring")

Конфигурация может содержать чувствительные данные, такие как API-ключи. Для этого используется флаг --secret при установке значения:

pulumi config set my-app:apiKey ABC123 --secret

Такие значения шифруются в состоянии и не отображаются в открытом виде в логах или веб-интерфейсе.

Стеки

Стек — это изолированное окружение инфраструктуры. Один и тот же код может развертываться в несколько стеков: dev, staging, prod. Каждый стек имеет своё собственное состояние и конфигурацию.

Создание стека:

pulumi stack init dev
pulumi stack init prod

Переключение между стеками:

pulumi stack select dev

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

Жизненный цикл инфраструктуры

Планирование и применение

Команда pulumi preview показывает, какие изменения будут внесены в инфраструктуру, без их фактического выполнения. Это аналог terraform plan.

Команда pulumi up применяет изменения. Pulumi:

  1. Выполняет пользовательский код.
  2. Сравнивает результат с текущим состоянием.
  3. Строит план изменений.
  4. Запрашивает подтверждение у пользователя.
  5. Выполняет операции в порядке зависимостей.

Если операция завершается ошибкой, Pulumi останавливает процесс и оставляет инфраструктуру в частично изменённом состоянии. Последующий запуск pulumi up продолжит с места остановки.

Обновление и замена ресурсов

Pulumi различает два типа изменений:

  • Обновление in-place — изменение атрибута, который поддерживает обновление без пересоздания (например, теги).
  • Замена — изменение атрибута, требующего удаления старого ресурса и создания нового (например, тип инстанса EC2).

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

Удаление

Команда pulumi destroy удаляет все ресурсы в стеке. Pulumi удаляет ресурсы в обратном порядке зависимостей, чтобы избежать ошибок (например, нельзя удалить VPC, пока в нём есть подсети).

Поддержка языков

TypeScript / JavaScript

TypeScript — один из самых популярных языков в экосистеме Pulumi. Он предоставляет строгую типизацию, автодополнение и рефакторинг через IDE. Все провайдеры генерируют TypeScript-определения автоматически.

Пример:

import * as aws from "@pulumi/aws";

const bucket = new aws.s3.Bucket("my-bucket", {
versioning: {
enabled: true,
},
});

Python

Python поддерживается полностью, включая типы (через mypy). Библиотеки провайдеров публикуются в PyPI.

Пример:

import pulumi_aws as aws

bucket = aws.s3.Bucket("my-bucket",
versioning=aws.s3.BucketVersioningArgs(
enabled=True
))

Go

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

Пример:

package main

import (
"github.com/pulumi/pulumi-aws/sdk/v6/go/aws/s3"
"github.com/pulumi/pulumi/sdk/v3/go/pulumi"
)

func main() {
pulumi.Run(func(ctx *pulumi.Context) error {
bucket, err := s3.NewBucket(ctx, "my-bucket", &s3.BucketArgs{
Versioning: s3.BucketVersioningArgs{
Enabled: pulumi.Bool(true),
},
})
if err != nil {
return err
}
return nil
})
}

C#

C# поддерживается через .NET SDK. Интеграция с Visual Studio и Rider обеспечивает полный опыт разработки.

Пример:

using Pulumi;
using Aws = Pulumi.Aws;

class MyStack : Stack
{
public MyStack()
{
var bucket = new Aws.S3.Bucket("my-bucket", new Aws.S3.BucketArgs
{
Versioning = new Aws.S3.Inputs.BucketVersioningArgs
{
Enabled = true,
}
});
}
}

Java

Java поддерживается начиная с версии 3.0 Pulumi. Используется стандартный Maven/Gradle workflow.

Пример:

import com.pulumi.Pulumi;
import com.pulumi.aws.s3.Bucket;
import com.pulumi.aws.s3.BucketArgs;
import com.pulumi.aws.s3.inputs.BucketVersioningArgs;

public class App {
public static void main(String[] args) {
Pulumi.run(ctx -> {
var bucket = new Bucket("my-bucket", BucketArgs.builder()
.versioning(BucketVersioningArgs.builder()
.enabled(true)
.build())
.build());
});
}
}

Провайдеры и экосистема

Официальные провайдеры

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

Примеры:

  • @pulumi/aws — для Amazon Web Services.
  • @pulumi/azure-native — для Azure с использованием нативных API Microsoft.
  • @pulumi/gcp — для Google Cloud Platform.
  • @pulumi/kubernetes — для управления Kubernetes-кластерами и ресурсами внутри них.

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

Terraform-мост

Pulumi может использовать провайдеры, созданные для Terraform, через специальный мост. Это расширяет поддержку до тысяч дополнительных сервисов, которые ещё не имеют нативного провайдера в Pulumi.

Для этого используется команда:

pulumi convert terraform

или установка пакета вида pulumi-terraform-bridge. Например, для работы с Cloudflare через Terraform-провайдер:

import pulumi_cloudflare as cloudflare

Мост преобразует вызовы Terraform-провайдера в совместимый с Pulumi интерфейс, сохраняя все зависимости и жизненный цикл ресурсов.

Кастомные провайдеры

Если нужного провайдера нет, его можно написать самостоятельно. Pulumi предоставляет SDK для создания провайдеров на Go. Провайдер реализует CRUD-операции (Create, Read, Update, Delete) для каждого типа ресурса и регистрирует их в системе.

Кастомный провайдер может управлять:

  • Внутренними корпоративными API.
  • Устаревшими системами без облачного интерфейса.
  • Специфичными SaaS-платформами.

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

Безопасность и управление секретами

Шифрование состояния

Все чувствительные данные, такие как пароли, ключи доступа или сертификаты, шифруются перед сохранением в состоянии. Pulumi использует AES-256-GCM для шифрования. Ключ шифрования хранится отдельно от состояния:

  • В Pulumi Service — ключ управляется платформой с поддержкой HSM.
  • В объектных хранилищах — ключ можно задать через KMS (AWS KMS, Azure Key Vault, GCP KMS).

Это исключает возможность чтения секретов даже при компрометации файла состояния.

Конфигурация с секретами

При установке значения через pulumi config set --secret, оно помечается как секретное. В коде такие значения доступны через методы вроде config.requireSecret(), которые возвращают специальный тип Output<T>, помеченный как секрет.

Пример на C#:

var config = new Pulumi.Config();
var dbPassword = config.RequireSecret("dbPassword");

var db = new Aws.Rds.Instance("main-db", new Aws.Rds.InstanceArgs
{
Password = dbPassword,
// ...
});

Pulumi гарантирует, что значение dbPassword никогда не будет отображено в логах, выводе CLI или веб-интерфейсе. Даже при экспорте состояния (pulumi stack export) секреты остаются зашифрованными.

Интеграция с внешними хранилищами секретов

Pulumi может читать секреты напрямую из HashiCorp Vault, AWS Secrets Manager, Azure Key Vault и других систем. Для этого используются соответствующие SDK внутри пользовательского кода.

Пример на Python с AWS Secrets Manager:

import boto3
import pulumi

def get_secret(secret_name):
client = boto3.client('secretsmanager', region_name='us-west-2')
response = client.get_secret_value(SecretId=secret_name)
return response['SecretString']

password = get_secret("prod/db-password")
# Использовать password как обычную строку

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

Интеграция с CI/CD

Автоматизация через CLI

Pulumi CLI поддерживает неразрывную интеграцию с любыми системами CI/CD: GitHub Actions, GitLab CI, Jenkins, Azure DevOps и другими. Основные команды:

  • pulumi login — аутентификация в бэкенде состояния.
  • pulumi stack select — выбор окружения.
  • pulumi preview — проверка изменений.
  • pulumi up --yes — автоматическое применение без подтверждения.

Все команды работают в headless-режиме, без интерактивного ввода.

Пример GitHub Actions

name: Deploy Infrastructure
on:
push:
branches: [main]

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 18

- name: Install dependencies
run: npm install

- name: Install Pulumi CLI
uses: pulumi/actions@v4
with:
command: install

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: us-west-2

- name: Deploy
uses: pulumi/actions@v4
with:
command: up
stack-name: prod
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}

Этот workflow развертывает инфраструктуру при каждом пуше в main. Все секреты передаются через GitHub Secrets, состояние хранится в Pulumi Service.

Управление правами доступа

Pulumi Service поддерживает RBAC (Role-Based Access Control). Можно назначать права:

  • Чтение стека — для аудита.
  • Запись — для разработчиков.
  • Администрирование — для DevOps-инженеров.

Права применяются на уровне организации, проекта или отдельного стека. Это позволяет точно контролировать, кто может вносить изменения в production-инфраструктуру.

Сравнение с другими инструментами

Pulumi и Terraform

Оба инструмента реализуют концепцию Infrastructure as Code, но различаются подходом:

  • Язык: Terraform использует HCL — декларативный DSL. Pulumi использует общие языки программирования.
  • Абстракции: В Terraform повторное использование достигается через модули. В Pulumi — через классы, функции и компоненты.
  • Состояние: Оба хранят состояние, но Pulumi предлагает встроенный облачный бэкенд с историей и совместной работой.
  • Производительность: Pulumi выполняет код один раз и строит план. Terraform парсит конфигурацию и строит граф зависимостей.

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

Pulumi и AWS CDK

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

CDK генерирует CloudFormation-шаблоны, тогда как Pulumi взаимодействует с API напрямую. Это делает Pulumi более гибким и быстрым при работе с ресурсами вне AWS.

Pulumi и Crossplane

Crossplane — это Kubernetes-ориентированный инструмент, который расширяет API Kubernetes для управления внешними ресурсами. Pulumi не требует Kubernetes и может управлять любыми ресурсами независимо от оркестратора.

Crossplane хорош для унификации управления через kubectl, но сложен в настройке. Pulumi проще в освоении и не требует эксплуатации дополнительного control plane.


Тестирование и отладка инфраструктурного кода

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

Pulumi поддерживает написание модульных тестов для инфраструктурного кода с использованием стандартных фреймворков языка: pytest для Python, Jest для TypeScript, NUnit/xUnit для C#, JUnit для Java. Тесты проверяют логику программы до её выполнения в облаке.

Пример на Python:

import pulumi
from pulumi_aws import s3
import pytest

def test_bucket_has_versioning():
# Отключаем реальное взаимодействие с AWS
pulumi.runtime.set_mocks(MyMocks())

# Импортируем модуль с инфраструктурой
import infra

# Проверяем, что бакет создан с версионированием
bucket = infra.my_bucket
assert bucket.versioning.enabled == True

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

Интеграционное тестирование

Для проверки работы инфраструктуры в реальном облаке используется подход deployment testing. Pulumi предоставляет утилиту pulumi preview --expect-no-changes для проверки идемпотентности: если инфраструктура уже развернута, повторный запуск не должен вносить изменений.

Также можно использовать временные стеки в CI/CD:

  1. Создать стек test-12345.
  2. Развернуть инфраструктуру.
  3. Выполнить проверки (например, через curl или SDK).
  4. Удалить стек.

Это гарантирует, что изменения не ломают работоспособность системы.

Отладка

Pulumi сохраняет полный журнал выполнения, включая входные и выходные данные каждого ресурса. При ошибке выводится:

  • Имя ресурса.
  • Тип ошибки.
  • Значения параметров.
  • Стек вызовов в пользовательском коде.

Для интерактивной отладки можно использовать стандартные средства IDE:

  • Точки останова в Visual Studio Code (с расширением Pulumi).
  • Пошаговое выполнение в PyCharm или Rider.
  • Логирование через pulumi.log.info().

Миграция и совместимость

Миграция из Terraform

Pulumi предоставляет утилиту pulumi convert terraform, которая автоматически преобразует HCL-конфигурации в код на выбранном языке. Процесс включает:

  1. Анализ .tf файлов.
  2. Генерация эквивалентного кода.
  3. Сохранение состояния Terraform как начального состояния Pulumi.

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

Совместное использование состояния

Если команда использует Terraform и Pulumi одновременно, важно избегать конфликтов. Рекомендуется:

  • Разделять ресурсы по доменам (например, сеть — Terraform, приложения — Pulumi).
  • Использовать pulumi import для захвата существующих ресурсов в управление Pulumi без их пересоздания.

Пример импорта:

pulumi import aws:s3/bucket:Bucket my-bucket existing-bucket-name

Эта команда добавит существующий S3-бакет в состояние Pulumi, сохранив его содержимое.

Передовые практики

Идемпотентность и воспроизводимость

Каждый запуск Pulumi-программы должен давать одинаковый результат при одинаковых входных данных. Для этого:

  • Избегайте побочных эффектов (например, генерации случайных значений без фиксированного seed).
  • Используйте явные зависимости между ресурсами.
  • Не полагайтесь на глобальное состояние.

Версионирование зависимостей

Все зависимости (провайдеры, SDK) должны быть закреплены в файле package.json, requirements.txt или *.csproj. Это гарантирует, что развертывание сегодня и через год даст одинаковый результат.

Пример package.json:

{
"dependencies": {
"@pulumi/pulumi": "3.100.0",
"@pulumi/aws": "6.40.0"
}
}

Минимизация привилегий

Учётные данные, используемые Pulumi для развёртывания, должны иметь минимально необходимые права. Например, для управления только S3 и CloudFront не требуется доступ к EC2 или IAM.

В AWS это достигается через IAM-роли с политиками вида:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:*", "cloudfront:*"],
"Resource": "*"
}
]
}

Документирование компонентов

Каждый пользовательский компонент должен содержать:

  • Описание назначения.
  • Список входных параметров.
  • Пример использования.

Это упрощает поддержку и передачу знаний в команде.

Экосистемные инструменты

Pulumi ESC (Environments, Secrets, and Configuration)

ESC — это сервис для централизованного управления окружениями и секретами. Он позволяет:

  • Хранить конфигурацию в YAML-файлах.
  • Динамически подставлять значения в зависимости от стека.
  • Интегрироваться с Vault, AWS Secrets Manager и другими системами.

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

values:
database:
host: ${aws.secretsManager("prod/db").host}
port: 5432

Эта конфигурация безопасно подставляется в Pulumi-программу без хранения секретов в коде.

Pulumi AI

Pulumi AI — это встроенный помощник, который генерирует инфраструктурный код по описанию на естественном языке. Например, запрос «Create an S3 bucket with public read access» превращается в готовый код на Python или TypeScript.

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