be stored remotely with locking mechanisms to prevent race conditions and corruption.
3. Policy as Code: Enforce guardrails (cost, security, compliance) using policy frameworks before resources are provisioned.
4. CI/CD Integration: IaC changes must go through the same pipeline as application code: linting, testing, previewing, and applying.
Step-by-Step Implementation
1. Project Structure
Adopt a structure that separates components, stacks, and tests.
infrastructure/
βββ components/ # Reusable infrastructure components
β βββ network/
β β βββ index.ts
β β βββ vpc.ts
β βββ database/
β βββ postgres.ts
βββ stacks/ # Environment-specific deployments
β βββ dev/
β β βββ index.ts
β βββ prod/
β βββ index.ts
βββ tests/ # Unit and integration tests
β βββ network.test.ts
βββ Pulumi.yaml # Global config
βββ package.json
2. Creating a Reusable Component
Components encapsulate logic and expose a clean interface. This reduces duplication and centralizes updates.
// components/network/vpc.ts
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
export interface VpcArgs {
cidrBlock: string;
enableDnsHostnames: boolean;
tags?: pulumi.Input<{ [key: string]: string }>;
}
export class Vpc extends pulumi.ComponentResource {
public readonly id: pulumi.Output<string>;
public readonly publicSubnetIds: pulumi.Output<string[]>;
constructor(name: string, args: VpcArgs, opts?: pulumi.ComponentResourceOptions) {
super("my:network:Vpc", name, {}, opts);
const vpc = new aws.ec2.Vpc(`${name}-vpc`, {
cidrBlock: args.cidrBlock,
enableDnsHostnames: args.enableDnsHostnames,
tags: { ...args.tags, Name: name },
}, { parent: this });
// Create subnets based on VPC
const subnets = args.cidrBlock.split('.').slice(0, 3).join('.') + '.0/24';
const publicSubnet = new aws.ec2.Subnet(`${name}-public`, {
vpcId: vpc.id,
cidrBlock: subnets,
mapPublicIpOnLaunch: true,
tags: { Name: `${name}-public` },
}, { parent: this });
this.id = vpc.id;
this.publicSubnetIds = pulumi.output([publicSubnet.id]);
this.registerOutputs({
id: this.id,
publicSubnetIds: this.publicSubnetIds,
});
}
}
3. Implementing Tests
Infrastructure code must be tested. Use Pulumi's testing library to validate resource properties without provisioning.
// tests/network.test.ts
import * as pulumi from "@pulumi/pulumi";
import { Vpc } from "../components/network/vpc";
pulumi.runtime.setMocks({
newResource: function(args: pulumi.runtime.MockResourceArgs): { id: string, state: any } {
return {
id: args.inputs.name + "_id",
state: args.inputs,
};
},
call: function(args: pulumi.runtime.MockCallArgs) {
return args.inputs;
},
});
describe("Vpc Component", () => {
it("should create VPC with correct CIDR", async () => {
const vpc = new Vpc("test-vpc", {
cidrBlock: "10.0.0.0/16",
enableDnsHostnames: true,
});
// Assertions run against the mock state
const state = await vpc.id.promise();
// In a real test, you would inspect the mock resources created.
// Pulumi testing allows deep inspection of the resource graph.
console.log("VPC ID resolved:", state);
});
});
4. Stack Configuration and Secrets
Never hardcode values. Use configuration and secret management.
// stacks/prod/index.ts
import * as pulumi from "@pulumi/pulumi";
import { Vpc } from "../../components/network/vpc";
const config = new pulumi.Config();
const env = config.require("env");
const dbPassword = config.requireSecret("dbPassword"); // Encrypted at rest
const vpc = new Vpc(`${env}-network`, {
cidrBlock: config.require("vpcCidr"),
enableDnsHostnames: true,
});
// Pass secrets securely to resources
// const db = new Database("prod-db", { password: dbPassword, vpcId: vpc.id });
export const vpcId = vpc.id;
5. CI/CD Pipeline Integration
Automate the workflow. The pipeline should run tests, check policies, and preview changes before applying.
# .github/workflows/infra-deploy.yml
name: Infrastructure Deploy
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: '18'
- run: npm ci
- run: npm test
- run: npm run lint
preview:
needs: validate
runs-on: ubuntu-latest
environment: production
steps:
- uses: actions/checkout@v3
- uses: pulumi/actions@v4
with:
command: preview
stack-name: prod
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_TOKEN }}
AWS_REGION: us-east-1
Pitfall Guide
Avoid these common mistakes to ensure infrastructure stability and security.
-
Storing Secrets in Plain Text
- Mistake: Hardcoding passwords or API keys in code or configuration files.
- Risk: Secrets leak via version control, logs, or state files.
- Best Practice: Use cloud-native secret managers (AWS Secrets Manager, HashiCorp Vault) and reference them via secure configuration inputs. Ensure state files are encrypted.
-
Ignoring State Locking
- Mistake: Using local state or remote backends without locking.
- Risk: Concurrent operations corrupt state, leading to resource duplication or deletion.
- Best Practice: Always use remote backends with locking support (e.g., S3 with DynamoDB for Terraform, Pulumi Cloud/Service).
-
Monolithic Stacks ("God Stacks")
- Mistake: Defining all resources for an environment in a single file or stack.
- Risk: Slow plan times, high blast radius for errors, difficult to test.
- Best Practice: Decompose into logical stacks (e.g.,
network, database, application). Use stack references to share outputs between stacks.
-
Manual Drift
- Mistake: Making changes via console or CLI to "fix" issues quickly.
- Risk: Drift causes
pulumi up or terraform apply to fail or revert changes unexpectedly.
- Best Practice: Enforce "No Console Access" policies. Implement automated drift detection in CI/CD to alert on deviations.
-
Lack of Idempotency Awareness
- Mistake: Writing code that assumes resources don't exist or fails on updates.
- Risk: Operations are not repeatable; upgrades fail.
- Best Practice: Design components to handle updates gracefully. Use
retainOnDelete for critical data resources. Test update scenarios, not just creation.
-
Hardcoding Resource Limits and Tags
- Mistake: Embedding values like instance types, CIDR ranges, or tag values directly in components.
- Risk: Components are not reusable across environments.
- Best Practice: Parameterize all variable inputs. Use configuration files for environment-specific values.
-
Skipping Policy Enforcement
- Mistake: Relying solely on code reviews for security and compliance.
- Risk: Human error allows non-compliant resources (e.g., public S3 buckets) to be deployed.
- Best Practice: Integrate Policy as Code (OPA, Sentinel, Pulumi CrossGuard) to block violations automatically during preview.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Multi-Account Enterprise | Stacks per account/env with centralized policy | Isolation, least privilege, auditability | Low (management overhead) |
| Rapid Prototyping | Single stack, local state, minimal components | Speed of iteration, low boilerplate | None |
| Compliance Heavy (HIPAA/SOC2) | Modular components + Policy as Code + Audit logs | Automated enforcement, traceability, reporting | Medium (setup complexity) |
| High Availability Systems | Component-based with state separation per AZ/Region | Fault isolation, independent scaling/recovery | Low |
| Cost Optimization | Policy checks + Tagging enforcement + Review | Prevents over-provisioning, enables chargeback | Low |
Configuration Template
Pulumi.yaml (Global Config)
config:
aws:region: us-east-1
env: production
vpcCidr: 10.0.0.0/16
instanceType: t3.medium
tags:
ManagedBy: "pulumi"
Team: "platform"
CostCenter: "engineering"
Directory Structure Template
project/
βββ .github/workflows/
β βββ infra-lint.yml
β βββ infra-deploy.yml
βββ components/
β βββ README.md # Documentation for components
β βββ network/
β βββ compute/
βββ stacks/
β βββ dev/
β βββ staging/
β βββ prod/
βββ tests/
β βββ unit/
β βββ integration/
βββ policies/
β βββ security.rego # OPA policies
β βββ cost.rego
βββ Pulumi.yaml
βββ Pulumi.dev.yaml
βββ Pulumi.prod.yaml
βββ package.json
Quick Start Guide
-
Initialize Project:
Run pulumi new aws-typescript to scaffold a project. Configure AWS credentials via environment variables or IAM roles.
-
Configure Remote State:
Login to Pulumi Service: pulumi login. Create a stack: pulumi stack init dev. This automatically sets up remote state with locking.
-
Create First Component:
Create components/vpc.ts using the template in Core Solution. Import and instantiate it in index.ts.
-
Preview and Deploy:
Run pulumi preview to verify changes. Run pulumi up to provision resources. Verify resources in the cloud console.
-
Add Tests and CI:
Install testing dependencies: npm install --save-dev @pulumi/pulumi. Add a test file. Configure a GitHub Action to run npm test and pulumi preview on pull requests.
By adhering to these practices, engineering teams transform infrastructure from a source of risk into a reliable, scalable, and secure asset that accelerates delivery.