Тестирование 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 в середине, реальная инфраструктура на вершине.