Pulumi: IaC на Python вместо Terraform HCL

Что такое Pulumi и зачем он нужен

Pulumi — платформа Infrastructure as Code (IaC), позволяющая описывать облачную инфраструктуру на обычных языках программирования: Python, TypeScript, Go, C#, Java. В отличие от Terraform с его декларативным DSL (HCL), Pulumi использует полноценные языки, что даёт доступ к циклам, условиям, функциям, классам, библиотекам и IDE-поддержке.

Pulumi поддерживает все основные облака (AWS, Azure, GCP, Yandex Cloud), Kubernetes, Docker, а также традиционные провайдеры (VMware, Proxmox). Внутренне Pulumi использует те же провайдеры, что и Terraform (bridge к terraform-провайдерам), поэтому набор ресурсов идентичен.

Ключевые преимущества Pulumi: привычные языки программирования вместо DSL, полноценное тестирование инфраструктуры (unit-тесты, integration-тесты), встроенное управление секретами с шифрованием, нативная поддержка Kubernetes и Helm, policy as code через CrossGuard.

Pulumi vs Terraform: сравнение

Объективное сравнение двух подходов:

КритерийPulumiTerraform
ЯзыкPython, TS, Go, C#, JavaHCL (декларативный DSL)
Условия/циклыНативные (if, for)count, for_each, dynamic
Тестированиеpytest, jest, go testterraform test (ограничен)
StatePulumi Cloud / S3 / localS3 / Consul / TFC / local
СекретыВстроенное шифрованиеVault / SOPS (внешние)
СообществоРастущееКрупнейшее в IaC
ЛицензияApache 2.0BSL (с августа 2023)

Установка и первый проект

Установка Pulumi CLI и создание первого проекта на Python:

# Установка Pulumi CLI
curl -fsSL https://get.pulumi.com | sh
export PATH=$PATH:$HOME/.pulumi/bin

# Проверка
pulumi version
# v3.110.0

# Создание нового проекта
mkdir my-infra && cd my-infra
pulumi new python -y
# Создаст:
# - Pulumi.yaml      — метаданные проекта
# - Pulumi.dev.yaml  — конфигурация стека dev
# - __main__.py      — основной код инфраструктуры
# - requirements.txt — зависимости Python
# - venv/            — виртуальное окружение

# Активация virtualenv и установка зависимостей
source venv/bin/activate
pip install -r requirements.txt

Структура проекта Pulumi

Типичная структура production-проекта:

my-infra/
├── Pulumi.yaml           # Имя проекта, runtime, описание
├── Pulumi.dev.yaml       # Конфигурация для стека dev
├── Pulumi.prod.yaml      # Конфигурация для стека prod
├── __main__.py           # Точка входа
├── network/
│   ├── __init__.py
│   └── vpc.py            # Модуль сети
├── compute/
│   ├── __init__.py
│   └── instances.py      # Модуль серверов
├── database/
│   ├── __init__.py
│   └── rds.py            # Модуль БД
├── requirements.txt
└── tests/
    ├── test_network.py
    └── test_compute.py

Создание инфраструктуры AWS на Python

Пример: VPC, подсети, Security Group и EC2-инстанс:

# __main__.py
import pulumi
import pulumi_aws as aws

config = pulumi.Config()
env = pulumi.get_stack()  # dev / staging / prod

# VPC
vpc = aws.ec2.Vpc("main-vpc",
    cidr_block="10.0.0.0/16",
    enable_dns_hostnames=True,
    tags={"Name": f"{env}-vpc", "Environment": env}
)

# Подсети (используем циклы Python!)
azs = aws.get_availability_zones(state="available")
subnets = []
for i, az in enumerate(azs.names[:3]):
    subnet = aws.ec2.Subnet(f"subnet-{i}",
        vpc_id=vpc.id,
        cidr_block=f"10.0.{i}.0/24",
        availability_zone=az,
        map_public_ip_on_launch=True,
        tags={"Name": f"{env}-subnet-{az}"}
    )
    subnets.append(subnet)

# Security Group с условной логикой
allow_ssh = config.get_bool("allow_ssh") or False

ingress_rules = [
    aws.ec2.SecurityGroupIngressArgs(
        protocol="tcp", from_port=80, to_port=80,
        cidr_blocks=["0.0.0.0/0"], description="HTTP"
    ),
    aws.ec2.SecurityGroupIngressArgs(
        protocol="tcp", from_port=443, to_port=443,
        cidr_blocks=["0.0.0.0/0"], description="HTTPS"
    ),
]

if allow_ssh:
    ingress_rules.append(
        aws.ec2.SecurityGroupIngressArgs(
            protocol="tcp", from_port=22, to_port=22,
            cidr_blocks=[config.require("admin_cidr")],
            description="SSH admin"
        )
    )

sg = aws.ec2.SecurityGroup("web-sg",
    vpc_id=vpc.id,
    ingress=ingress_rules,
    egress=[aws.ec2.SecurityGroupEgressArgs(
        protocol="-1", from_port=0, to_port=0,
        cidr_blocks=["0.0.0.0/0"]
    )]
)

# EC2 инстанс
ami = aws.ec2.get_ami(
    most_recent=True,
    owners=["099720109477"],  # Canonical (Ubuntu)
    filters=[aws.ec2.GetAmiFilterArgs(
        name="name", values=["ubuntu/images/hvm-ssd/ubuntu-*-24.04-amd64-server-*"]
    )]
)

instance = aws.ec2.Instance("web-server",
    ami=ami.id,
    instance_type=config.get("instance_type") or "t3.micro",
    subnet_id=subnets[0].id,
    vpc_security_group_ids=[sg.id],
    tags={"Name": f"{env}-web-server"}
)

# Экспорт значений (аналог output в Terraform)
pulumi.export("vpc_id", vpc.id)
pulumi.export("instance_ip", instance.public_ip)
pulumi.export("instance_id", instance.id)

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

Стек (stack) в Pulumi — это изолированный экземпляр инфраструктуры (аналог workspace в Terraform). Типичное использование: dev, staging, prod.

# Создание стеков
pulumi stack init dev
pulumi stack init staging
pulumi stack init prod

# Переключение между стеками
pulumi stack select prod

# Конфигурация стека
pulumi config set aws:region eu-central-1
pulumi config set instance_type t3.large
pulumi config set allow_ssh true
pulumi config set admin_cidr 10.0.0.0/8

# Секреты (шифруются автоматически!)
pulumi config set --secret db_password 'SuperSecretPass'
pulumi config set --secret api_key 'sk-live-xxx'

# В коде:
config = pulumi.Config()
db_pass = config.require_secret("db_password")  # Output[str]
# Значение зашифровано в Pulumi.prod.yaml
# и расшифровывается только при pulumi up

State backend: где хранить состояние

Pulumi поддерживает несколько бэкендов для хранения state:

# 1. Pulumi Cloud (по умолчанию, бесплатно для индивидуального использования)
pulumi login

# 2. S3-совместимое хранилище
pulumi login s3://my-pulumi-state?region=eu-central-1

# 3. Локальная файловая система (для тестов)
pulumi login --local
# State хранится в ~/.pulumi/stacks/

# 4. Azure Blob
pulumi login azblob://pulumi-state

# 5. Google Cloud Storage
pulumi login gs://pulumi-state-bucket

Для командной работы рекомендуется Pulumi Cloud или S3 с DynamoDB-блокировкой (аналог Terraform backend).

Тестирование инфраструктуры

Главное преимущество Pulumi — возможность писать полноценные тесты на pytest/unittest:

# tests/test_network.py
import pulumi
import pytest

class MockedMixin:
    """Mock для Pulumi resources в тестах"""
    def __init__(self, name, props=None):
        self.name = name
        self.props = props or {}

# Unit-тест: проверяем теги VPC
@pulumi.runtime.test
def test_vpc_has_environment_tag():
    """VPC должен иметь тег Environment"""
    import __main__
    
    def check_tags(args):
        tags = args[0]
        assert "Environment" in tags, "VPC missing Environment tag"
        assert tags["Environment"] in ["dev", "staging", "prod"]
    
    return pulumi.Output.all(
        __main__.vpc.tags
    ).apply(check_tags)

# Policy test: проверяем что SSH не открыт на 0.0.0.0/0
@pulumi.runtime.test
def test_no_open_ssh():
    """Security Group не должен открывать SSH на весь мир"""
    import __main__
    
    def check_sg(args):
        ingress_rules = args[0]
        for rule in ingress_rules:
            if rule["from_port"] == 22:
                assert "0.0.0.0/0" not in rule.get("cidr_blocks", []), \
                    "SSH open to 0.0.0.0/0 is not allowed!"
    
    return pulumi.Output.all(
        __main__.sg.ingress
    ).apply(check_sg)

# Запуск тестов
# pytest tests/ -v

Integration тесты с реальными ресурсами

Pulumi Automation API позволяет запускать стеки программно — идеально для integration-тестов:

import pulumi.automation as auto
import requests

def test_web_server_responds():
    """Integration test: деплой и проверка HTTP"""
    stack = auto.create_or_select_stack(
        stack_name="test",
        project_name="my-infra",
        program=lambda: None,  # Используем существующий код
        work_dir="."
    )
    
    try:
        # Деплой тестового стека
        up_result = stack.up(on_output=print)
        ip = up_result.outputs["instance_ip"].value
        
        # Проверяем HTTP-ответ
        response = requests.get(f"http://{ip}", timeout=30)
        assert response.status_code == 200
    finally:
        # Удаляем тестовую инфраструктуру
        stack.destroy(on_output=print)
        stack.workspace.remove_stack("test")

Pulumi в CI/CD Pipeline

Интеграция Pulumi в GitLab CI/GitHub Actions для автоматического деплоя:

# .github/workflows/deploy.yml
name: Deploy Infrastructure

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  preview:
    if: github.event_name == 'pull_request'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: '3.12' }
      - run: pip install -r requirements.txt
      - uses: pulumi/actions@v5
        with:
          command: preview
          stack-name: prod
          comment-on-pr: true  # Добавит diff в PR
        env:
          PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

  deploy:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with: { python-version: '3.12' }
      - run: pip install -r requirements.txt
      - uses: pulumi/actions@v5
        with:
          command: up
          stack-name: prod
        env:
          PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Миграция с Terraform на Pulumi

Pulumi предоставляет утилиту pulumi convert для автоматической конвертации HCL в Python/TypeScript:

# Конвертация Terraform HCL → Pulumi Python
pulumi convert --from terraform --language python --out ./pulumi-project

# Импорт существующих ресурсов (аналог terraform import)
pulumi import aws:ec2/instance:Instance web-server i-0abc123def456
# Pulumi сгенерирует код Python для существующего ресурса

# Импорт Terraform state в Pulumi
pulumi import --from terraform ./terraform.tfstate

# Пример конвертации:
# Terraform HCL:
# resource "aws_instance" "web" {
#   ami           = "ami-0c55b159cbfafe1f0"
#   instance_type = "t3.micro"
#   tags = { Name = "web" }
# }

# Pulumi Python (автогенерация):
import pulumi_aws as aws
web = aws.ec2.Instance("web",
    ami="ami-0c55b159cbfafe1f0",
    instance_type="t3.micro",
    tags={"Name": "web"}
)

Важно: автоконвертация покрывает 80-90% кода, сложные конструкции (dynamic blocks, provisioners) требуют ручной доработки. Рекомендуется конвертировать модуль за модулем, а не весь проект сразу.

Часто задаваемые вопросы

Pulumi CLI и все провайдеры — open-source (Apache 2.0), полностью бесплатные. Pulumi Cloud (SaaS для state management, secrets, team collaboration) имеет бесплатный тариф для индивидуального использования и до 200 ресурсов. Платные тарифы нужны для команд (RBAC, audit log, SAML SSO). Можно обойтись без Pulumi Cloud, храня state в S3/GCS — тогда платить не за что.

Python — лучший выбор для DevOps/SRE инженеров: большинство инфраструктурных скриптов уже на Python, знакомый синтаксис, огромная экосистема библиотек. TypeScript — для команд с frontend-бэкграундом и проектов с Kubernetes (хорошая типизация). Go — для высокопроизводительных провайдеров и командных утилит. Все языки предоставляют идентичный функционал — выбирайте по компетенциям команды.

Да, существует провайдер pulumi-yandex с поддержкой VPC, Compute, Managed DB, Container Registry, Cloud Functions и других сервисов. Установка: pip install pulumi-yandex. Конфигурация: pulumi config set yandex:token YC_TOKEN и pulumi config set yandex:cloud-id xxx. Провайдер использует terraform-provider-yandex bridge, поэтому покрытие ресурсов идентично.

Команда pulumi refresh считывает реальное состояние ресурсов из облака и обновляет state. Если кто-то изменил ресурс вручную (drift), pulumi preview покажет разницу. В Pulumi Cloud Enterprise есть автоматический drift detection по расписанию. Для бесплатной версии — запускайте pulumi refresh && pulumi preview по cron и алертите при обнаружении расхождений.

Да, и это одна из сильнейших сторон. Вы используете обычные механизмы языка: Python-пакеты (pip), npm-модули (TypeScript), Go-модули. Создайте класс VpcStack с параметрами (CIDR, количество подсетей, теги) и переиспользуйте в dev/staging/prod стеках. Pulumi также поддерживает Component Resources — абстракции над группой ресурсов, аналогичные Terraform-модулям, но с полноценным ООП.

Нужна помощь с настройкой?

Специалисты АйТи Фреш помогут с внедрением и настройкой — 15+ лет опыта, обслуживание от 15 000 ₽/мес

📞 Связаться с нами
#Pulumi#Infrastructure as Code#IaC Python#Pulumi vs Terraform#Pulumi AWS#DevOps инструменты#инфраструктура как код#Pulumi stack
Комментарии 0

Оставить комментарий

загрузка...