Modern QA2026Тестирование Pulumi
Join

Course08 Infrastructure as Code Testing

Cutting-edge · Chapter 08

Тестирование Pulumi

Updated Jul 2026

Почему Pulumi меняет правила игры в тестировании

Фундаментальное преимущество Pulumi заключается в том, что инфраструктура описывается на реальных языках программирования — TypeScript, Python, Go или C#. Это означает, что вы можете использовать те же тестовые фреймворки, библиотеки утверждений и CI-паттерны, которые уже знаете. Не нужно осваивать новый DSL или инструмент тестирования: если вы знаете pytest, вы можете тестировать программы Pulumi на Python. Если знаете vitest, можете тестировать программы Pulumi на TypeScript.

Это существенный отход от HCL в Terraform, где для тестирования требуются либо специализированные инструменты (Terratest, написанный на Go), либо анализ JSON-плана. В Pulumi тест — это просто код, тестирующий код.

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

Фреймворк моков Pulumi позволяет тестировать логику инфраструктуры без развёртывания чего-либо. Моки перехватывают все вызовы облачных API и возвращают предсказуемые ответы.

Модульные тесты на TypeScript

import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
import { describe, it, expect, beforeAll } from "vitest";

// Mock all Pulumi resource creation
pulumi.runtime.setMocks({
    newResource: (args: pulumi.runtime.MockResourceArgs) => {
        // Return mock values for each resource type
        switch (args.type) {
            case "aws:s3/bucket:Bucket":
                return {
                    id: `${args.name}-id`,
                    state: {
                        ...args.inputs,
                        arn: `arn:aws:s3:::${args.inputs.bucket || args.name}`,
                        bucket: args.inputs.bucket || args.name,
                    },
                };
            case "aws:rds/instance:Instance":
                return {
                    id: `${args.name}-id`,
                    state: {
                        ...args.inputs,
                        endpoint: "mock-db.cluster-abc123.us-east-1.rds.amazonaws.com",
                    },
                };
            default:
                return {
                    id: `${args.name}-id`,
                    state: args.inputs,
                };
        }
    },
    call: (args: pulumi.runtime.MockCallArgs) => {
        return args.inputs;
    },
});

describe("S3 Data Bucket", () => {
    it("must have encryption enabled", async () => {
        const bucket = new aws.s3.Bucket("test-bucket", {
            serverSideEncryptionConfiguration: {
                rule: {
                    applyServerSideEncryptionByDefault: {
                        sseAlgorithm: "aws:kms",
                    },
                },
            },
        });

        const encryption = await new Promise((resolve) =>
            bucket.serverSideEncryptionConfiguration.apply((config) =>
                resolve(config?.rule?.applyServerSideEncryptionByDefault?.sseAlgorithm)
            )
        );
        expect(encryption).toBe("aws:kms");
    });

    it("must block public access", async () => {
        const bucket = new aws.s3.Bucket("test-bucket", {});
        const publicAccessBlock = new aws.s3.BucketPublicAccessBlock("test-pab", {
            bucket: bucket.id,
            blockPublicAcls: true,
            blockPublicPolicy: true,
            ignorePublicAcls: true,
            restrictPublicBuckets: true,
        });

        const blockAcls = await new Promise((resolve) =>
            publicAccessBlock.blockPublicAcls.apply(resolve)
        );
        expect(blockAcls).toBe(true);
    });

    it("must have versioning enabled", async () => {
        const bucket = new aws.s3.Bucket("test-bucket", {
            versioning: { enabled: true },
        });

        const versioning = await new Promise((resolve) =>
            bucket.versioning.apply((v) => resolve(v?.enabled))
        );
        expect(versioning).toBe(true);
    });
});

Модульные тесты на Python

# tests/test_infrastructure.py
import pulumi
import pytest

class MockResource:
    """Mock Pulumi resource provider for unit testing."""

    def __init__(self):
        self.resources = {}

    def __call__(self, args):
        self.resources[args.name] = args.inputs
        return pulumi.runtime.MockResourceArgs(
            id_=f"{args.name}-id",
            state=args.inputs,
        )

@pytest.fixture
def mock_pulumi():
    mock = MockResource()
    pulumi.runtime.set_mocks(
        pulumi.runtime.Mocks(
            new_resource=mock,
            call=lambda args: {},
        )
    )
    return mock

def test_database_not_publicly_accessible(mock_pulumi):
    """RDS instances must never be publicly accessible."""
    import pulumi_aws as aws

    db = aws.rds.Instance(
        "test-db",
        instance_class="db.t4g.micro",
        allocated_storage=20,
        engine="postgres",
        publicly_accessible=False,
    )

    # Assert the publicly_accessible property
    def check(value):
        assert value is False, "Database must not be publicly accessible"

    db.publicly_accessible.apply(check)

def test_rds_has_encryption(mock_pulumi):
    """RDS instances must have storage encryption enabled."""
    import pulumi_aws as aws

    db = aws.rds.Instance(
        "test-db",
        instance_class="db.t4g.micro",
        allocated_storage=20,
        engine="postgres",
        storage_encrypted=True,
    )

    def check(value):
        assert value is True, "Database storage must be encrypted"

    db.storage_encrypted.apply(check)

Тесты политик с Pulumi CrossGuard

Pulumi CrossGuard — это фреймворк политик как кода от Pulumi. В отличие от внешних инструментов типа Checkov или tfsec, которые анализируют HCL, политики CrossGuard написаны на том же языке, что и ваша инфраструктура, и выполняются во время развёртывания.

Пакет политик на TypeScript

// policy/index.ts
import { PolicyPack, validateResourceOfType } from "@pulumi/policy";
import * as aws from "@pulumi/aws";

new PolicyPack("security-policies", {
    policies: [
        {
            name: "s3-no-public-read",
            description: "S3 buckets must not allow public read access.",
            enforcementLevel: "mandatory",
            validateResource: validateResourceOfType(aws.s3.Bucket, (bucket, args, reportViolation) => {
                if (bucket.acl === "public-read" || bucket.acl === "public-read-write") {
                    reportViolation("S3 bucket has public read access. Use 'private' ACL.");
                }
            }),
        },
        {
            name: "rds-encryption-required",
            description: "RDS instances must have storage encryption enabled.",
            enforcementLevel: "mandatory",
            validateResource: validateResourceOfType(aws.rds.Instance, (db, args, reportViolation) => {
                if (!db.storageEncrypted) {
                    reportViolation("RDS instance must have storageEncrypted set to true.");
                }
            }),
        },
        {
            name: "ec2-no-public-ip",
            description: "EC2 instances must not have public IP addresses in production.",
            enforcementLevel: "advisory",
            validateResource: validateResourceOfType(aws.ec2.Instance, (instance, args, reportViolation) => {
                if (instance.associatePublicIpAddress) {
                    reportViolation("EC2 instance should not have a public IP. Use a load balancer.");
                }
            }),
        },
        {
            name: "required-tags",
            description: "All taggable resources must have required tags.",
            enforcementLevel: "mandatory",
            validateResource: (args, reportViolation) => {
                const tags = (args.props as any).tags;
                const requiredTags = ["Environment", "Team", "CostCenter"];

                if (tags) {
                    for (const required of requiredTags) {
                        if (!tags[required]) {
                            reportViolation(`Missing required tag: ${required}`);
                        }
                    }
                }
            },
        },
    ],
});

Запуск политик CrossGuard

# Run policies against a Pulumi preview (dry run)
pulumi preview --policy-pack ./policy/

# Run policies on actual deployment
pulumi up --policy-pack ./policy/

# Publish policies to Pulumi Cloud for organization-wide enforcement
pulumi policy publish ./policy/

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

Pulumi Automation API позволяет управлять Pulumi из тестового кода, аналогично тому, как Terratest управляет Terraform:

// tests/integration/s3-bucket.test.ts
import { LocalWorkspace, Stack } from "@pulumi/pulumi/automation";
import { S3Client, GetBucketEncryptionCommand } from "@aws-sdk/client-s3";
import { describe, it, expect, afterAll } from "vitest";

describe("S3 Data Bucket Integration Test", () => {
    let stack: Stack;
    let bucketName: string;

    it("deploys and validates the bucket", async () => {
        // Create a stack using inline Pulumi program
        stack = await LocalWorkspace.createOrSelectStack({
            stackName: "test-integration",
            projectName: "s3-test",
            program: async () => {
                const aws = await import("@pulumi/aws");
                const bucket = new aws.s3.Bucket("test-data-bucket", {
                    serverSideEncryptionConfiguration: {
                        rule: {
                            applyServerSideEncryptionByDefault: {
                                sseAlgorithm: "aws:kms",
                            },
                        },
                    },
                    versioning: { enabled: true },
                });
                return { bucketName: bucket.bucket };
            },
        });

        // Deploy real infrastructure
        const upResult = await stack.up({ onOutput: console.log });
        bucketName = upResult.outputs.bucketName.value;

        // Verify with AWS SDK
        const s3 = new S3Client({ region: "us-east-1" });
        const encryption = await s3.send(
            new GetBucketEncryptionCommand({ Bucket: bucketName })
        );

        expect(
            encryption.ServerSideEncryptionConfiguration?.Rules?.[0]
                ?.ApplyServerSideEncryptionByDefault?.SSEAlgorithm
        ).toBe("aws:kms");
    }, 120_000); // 2-minute timeout for real infra

    afterAll(async () => {
        if (stack) {
            await stack.destroy({ onOutput: console.log });
            await stack.workspace.removeStack("test-integration");
        }
    });
});

Сравнение тестирования Pulumi и Terraform

Аспект Terraform Pulumi
Язык модульных тестов Go (Terratest) или Python (анализ JSON плана) Тот же язык, что и инфраструктура (TS, Python, Go, C#)
Поддержка моков Ограниченная (анализ плана) Встроенный фреймворк моков
Фреймворк политик Внешний (OPA, Checkov, tfsec) Встроенный (CrossGuard)
Интеграционные тесты Terratest (только Go) Automation API (любой язык)
Скорость тестов (модульные) Быстро (анализ плана) Очень быстро (моки, без облака)
Скорость тестов (интеграционные) Медленно (реальная инфраструктура) Медленно (реальная инфраструктура)
Кривая обучения Средняя (изучить Go для Terratest) Низкая (используйте знакомый язык)
Экосистема инструментов Обширная (Terratest, Checkov, tfsec) Растущая (CrossGuard, нативные тесты)

Что выбрать

  • Если ваша команда уже использует Terraform и Go, Terratest — зрелый и хорошо задокументированный инструмент.
  • Если ваша команда использует TypeScript или Python, нативное тестирование Pulumi значительно проще во внедрении.
  • Если вам нужно обеспечить применение политик на уровне организации, Pulumi CrossGuard предлагает более интегрированный опыт, чем подключение OPA к Terraform.
  • Независимо от выбора, принцип тестирования один: статические проверки в основании, анализ plan/preview в середине, реальная инфраструктура на вершине.