Pulumi — Practical
Pulumi — Practical patterns
Section titled “Pulumi — Practical patterns”Project layout
Section titled “Project layout”infra/ Pulumi.yaml Pulumi.dev.yaml # stack config Pulumi.prod.yaml package.json index.ts components/ web-app.ts db.tsPulumi.yaml
Section titled “Pulumi.yaml”name: platformruntime: nodejsdescription: Platform infraconfig: aws:region: eu-west-1Pulumi..yaml
Section titled “Pulumi..yaml”config: aws:region: eu-west-1 platform:env: prod platform:db_size: db.r6g.xlarge platform:db_password: secure: AAA...Reusable component
Section titled “Reusable component”import * as pulumi from "@pulumi/pulumi";import * as aws from "@pulumi/aws";
interface Args { image: pulumi.Input<string>; vpcId: pulumi.Input<string>; subnets: pulumi.Input<string[]>;}
export class WebApp extends pulumi.ComponentResource { url: pulumi.Output<string>; constructor(name: string, args: Args, opts?: pulumi.ComponentResourceOptions) { super("custom:WebApp", name, {}, opts);
const sg = new aws.ec2.SecurityGroup(`${name}-sg`, { vpcId: args.vpcId, ingress: [{ protocol: "tcp", fromPort: 80, toPort: 80, cidrBlocks: ["0.0.0.0/0"] }], }, { parent: this });
const lb = new aws.lb.LoadBalancer(`${name}-lb`, { loadBalancerType: "application", subnets: args.subnets, securityGroups: [sg.id], }, { parent: this });
// ... TG, listener, ECS service, etc.
this.url = pulumi.interpolate`http://${lb.dnsName}`; this.registerOutputs({ url: this.url }); }}Using components
Section titled “Using components”import * as pulumi from "@pulumi/pulumi";import { WebApp } from "./components/web-app";
const cfg = new pulumi.Config();const network = new pulumi.StackReference("acme/network/prod");
const app = new WebApp("api", { image: cfg.require("image"), vpcId: network.requireOutput("vpcId"), subnets: network.requireOutput("publicSubnets") as any,});
export const apiUrl = app.url;Working with Outputs
Section titled “Working with Outputs”// interpolation (preferred for strings)const url = pulumi.interpolate`https://${bucket.bucketDomainName}/${key}`;
// apply for transformationconst upper = bucket.arn.apply(arn => arn.toUpperCase());
// combine multipleconst combined = pulumi.all([bucket.arn, queue.arn]) .apply(([b, q]) => ({ bucket: b, queue: q }));Secrets
Section titled “Secrets”const dbPassword = cfg.requireSecret("dbPassword");
const db = new aws.rds.Instance("db", { // dbPassword stays secret in state password: dbPassword, ...});
// or wrap explicitlyconst apiKey = pulumi.secret("super-key");pulumi config set --secret dbPassword 'sUp3r$ecret'Stack references (cross-project)
Section titled “Stack references (cross-project)”const platform = new pulumi.StackReference("acme/platform/prod");const vpcId = platform.requireOutput("vpcId");Import existing
Section titled “Import existing”# discover & importpulumi import aws:s3/bucket:Bucket logs my-existing-bucket
# with options to generate codepulumi import aws:s3/bucket:Bucket logs my-existing-bucket \ --generate-code --out generated.tsUnit testing
Section titled “Unit testing”import * as pulumi from "@pulumi/pulumi";pulumi.runtime.setMocks({ newResource: (args) => ({ id: `${args.name}_id`, state: args.inputs }), call: (args) => args.inputs,});
// __tests__/bucket.test.tsimport { describe, it, expect } from "vitest";import * as infra from "../src";
describe("infra", () => { it("bucket has encryption", async () => { const enc = await new Promise(res => (infra.bucket.serverSideEncryptionConfiguration as any).apply(res)); expect(enc.rule.applyServerSideEncryptionByDefault.sseAlgorithm).toBe("AES256"); });});Integration testing (ephemeral stack)
Section titled “Integration testing (ephemeral stack)”import * as automation from "@pulumi/pulumi/automation";
const stack = await automation.LocalWorkspace.createOrSelectStack({ stackName: "test-" + Date.now(), workDir: ".",});await stack.setConfig("aws:region", { value: "eu-west-1" });const up = await stack.up({ onOutput: console.log });expect(up.outputs.bucketName.value).toContain("logs-");await stack.destroy();CI (GitHub Actions)
Section titled “CI (GitHub Actions)”- uses: aws-actions/configure-aws-credentials@v4 with: role-to-assume: ${{ secrets.AWS_ROLE }} aws-region: eu-west-1- uses: pulumi/actions@v5 with: command: preview stack-name: org/proj/dev cloud-url: https://app.pulumi.com env: PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_TOKEN }}- uses: pulumi/actions@v5 if: github.ref == 'refs/heads/main' with: { command: up, stack-name: org/proj/dev }Self-hosted backend
Section titled “Self-hosted backend”pulumi login s3://my-state-bucket?region=eu-west-1# secret encryption with KMSpulumi stack init prod --secrets-provider="awskms://alias/pulumi"Useful patterns
Section titled “Useful patterns”- Use ComponentResource for any pattern repeated >2x.
- Tag everything via a default in
Pulumi.<stack>.yaml+ a wrapper. - Output every important id/url/arn → consumed by other stacks via
StackReference. - Run
pulumi preview --diffin PR comment. - Keep one stack per environment, not one giant stack with branches.
- Pulumi Cloud — managed state + audit + RBAC.
- CrossGuard — policy as code.
- Automation API — programmatic stack ops.
- Pulumi ESC — secret/config service.