Pulumi Policy-as-Code Testing: CrossGuard Policies and Compliance in CI/CD
Pulumi manages infrastructure using general-purpose programming languages — TypeScript, Python, Go, C#, Java. This means Pulumi programs benefit from the same testing infrastructure as application code: unit tests, integration tests, and CI/CD pipelines.
Pulumi's policy-as-code system, CrossGuard, extends this to compliance testing: you write policies in TypeScript or Python that evaluate Pulumi resource configurations, and CrossGuard enforces them during deployment. This guide covers testing Pulumi programs, writing CrossGuard policies, and testing those policies.
Testing Pulumi Programs
Pulumi programs are code, so they can be tested like code.
Unit testing with mocked resource providers:
// infrastructure/storage.ts
import * as aws from "@pulumi/aws";
export function createStorage(name: string, encrypted: boolean = true) {
const bucket = new aws.s3.Bucket(name, {
versioning: {
enabled: true,
},
serverSideEncryptionConfiguration: encrypted ? {
rule: {
applyServerSideEncryptionByDefault: {
sseAlgorithm: "aws:kms",
},
},
} : undefined,
tags: {
Environment: pulumi.getStack(),
ManagedBy: "pulumi",
},
});
return bucket;
}// infrastructure/storage.test.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
// Configure Pulumi to use mocks
pulumi.runtime.setMocks({
newResource: function(args: pulumi.runtime.MockResourceArgs): {id: string, state: any} {
return {
id: `${args.name}-id`,
state: args.inputs,
};
},
call: function(args: pulumi.runtime.MockCallArgs) {
return args.inputs;
},
}, "project", "test-stack", false);
// Tests must be imported AFTER setting up mocks
import { createStorage } from './storage';
describe("Storage", () => {
test("S3 bucket has versioning enabled", async () => {
const bucket = createStorage("test-bucket");
const versioning = await bucket.versioning;
expect(versioning?.enabled).toBe(true);
});
test("S3 bucket has encryption enabled by default", async () => {
const bucket = createStorage("test-bucket");
const encryption = await bucket.serverSideEncryptionConfiguration;
expect(encryption).toBeDefined();
expect(encryption?.rule?.applyServerSideEncryptionByDefault?.sseAlgorithm)
.toBe("aws:kms");
});
test("S3 bucket has required tags", async () => {
const bucket = createStorage("test-bucket");
const tags = await bucket.tags;
expect(tags).toMatchObject({
ManagedBy: "pulumi",
});
});
test("Encryption can be disabled explicitly", async () => {
const bucket = createStorage("test-bucket", false);
const encryption = await bucket.serverSideEncryptionConfiguration;
expect(encryption).toBeUndefined();
});
});Python unit tests:
# test_storage.py
import unittest
import pulumi
class MyMocks(pulumi.runtime.Mocks):
def new_resource(self, args: pulumi.runtime.MockResourceArgs):
return [args.name + '_id', args.inputs]
def call(self, args: pulumi.runtime.MockCallArgs):
return {}
pulumi.runtime.set_mocks(MyMocks(), preview=False)
# Import after setting mocks
from storage import create_storage
class StorageTest(unittest.TestCase):
@pulumi.runtime.test
def test_versioning_enabled(self):
bucket = create_storage("test-bucket")
def check_versioning(args):
versioning = args[0]
self.assertIsNotNone(versioning)
self.assertTrue(versioning["enabled"])
return pulumi.Output.all(bucket.versioning).apply(check_versioning)
@pulumi.runtime.test
def test_encryption_enabled_by_default(self):
bucket = create_storage("test-bucket")
def check_encryption(args):
encryption = args[0]
self.assertIsNotNone(encryption)
return pulumi.Output.all(bucket.server_side_encryption_configuration).apply(check_encryption)Integration Testing
Pulumi's test framework supports deploying real infrastructure in CI:
// integration_test.go (Go-based integration tests)
package main
import (
"testing"
"github.com/pulumi/pulumi/sdk/v3/go/auto"
"github.com/pulumi/pulumi/sdk/v3/go/auto/optdestroy"
"github.com/pulumi/pulumi/sdk/v3/go/auto/optup"
)
func TestInfrastructure(t *testing.T) {
ctx := context.Background()
// Create a temporary stack for testing
stackName := fmt.Sprintf("test-%d", time.Now().Unix())
s, err := auto.NewStackLocalSource(ctx, stackName, "./infrastructure")
if err != nil {
t.Fatal(err)
}
// Configure the stack
s.SetConfig(ctx, "aws:region", auto.ConfigValue{Value: "us-east-1"})
// Deploy
_, err = s.Up(ctx, optup.ProgressStreams(os.Stdout))
if err != nil {
t.Fatal(err)
}
// Clean up on test completion
defer s.Destroy(ctx, optdestroy.ProgressStreams(os.Stdout))
// Get outputs
outputs, err := s.Outputs(ctx)
if err != nil {
t.Fatal(err)
}
// Verify expected outputs exist
bucketName, ok := outputs["bucketName"]
if !ok {
t.Fatal("Expected output 'bucketName' not found")
}
// Test that the bucket actually exists
sess := session.Must(session.NewSession())
svc := s3.New(sess)
_, err = svc.HeadBucket(&s3.HeadBucketInput{
Bucket: aws.String(bucketName.Value.(string)),
})
if err != nil {
t.Fatalf("Bucket %s does not exist: %v", bucketName.Value, err)
}
}CrossGuard Policy Basics
CrossGuard policies are written as a separate Pulumi program (a Policy Pack) that evaluates resources before they're deployed:
mkdir my-policy-pack && cd my-policy-pack
pulumi policy new aws-typescriptmy-policy-pack/
├── index.ts
├── package.json
└── PulumiPolicy.yaml// index.ts - Basic policy pack
import { PolicyPack, ReportViolation, ResourceValidationPolicy, validateResourceOfType } from "@pulumi/policy";
import * as aws from "@pulumi/aws";
const policies: ResourceValidationPolicy[] = [
{
name: "s3-bucket-versioning-required",
description: "S3 buckets must have versioning enabled",
validateResource: validateResourceOfType(aws.s3.Bucket, (bucket, args, reportViolation) => {
if (!bucket.versioning || !bucket.versioning.enabled) {
reportViolation("S3 bucket must have versioning enabled.");
}
}),
},
{
name: "s3-no-public-access",
description: "S3 buckets must block all public access",
validateResource: validateResourceOfType(aws.s3.BucketPublicAccessBlock, (block, args, reportViolation) => {
if (!block.blockPublicAcls || !block.blockPublicPolicy ||
!block.ignorePublicAcls || !block.restrictPublicBuckets) {
reportViolation("S3 bucket must block all public access.");
}
}),
},
{
name: "ec2-no-public-ssh",
description: "Security groups must not allow public SSH access",
validateResource: validateResourceOfType(aws.ec2.SecurityGroup, (sg, args, reportViolation) => {
if (sg.ingress) {
for (const rule of sg.ingress) {
if (rule.fromPort <= 22 && rule.toPort >= 22) {
if (rule.cidrBlocks?.includes("0.0.0.0/0") ||
rule.ipv6CidrBlocks?.includes("::/0")) {
reportViolation("Security groups must not allow SSH (port 22) from 0.0.0.0/0.");
}
}
}
}
}),
},
];
new PolicyPack("aws-security-policies", {
policies,
enforcementLevel: "mandatory", // "advisory" just warns, "mandatory" blocks
});Testing CrossGuard Policies
Pulumi provides a policy testing framework that lets you test policies against mock resources without deploying anything:
// index.test.ts
import { PolicyPackConfig } from "@pulumi/policy";
import { runResourceTests } from "@pulumi/policy/testing";
import { s3BucketVersioningRequired, s3NoPublicAccess } from "./policies";
describe("S3 bucket versioning policy", () => {
test("passes when versioning is enabled", async () => {
const result = await runResourceTests({
policyPackPath: ".",
policyName: "s3-bucket-versioning-required",
resources: [
{
type: "aws:s3/bucket:Bucket",
props: {
versioning: {
enabled: true,
},
},
},
],
});
expect(result.length).toBe(0); // No violations
});
test("reports violation when versioning is disabled", async () => {
const result = await runResourceTests({
policyPackPath: ".",
policyName: "s3-bucket-versioning-required",
resources: [
{
type: "aws:s3/bucket:Bucket",
props: {
versioning: {
enabled: false,
},
},
},
],
});
expect(result.length).toBe(1);
expect(result[0].message).toContain("versioning");
});
test("reports violation when versioning is not set", async () => {
const result = await runResourceTests({
policyPackPath: ".",
policyName: "s3-bucket-versioning-required",
resources: [
{
type: "aws:s3/bucket:Bucket",
props: {},
},
],
});
expect(result.length).toBe(1);
});
});
describe("EC2 security group SSH policy", () => {
test("passes when SSH is restricted to internal IP", async () => {
const result = await runResourceTests({
policyPackPath: ".",
policyName: "ec2-no-public-ssh",
resources: [
{
type: "aws:ec2/securityGroup:SecurityGroup",
props: {
ingress: [
{
fromPort: 22,
toPort: 22,
protocol: "tcp",
cidrBlocks: ["10.0.0.0/8"], // Internal only
},
],
},
},
],
});
expect(result.length).toBe(0);
});
test("reports violation when SSH is open to internet", async () => {
const result = await runResourceTests({
policyPackPath: ".",
policyName: "ec2-no-public-ssh",
resources: [
{
type: "aws:ec2/securityGroup:SecurityGroup",
props: {
ingress: [
{
fromPort: 22,
toPort: 22,
protocol: "tcp",
cidrBlocks: ["0.0.0.0/0"],
},
],
},
},
],
});
expect(result.length).toBe(1);
});
});Stack Policies (Organization-Wide Enforcement)
CrossGuard policies can be applied to Pulumi stacks through the Pulumi Cloud console, enforcing compliance across all deployments in an organization:
# Publish a policy pack to Pulumi Cloud
cd my-policy-pack
pulumi policy publish
# Apply to a specific stack
pulumi policy enable my-org/aws-security-policies latest --stack my-org/my-project/production
# Apply to all stacks in an organization
pulumi policy enable my-org/aws-security-policies latest --all-stacks
# Run a preview to see policy violations without deploying
cd my-infrastructure
pulumi preview --policy-pack ../my-policy-packWhen running pulumi up, any mandatory policy violations prevent the deployment from completing.
CI/CD Integration
# GitHub Actions workflow
test-and-validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
run: |
cd infrastructure && npm ci
cd ../policy-pack && npm ci
- name: Run unit tests
run: cd infrastructure && npm test
- name: Run policy tests
run: cd policy-pack && npm test
- name: Preview with policies
run: |
cd infrastructure
pulumi preview \
--policy-pack ../policy-pack \
--stack dev
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 }}# GitLab CI
pulumi-test:
stage: test
image: node:20
script:
- cd infrastructure && npm ci && npm test
- cd policy-pack && npm ci && npm test
pulumi-preview:
stage: validate
image: node:20
script:
- curl -fsSL https://get.pulumi.com | sh
- export PATH=$PATH:$HOME/.pulumi/bin
- cd infrastructure && npm ci
- cd policy-pack && npm ci
- cd infrastructure && pulumi preview --policy-pack ../policy-pack --stack dev
variables:
PULUMI_ACCESS_TOKEN: $PULUMI_ACCESS_TOKEN
only:
- merge_requestsCompliance Reporting
CrossGuard integrates with Pulumi's audit and compliance features. Policy violations are logged with:
- Which resource violated which policy
- The specific violation message
- The stack and operation that triggered the policy evaluation
For compliance auditing, the Pulumi Cloud console provides a policy violations history — useful for showing auditors that infrastructure deployments are subject to automated compliance checks.
Policy tests running in CI, combined with mandatory CrossGuard policies in the deployment pipeline, create a complete compliance-as-code workflow: policies are version-controlled, tested, and enforced automatically — with no manual review required to catch common misconfigurations.