infra-iac-pulumi
TypeScript-native Infrastructure as Code with Pulumi
Install
npx skills add https://github.com/agents-inc/skills/tree/main/dist/plugins/infra-iac-pulumi/skills/infra-iac-pulumi
claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install agents-inc-skills@llmmart
git clone https://github.com/agents-inc/skills.git
The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole agents-inc/skills collection as a plugin from our marketplace. Git is the plain clone.
Skill manifest
Pulumi Infrastructure as Code
Quick Guide: Define cloud infrastructure in TypeScript with full type safety. Use
ComponentResourceto encapsulate reusable infrastructure patterns. Pass{ parent: this }to all child resources inside components. Usepulumi.interpolatefor string building with Outputs (not string concatenation). Never create resources inside.apply(). UseConfig.requireSecret()for sensitive values. Prefertransformsover deprecatedtransformations. Always callthis.registerOutputs()at the end of component constructors.
<critical_requirements>
CRITICAL: Before Using This Skill
All code must follow project conventions in CLAUDE.md (kebab-case, named exports, import ordering,
import type, named constants)
(You MUST pass { parent: this } to ALL child resources inside a ComponentResource -- omitting it breaks the resource tree and state tracking)
(You MUST use pulumi.interpolate for string building with Outputs -- string concatenation silently produces [object Object])
(You MUST NEVER create resources inside .apply() -- they will not appear in pulumi preview and cause ordering issues)
(You MUST use Config.requireSecret() for sensitive values -- Config.require() stores values as plaintext in state)
(You MUST call this.registerOutputs() at the end of every ComponentResource constructor -- omitting it prevents output tracking)
</critical_requirements>
Detailed Resources:
- examples/core.md - Resource definitions, component resources, naming, Outputs, config/secrets
- examples/advanced.md - Stack references, transforms, dynamic providers, Automation API, policy packs
- reference.md - Decision frameworks, resource options table, API reference, CLI commands
Auto-detection: Pulumi, @pulumi/pulumi, @pulumi/aws, @pulumi/gcp, @pulumi/azure, @pulumi/kubernetes, pulumi.ComponentResource, pulumi.CustomResource, pulumi.Output, pulumi.Config, pulumi.interpolate, pulumi.all, registerOutputs, StackReference, ComponentResourceOptions, CustomResourceOptions, dynamic.Resource, dynamic.ResourceProvider, LocalWorkspace, InlineProgramArgs, Automation API, CrossGuard, PolicyPack
When to use:
- Defining cloud infrastructure in TypeScript with type-safe resource APIs
- Creating reusable infrastructure components with
ComponentResource - Managing multi-stack architectures with stack references
- Handling secrets and environment-specific configuration
- Building self-service infrastructure platforms with the Automation API
- Writing compliance policies with CrossGuard policy packs
When NOT to use:
- One-off shell scripts that create a single resource (use the cloud CLI directly)
- Projects where the team has no TypeScript experience (consider other IaC language options)
Key patterns covered:
- Resource definitions with typed inputs and auto-naming
- ComponentResource encapsulation (parent, naming, registerOutputs)
- Outputs:
apply,all,interpolate, and lifting - Config and secrets management (
Config.require,Config.requireSecret,pulumi.secret) - Stack references for cross-stack data sharing
- Resource options (
dependsOn,protect,aliases,ignoreChanges,transforms) - Dynamic providers for custom CRUD resources
- Automation API for programmatic stack management
- CrossGuard policy packs for compliance enforcement
<red_flags>
RED FLAGS
High Priority:
- Creating resources inside
.apply()-- They won't appear inpulumi preview, cause ordering issues, and break the dependency graph. Pass Outputs directly as resource inputs. - Missing
{ parent: this }in ComponentResource children -- Resources appear at the root of the state tree, breaking logical grouping and component delete cascading. - String concatenation with Outputs --
"https://" + bucket.idproduces[object Object]. Usepulumi.interpolateinstead. - Using
Config.require()for passwords/keys -- Stores the value as plaintext in state. UseConfig.requireSecret()to encrypt. - Forgetting
this.registerOutputs()-- Component outputs won't be tracked properly in state or available via stack references. - Changing logical names without aliases -- Pulumi deletes and recreates the resource. Use
aliases: [{ name: "old-name" }]to rename safely.
Medium Priority:
- Relying on default providers in multi-region setups -- Use explicit providers per region. Set
pulumi:disable-default-providersin config to enforce. - Starting with functions instead of ComponentResource -- Migrating later requires aliases or resource recreation. Use components from the start.
- Not using
dependsOnfor non-obvious dependencies -- Pulumi infers dependencies from Input/Output wiring, but side effects (IAM propagation, DNS) need explicit ordering. - Using deprecated
transformations-- Usetransformsinstead.transformsalso support modifying child resources of packaged components. - Hardcoding region/account in resource args -- Use
pulumi.Configand providers for environment-specific values.
Gotchas & Edge Cases:
- Auto-naming appends a random suffix -- never rely on exact physical resource names in external systems
- Pulumi does not refresh state by default -- use
pulumi refreshto detect drift pulumi.secret()wraps a value so it's encrypted in state -- any derived Output is automatically secret tooOutput.apply()runs duringpulumi up, not duringpreviewfor unknown values -- conditional logic based on unknown outputs may not evaluate during preview- Component type tokens must follow
pkg:module:Typeformat (e.g.,myinfra:network:Vpc) to avoid conflicts - Dynamic providers serialize the provider class -- closures over external state, functions, or DOM nodes will fail
protect: trueonly preventspulumi destroydeletion, not manual cloud console deletion- Stack names in
StackReferenceare fully qualified:org/project/stack
</red_flags>
<critical_reminders>
CRITICAL REMINDERS
All code must follow project conventions in CLAUDE.md
(You MUST pass { parent: this } to ALL child resources inside a ComponentResource -- omitting it breaks the resource tree and state tracking)
(You MUST use pulumi.interpolate for string building with Outputs -- string concatenation silently produces [object Object])
(You MUST NEVER create resources inside .apply() -- they will not appear in pulumi preview and cause ordering issues)
(You MUST use Config.requireSecret() for sensitive values -- Config.require() stores values as plaintext in state)
(You MUST call this.registerOutputs() at the end of every ComponentResource constructor -- omitting it prevents output tracking)
Failure to follow these rules will cause broken state tracking, silent data exposure, and unpredictable deployment behavior.
</critical_reminders>
Files (skills)
-
examples
-
advanced.md 13.3 KB
# Pulumi - Advanced Examples > Stack references, transforms, dynamic providers, Automation API, and policy packs. See [SKILL.md](../SKILL.md) for decision guidance and [core.md](core.md) for fundamental patterns. --- ## Pattern 1: Stack References ### Exporting Outputs (Source Stack) ```typescript // networking/index.ts import * as pulumi from "@pulumi/pulumi"; import * as aws from "@pulumi/aws"; const vpc = new aws.ec2.Vpc("main", { cidrBlock: "10.0.0.0/16" }); const publicSubnets = [ new aws.ec2.Subnet("public-1", { vpcId: vpc.id, cidrBlock: "10.0.1.0/24", availabilityZone: "us-east-1a", }), new aws.ec2.Subnet("public-2", { vpcId: vpc.id, cidrBlock: "10.0.2.0/24", availabilityZone: "us-east-1b", }), ]; // Export values for other stacks to consume export const vpcId = vpc.id; export const publicSubnetIds = publicSubnets.map((s) => s.id); ``` ### Consuming Outputs (Consumer Stack) ```typescript // application/index.ts import * as pulumi from "@pulumi/pulumi"; import * as aws from "@pulumi/aws"; const config = new pulumi.Config(); const org = config.require("org"); const stack = pulumi.getStack(); // Reference the networking stack const networkStack = new pulumi.StackReference(`${org}/networking/${stack}`); // getOutput returns Output<any> -- value is undefined if output doesn't exist const vpcId = networkStack.getOutput("vpcId"); const subnetIds = networkStack.getOutput("publicSubnetIds"); // requireOutput throws if output doesn't exist (safer for critical dependencies) const vpcIdRequired = networkStack.requireOutput("vpcId"); // Use in resource definitions const cluster = new aws.ecs.Cluster("app", {}); const service = new aws.ecs.Service("web", { cluster: cluster.arn, networkConfiguration: { subnets: subnetIds, assignPublicIp: true, }, desiredCount: 2, }); ``` **Key point:** Stack reference names are fully qualified: `org/project/stack`. Use config for the org name to avoid hardcoding. ### getOutputDetails (Typed Access) ```typescript // Returns plain value instead of Output -- no apply needed const details = await networkStack.getOutputDetails("vpcId"); if (details.value) { console.log(`VPC: ${details.value}`); // Plain string, not Output } if (details.secretValue) { console.log("This output is a secret"); } ``` --- ## Pattern 2: Transforms ### Resource-Level Transform (Tag All Children) ```typescript import * as pulumi from "@pulumi/pulumi"; const MANAGED_BY_TAG = "pulumi"; // Apply tags to all taggable child resources const vpc = new MyVpcComponent( "production", {}, { transforms: [ (args) => { if (isTaggable(args.type)) { return { props: { ...args.props, tags: { ...args.props["tags"], ManagedBy: MANAGED_BY_TAG }, }, opts: args.opts, }; } return undefined; // Return undefined to leave unmodified }, ], }, ); function isTaggable(type: string): boolean { // AWS resources generally support tags return type.startsWith("aws:"); } ``` ### Stack-Level Transform (Global Policy) ```typescript // Apply to ALL resources in the stack pulumi.runtime.registerResourceTransform((args) => { if (isTaggable(args.type)) { return { props: { ...args.props, tags: { ...args.props["tags"], Environment: pulumi.getStack(), Project: pulumi.getProject(), }, }, opts: args.opts, }; } return undefined; }); ``` ### Modify Resource Options via Transform ```typescript // Ignore tag drift on all resources in a component const vpc = new MyVpcComponent( "vpc", {}, { transforms: [ (args) => { if ( args.type === "aws:ec2/vpc:Vpc" || args.type === "aws:ec2/subnet:Subnet" ) { return { props: args.props, opts: pulumi.mergeOptions(args.opts, { ignoreChanges: ["tags"] }), }; } return undefined; }, ], }, ); ``` **Migration note:** `transforms` replaces the deprecated `transformations`. Key differences: `transforms` support modifying packaged component children (awsx, eks), support async callbacks, and do not pass a Resource object (use `args.type` instead). --- ## Pattern 3: Dynamic Providers Create custom resources with CRUD lifecycle for APIs not covered by native providers. ### Provider with Full CRUD ```typescript import * as pulumi from "@pulumi/pulumi"; interface WebhookInputs { url: string; events: string[]; secret: string; } interface WebhookOutputs extends WebhookInputs { webhookId: string; createdAt: string; } class WebhookProvider implements pulumi.dynamic.ResourceProvider { async create(inputs: WebhookInputs): Promise<pulumi.dynamic.CreateResult> { // Call external API to create webhook const response = await fetch("https://api.example.com/webhooks", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(inputs), }); const data = await response.json(); return { id: data.id, outs: { ...inputs, webhookId: data.id, createdAt: data.created_at }, }; } async read( id: string, props: WebhookOutputs, ): Promise<pulumi.dynamic.ReadResult> { const response = await fetch(`https://api.example.com/webhooks/${id}`); if (!response.ok) { // Return empty to signal resource was deleted externally return { id: "", outs: {} }; } const data = await response.json(); return { id, outs: { ...props, ...data } }; } async update( id: string, olds: WebhookOutputs, news: WebhookInputs, ): Promise<pulumi.dynamic.UpdateResult> { await fetch(`https://api.example.com/webhooks/${id}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(news), }); return { outs: { ...news, webhookId: id, createdAt: olds.createdAt } }; } async delete(id: string): Promise<void> { await fetch(`https://api.example.com/webhooks/${id}`, { method: "DELETE" }); } } // Resource class wrapping the provider export interface WebhookResourceInputs { url: pulumi.Input<string>; events: pulumi.Input<pulumi.Input<string>[]>; secret: pulumi.Input<string>; } export class Webhook extends pulumi.dynamic.Resource { declare readonly webhookId: pulumi.Output<string>; declare readonly createdAt: pulumi.Output<string>; constructor( name: string, args: WebhookResourceInputs, opts?: pulumi.CustomResourceOptions, ) { super( new WebhookProvider(), name, { webhookId: undefined, createdAt: undefined, ...args }, opts, ); } } // Usage const hook = new Webhook("deploy-hook", { url: "https://myapp.com/webhooks/deploy", events: ["push", "release"], secret: config.requireSecret("webhookSecret"), }); export const hookId = hook.webhookId; ``` **Key points:** - Output properties use `declare readonly` (not `public readonly`) - Pass `{ outputName: undefined, ...args }` to super to register output properties - Provider methods receive unwrapped plain values (not `Output<T>`) - The provider class is serialized -- avoid closures over external state --- ## Pattern 4: Automation API ### Inline Program (Self-Contained) ```typescript import { InlineProgramArgs, LocalWorkspace } from "@pulumi/pulumi/automation"; import * as pulumi from "@pulumi/pulumi"; import * as aws from "@pulumi/aws"; const STACK_NAME = "dev"; const PROJECT_NAME = "self-service-infra"; // Define infrastructure as a function const program = async () => { const bucket = new aws.s3.Bucket("managed-bucket", { versioning: { enabled: true }, }); return { bucketName: bucket.id, bucketArn: bucket.arn }; }; async function deploy() { const args: InlineProgramArgs = { stackName: STACK_NAME, projectName: PROJECT_NAME, program, }; // Create or select the stack const stack = await LocalWorkspace.createOrSelectStack(args); // Set config await stack.setConfig("aws:region", { value: "us-east-1" }); // Preview changes const previewResult = await stack.preview({ onOutput: console.log }); console.log(`Preview: ${previewResult.changeSummary}`); // Deploy const upResult = await stack.up({ onOutput: console.log }); console.log(`Bucket: ${upResult.outputs.bucketName.value}`); console.log(`Secret? ${upResult.outputs.bucketName.secret}`); // Get outputs const outputs = await stack.outputs(); console.log(`Bucket ARN: ${outputs.bucketArn.value}`); } ``` ### Local Program (Existing Pulumi Project) ```typescript import { LocalWorkspace } from "@pulumi/pulumi/automation"; async function deployExisting() { const stack = await LocalWorkspace.createOrSelectStack({ stackName: "dev", workDir: "/path/to/existing/pulumi/project", }); // Refresh state from cloud provider await stack.refresh({ onOutput: console.log }); // Deploy const result = await stack.up({ onOutput: console.log }); return result.outputs; } ``` ### Destroy Stack ```typescript async function teardown() { const stack = await LocalWorkspace.selectStack({ stackName: "dev", projectName: "self-service-infra", program: async () => ({}), }); await stack.destroy({ onOutput: console.log }); await stack.workspace.removeStack("dev"); } ``` **Key point:** The Automation API requires the Pulumi CLI to be installed and on PATH, even though you're not calling it directly. The API uses the CLI's engine under the hood. --- ## Pattern 5: Policy Packs (CrossGuard) ### Resource Validation Policy ```typescript import * as policy from "@pulumi/policy"; const REQUIRED_TAGS = ["Environment", "Team", "ManagedBy"]; new policy.PolicyPack("compliance", { policies: [ { name: "required-tags", description: "All resources must have required tags", enforcementLevel: "mandatory", validateResource: policy.validateResourceOfType( // Use the provider's resource type "aws.s3.Bucket", (bucket, args, reportViolation) => { const tags = bucket.tags ?? {}; for (const tag of REQUIRED_TAGS) { if (!(tag in tags)) { reportViolation(`Missing required tag: ${tag}`); } } }, ), }, { name: "no-public-buckets", description: "S3 buckets must not have public access", enforcementLevel: "mandatory", validateResource: policy.validateResourceOfType( "aws.s3.Bucket", (bucket, args, reportViolation) => { if ( bucket.acl === "public-read" || bucket.acl === "public-read-write" ) { reportViolation("S3 buckets must not have public ACLs"); } }, ), }, ], }); ``` ### Stack Validation Policy ```typescript new policy.PolicyPack("stack-policies", { policies: [ { name: "no-unencrypted-secrets", description: "Stack must not have plaintext secrets in config", enforcementLevel: "mandatory", validateStack: (args, reportViolation) => { for (const resource of args.resources) { // Check for resources that should use encryption if (resource.type === "aws:rds/instance:Instance") { const props = resource.props as Record<string, unknown>; if (!props.storageEncrypted) { reportViolation( `RDS instance ${resource.name} must have storage encryption enabled`, ); } } } }, }, ], }); ``` **Running policies:** ```bash # Run policy pack against a stack pulumi preview --policy-pack ./policy # Publish to Pulumi Cloud for organization-wide enforcement pulumi policy publish ./policy ``` **Enforcement levels:** `advisory` (warning only) or `mandatory` (blocks deployment). Use `advisory` during rollout, then switch to `mandatory`. --- ## Pattern 6: Aliases (Safe Refactoring) ### Rename a Resource ```typescript // Before: resource was named "my-bucket" // After: rename to "data-bucket" without destroying and recreating const bucket = new aws.s3.Bucket( "data-bucket", { /* ... */ }, { aliases: [{ name: "my-bucket" }], }, ); ``` ### Move Resource Into a Component ```typescript // Resource was at the root, now moving into a component class StorageComponent extends pulumi.ComponentResource { constructor(name: string, opts?: pulumi.ComponentResourceOptions) { super("myinfra:storage:StorageComponent", name, {}, opts); // Alias tells Pulumi this was previously at root (no parent) const bucket = new aws.s3.Bucket( "data-bucket", {}, { parent: this, aliases: [{ parent: pulumi.rootStackResource }], }, ); this.registerOutputs({}); } } ``` ### Rename a Component Type ```typescript // Changed the component type token from "pkg:old:Name" to "pkg:new:Name" class MyComponent extends pulumi.ComponentResource { constructor(name: string, opts?: pulumi.ComponentResourceOptions) { super( "myinfra:v2:MyComponent", name, {}, { ...opts, aliases: [{ type: "myinfra:v1:MyComponent" }], }, ); this.registerOutputs({}); } } ``` **Key point:** Always add aliases when renaming resources, changing parent relationships, or changing type tokens. Without aliases, Pulumi deletes the old resource and creates a new one. -
core.md 10.6 KB
# Pulumi - Core Examples > Resource definitions, component resources, naming, Outputs, and config/secrets. See [SKILL.md](../SKILL.md) for decision guidance and [reference.md](../reference.md) for resource options table. **Additional Examples:** - [advanced.md](advanced.md) - Stack references, transforms, dynamic providers, Automation API --- ## Pattern 1: Resource Definitions ### Basic Resource with Options ```typescript import * as pulumi from "@pulumi/pulumi"; import * as aws from "@pulumi/aws"; const BUCKET_EXPIRY_DAYS = 90; // Good: explicit logical name, typed args, resource options const bucket = new aws.s3.Bucket( "data-bucket", { versioning: { enabled: true }, lifecycleRules: [ { enabled: true, expiration: { days: BUCKET_EXPIRY_DAYS }, }, ], }, { protect: true }, ); export const bucketName = bucket.id; export const bucketArn = bucket.arn; ``` **Why good:** named constant for expiry, `protect: true` prevents accidental deletion, outputs exported for cross-stack use ```typescript // Bad: magic numbers, no protection on production resources const bucket = new aws.s3.Bucket("data-bucket", { lifecycleRules: [{ enabled: true, expiration: { days: 90 } }], }); ``` **Why bad:** magic number `90` is undocumented, no `protect` on a data resource, no exports for other stacks --- ### Auto-Naming and Physical Names ```typescript // Pulumi auto-appends a random suffix: "data-bucket" -> "data-bucket-a1b2c3d" // This prevents collisions and enables zero-downtime replacement. // Override auto-naming only when you must (shared external references): const bucket = new aws.s3.Bucket( "shared-assets", { bucket: `${pulumi.getProject()}-${pulumi.getStack()}-assets`, // Explicit physical name }, { deleteBeforeReplace: true }, ); // Required when naming explicitly (uniqueness constraint) ``` **Gotcha:** Explicit physical names make your project susceptible to naming collisions across stacks. Prefer auto-naming unless an external system needs a predictable name. ### Auto-Naming Configuration (Pulumi.yaml) ```yaml # Default: random suffix config: pulumi:autonaming: mode: default # Verbatim: use logical name as-is config: pulumi:autonaming: mode: verbatim # Custom pattern config: pulumi:autonaming: pattern: ${name}-${stack}-${hex(4)} ``` --- ### Explicit Provider (Multi-Region / Multi-Account) ```typescript const usEast = new aws.Provider("us-east", { region: "us-east-1" }); const euWest = new aws.Provider("eu-west", { region: "eu-west-1" }); const usTable = new aws.dynamodb.Table( "us-users", { /* ... */ }, { provider: usEast }, ); const euTable = new aws.dynamodb.Table( "eu-users", { /* ... */ }, { provider: euWest }, ); ``` **Key point:** Relying on the default provider causes gotchas in multi-region setups. Enforce explicit providers: ```yaml # Pulumi.yaml -- disable default providers config: pulumi:disable-default-providers: - aws ``` --- ## Pattern 2: ComponentResource Encapsulation ### Full Component Example ```typescript import * as pulumi from "@pulumi/pulumi"; import * as aws from "@pulumi/aws"; interface DatabaseArgs { engine: pulumi.Input<string>; instanceClass: pulumi.Input<string>; allocatedStorage: pulumi.Input<number>; masterPassword: pulumi.Input<string>; // Will be secret if passed from config.requireSecret } export class Database extends pulumi.ComponentResource { public readonly endpoint: pulumi.Output<string>; public readonly port: pulumi.Output<number>; constructor( name: string, args: DatabaseArgs, opts?: pulumi.ComponentResourceOptions, ) { // Type token format: "pkg:module:Type" super("myinfra:data:Database", name, args, opts); const subnetGroup = new aws.rds.SubnetGroup( `${name}-subnets`, { subnetIds: [ /* ... */ ], }, { parent: this }, ); // Always pass parent const securityGroup = new aws.ec2.SecurityGroup( `${name}-sg`, { vpcId: "vpc-xxx", ingress: [ { protocol: "tcp", fromPort: 5432, toPort: 5432, cidrBlocks: ["10.0.0.0/8"], }, ], }, { parent: this }, ); const db = new aws.rds.Instance( `${name}-instance`, { engine: args.engine, instanceClass: args.instanceClass, allocatedStorage: args.allocatedStorage, password: args.masterPassword, dbSubnetGroupName: subnetGroup.name, vpcSecurityGroupIds: [securityGroup.id], skipFinalSnapshot: false, }, { parent: this, protect: true }, ); // Protect the actual database this.endpoint = db.endpoint; this.port = db.port; this.registerOutputs({ endpoint: this.endpoint, port: this.port }); } } // Usage const config = new pulumi.Config(); const db = new Database("primary", { engine: "postgres", instanceClass: "db.t3.micro", allocatedStorage: 20, masterPassword: config.requireSecret("dbPassword"), }); export const dbEndpoint = db.endpoint; ``` **Why good:** `{ parent: this }` on all children, `protect: true` on the critical resource, `registerOutputs` at the end, type token follows convention, name prefixed to children, args typed with `pulumi.Input<T>` for flexibility --- ### Bad: Missing Parent, Missing registerOutputs ```typescript // Bad: this is how NOT to write a component class Database extends pulumi.ComponentResource { constructor( name: string, args: DatabaseArgs, opts?: pulumi.ComponentResourceOptions, ) { super("myinfra:data:Database", name, args, opts); // Missing { parent: this } -- resources appear at root of state tree const sg = new aws.ec2.SecurityGroup(`${name}-sg`, { /* ... */ }); const db = new aws.rds.Instance(`${name}-db`, { /* ... */ }); // Missing registerOutputs -- outputs not tracked in state } } ``` **Why bad:** without `parent`, child resources are detached from the component in the state tree (deleting the component won't cascade). Without `registerOutputs`, stack references and the Pulumi engine can't track component-level outputs. --- ## Pattern 3: Working with Outputs ### pulumi.interpolate (Preferred for Strings) ```typescript // Good: interpolate handles Output<string> transparently const connectionString = pulumi.interpolate`postgres://admin:${password}@${db.endpoint}:${db.port}/mydb`; // Good: interpolate works in resource args const record = new aws.route53.Record( "api-dns", { name: pulumi.interpolate`api.${zone.name}`, type: "CNAME", records: [lb.dnsName], ttl: 300, }, { parent: this }, ); ``` ```typescript // Bad: string concatenation with Outputs const url = "https://" + bucket.id; // Produces "https://[object Object]" ``` **Why bad:** `Output<string>` is not a string -- concatenation calls `.toString()` which returns `[object Object]` --- ### pulumi.all (Combine Multiple Outputs) ```typescript const HTTP_PORT = 80; const endpoint = pulumi .all([lb.dnsName, listener.port]) .apply(([dns, port]) => `http://${dns}:${port}`); // Also works for building complex objects from multiple outputs const dbConfig = pulumi .all([db.endpoint, db.port, db.dbName]) .apply(([host, port, name]) => ({ host, port, database: name, connectionString: `postgres://${host}:${port}/${name}`, })); ``` --- ### apply (Transform a Single Output) ```typescript // Good: transform an output value const bucketUrl = bucket.websiteEndpoint.apply( (endpoint) => `https://${endpoint}`, ); // Good: conditional logic on an output const displayName = instance.tags.apply((tags) => tags?.["Name"] ?? "unnamed"); ``` ```typescript // Bad: creating resources inside apply bucket.id.apply((id) => { // This resource won't appear in pulumi preview! new aws.s3.BucketPolicy("policy", { bucket: id /* ... */ }); }); ``` **Why bad:** resources inside `apply` are invisible to `pulumi preview` and cause ordering issues. Pass the Output directly as an input instead: ```typescript // Good: pass Output directly as input new aws.s3.BucketPolicy("policy", { bucket: bucket.id, // Output<string> accepted as Input<string> policy: bucket.arn.apply((arn) => JSON.stringify({ Statement: [ { Effect: "Allow", Action: ["s3:GetObject"], Resource: `${arn}/*` }, ], }), ), }); ``` --- ### pulumi.output (Wrap Plain Values) ```typescript // Convert a plain value to an Output (useful in functions that accept Input<T>) function buildUrl(host: pulumi.Input<string>): pulumi.Output<string> { return pulumi.output(host).apply((h) => `https://${h}`); } // Works with both plain strings and Output<string> const fromPlain = buildUrl("example.com"); const fromOutput = buildUrl(instance.publicDns); ``` --- ## Pattern 4: Config and Secrets ### Basic Config Access ```typescript const config = new pulumi.Config(); // Required values -- fail if missing const region = config.require("region"); const nodeCount = config.requireNumber("nodeCount"); const enableLogs = config.requireBoolean("enableLogs"); // Optional values -- return undefined if missing const customDomain = config.get("customDomain"); const maxRetries = config.getNumber("maxRetries"); // Secret values -- encrypted in state const dbPassword = config.requireSecret("dbPassword"); const apiKey = config.getSecret("apiKey"); ``` ### Setting Config from CLI ```bash # Plain values pulumi config set region us-east-1 pulumi config set nodeCount 3 # Secret values -- encrypted at rest pulumi config set --secret dbPassword hunter2 pulumi config set --secret apiKey sk_live_abc123 # Namespaced config (for providers or custom namespaces) pulumi config set aws:region us-east-1 pulumi config set myapp:featureFlag true ``` ### Namespaced Config ```typescript // Read provider-specific config const awsConfig = new pulumi.Config("aws"); const region = awsConfig.require("region"); // Custom namespace for your app const appConfig = new pulumi.Config("myapp"); const featureFlag = appConfig.getBoolean("featureFlag") ?? false; ``` ### Programmatic Secrets ```typescript // Mark a computed value as secret const token = pulumi.secret(generateToken()); // Any output derived from a secret is automatically secret const connectionString = pulumi.interpolate`postgres://admin:${dbPassword}@${db.endpoint}/mydb`; // connectionString is automatically secret because dbPassword is secret -- no need to re-mark // Explicitly wrap an output as secret const sensitiveOutput = pulumi.secret(pulumi.interpolate`key-${someValue}`); ``` **Gotcha:** `pulumi.secret()` only encrypts the value in Pulumi state. If you export it as a stack output, consumers see `[secret]` unless they use `--show-secrets`.
-
-
reference.md 7.2 KB
# Pulumi Quick Reference Decision frameworks, resource options, API reference tables, and CLI commands. --- ## Decision Framework ### When to Use ComponentResource vs Plain Function? ``` Are you grouping 2+ related resources? ├─ YES → Do they need to appear as a single unit in state/UI? │ ├─ YES → ComponentResource (shows as parent in `pulumi stack`) │ └─ NO → Plain function returning resources is fine └─ NO → Single resource? Just create it directly. ``` ### When to Use Explicit Providers? ``` Are you deploying to multiple regions or accounts? ├─ YES → Always use explicit providers │ └─ Set pulumi:disable-default-providers to enforce └─ NO → Default provider is fine for single-region projects ``` ### When to Use Stack References vs Passing Values? ``` Are the resources in different Pulumi projects? ├─ YES → Stack references (StackReference + getOutput/requireOutput) └─ NO → Are they in different stacks of the same project? ├─ YES → Stack references └─ NO → Pass values directly (same stack) ``` ### When to Use Dynamic Providers? ``` Does a native Pulumi provider exist for this service? ├─ YES → Use the native provider (better state tracking, preview) └─ NO → Is the resource lifecycle CRUD-based? ├─ YES → Dynamic provider (pulumi.dynamic.Resource) └─ NO → Is it a one-shot action? ├─ YES → Use a Command resource or local script └─ NO → Consider the Automation API ``` ### When to Use protect vs retainOnDelete? ``` Want to prevent accidental `pulumi destroy`? ├─ YES → protect: true (blocks deletion, must unprotect first) └─ NO → Want to keep the cloud resource when removing from Pulumi? ├─ YES → retainOnDelete: true (Pulumi forgets it, cloud keeps it) └─ NO → Default behavior (Pulumi deletes cloud resource) ``` --- ## Resource Options Reference | Option | Type | Purpose | | --------------------- | -------------------------- | ------------------------------------------------------------ | | `parent` | `Resource` | Set parent (establishes resource tree, cascading delete) | | `provider` | `ProviderResource` | Explicit provider for this resource | | `providers` | `Record<string, Provider>` | Provider map for child resources (components only) | | `dependsOn` | `Input<Resource[]>` | Explicit ordering beyond automatic dependency inference | | `protect` | `boolean` | Prevent accidental deletion (must unprotect first) | | `retainOnDelete` | `boolean` | Keep cloud resource when removed from Pulumi state | | `deleteBeforeReplace` | `boolean` | Delete old before creating new (for unique name constraints) | | `ignoreChanges` | `string[]` | Ignore drift on specific properties | | `aliases` | `Input<Alias[]>` | Old names/types/parents for safe renaming | | `replaceOnChanges` | `string[]` | Force replacement on specific property changes | | `import` | `string` | Import existing cloud resource into Pulumi state | | `transforms` | `ResourceTransform[]` | Modify child resource properties/options dynamically | | `customTimeouts` | `CustomTimeouts` | Override default create/update/delete timeouts | | `hooks` | `ResourceHooks` | Lifecycle callbacks (before/after create, update, delete) | --- ## Output Methods Reference | Method | Input | Output | Use When | | ------------------------- | ---------------- | ---------------- | ----------------------------------------------------- | | `pulumi.interpolate` | Template literal | `Output<string>` | Building strings from Outputs (preferred) | | `.apply(fn)` | `Output<T>` | `Output<U>` | Transforming a single Output value | | `pulumi.all([...])` | `Output<T>[]` | `Output<T[]>` | Combining multiple Outputs | | `pulumi.output(val)` | `T \| Output<T>` | `Output<T>` | Wrapping a plain value as an Output | | `pulumi.secret(val)` | `T \| Output<T>` | `Output<T>` | Marking a value as secret (encrypted in state) | | `.getOutput(name)` | `StackReference` | `Output<any>` | Reading a stack output (returns undefined if missing) | | `.requireOutput(name)` | `StackReference` | `Output<any>` | Reading a stack output (throws if missing) | | `.getOutputDetails(name)` | `StackReference` | `OutputDetails` | Reading a stack output as plain value | --- ## Config Methods Reference | Method | Returns | Behavior When Missing | | ---------------------------- | ----------------------------- | --------------------- | | `config.get(key)` | `string \| undefined` | Returns undefined | | `config.require(key)` | `string` | Throws error | | `config.getNumber(key)` | `number \| undefined` | Returns undefined | | `config.requireNumber(key)` | `number` | Throws error | | `config.getBoolean(key)` | `boolean \| undefined` | Returns undefined | | `config.requireBoolean(key)` | `boolean` | Throws error | | `config.getSecret(key)` | `Output<string> \| undefined` | Returns undefined | | `config.requireSecret(key)` | `Output<string>` | Throws error | --- ## Common CLI Commands ```bash # Stack lifecycle pulumi new typescript # Create new project pulumi stack init dev # Create new stack pulumi stack select prod # Switch stacks pulumi config set key value # Set config pulumi config set --secret key value # Set encrypted config # Deployment pulumi preview # Show planned changes pulumi up # Deploy changes pulumi up --yes # Deploy without confirmation pulumi destroy # Tear down all resources pulumi refresh # Sync state with cloud provider # Inspection pulumi stack # Show current stack info pulumi stack output # Show stack outputs pulumi stack output --json # JSON format outputs pulumi stack export # Export state as JSON # Resource management pulumi state unprotect <urn> # Remove protection pulumi state delete <urn> # Remove from state (does not delete cloud resource) pulumi import <type> <name> <id> # Import existing cloud resource # Policy pulumi preview --policy-pack ./policy # Run with policy pack pulumi policy publish ./policy # Publish to Pulumi Cloud ``` --- > For anti-patterns and common mistakes, see the RED FLAGS section in [SKILL.md](SKILL.md). -
SKILL.md 15.8 KB
--- name: infra-iac-pulumi description: TypeScript-native Infrastructure as Code with Pulumi --- # Pulumi Infrastructure as Code > **Quick Guide:** Define cloud infrastructure in TypeScript with full type safety. Use `ComponentResource` to encapsulate reusable infrastructure patterns. Pass `{ parent: this }` to all child resources inside components. Use `pulumi.interpolate` for string building with Outputs (not string concatenation). Never create resources inside `.apply()`. Use `Config.requireSecret()` for sensitive values. Prefer `transforms` over deprecated `transformations`. Always call `this.registerOutputs()` at the end of component constructors. --- <critical_requirements> ## CRITICAL: Before Using This Skill > **All code must follow project conventions in CLAUDE.md** (kebab-case, named exports, import ordering, `import type`, named constants) **(You MUST pass `{ parent: this }` to ALL child resources inside a ComponentResource -- omitting it breaks the resource tree and state tracking)** **(You MUST use `pulumi.interpolate` for string building with Outputs -- string concatenation silently produces `[object Object]`)** **(You MUST NEVER create resources inside `.apply()` -- they will not appear in `pulumi preview` and cause ordering issues)** **(You MUST use `Config.requireSecret()` for sensitive values -- `Config.require()` stores values as plaintext in state)** **(You MUST call `this.registerOutputs()` at the end of every ComponentResource constructor -- omitting it prevents output tracking)** </critical_requirements> --- **Detailed Resources:** - [examples/core.md](examples/core.md) - Resource definitions, component resources, naming, Outputs, config/secrets - [examples/advanced.md](examples/advanced.md) - Stack references, transforms, dynamic providers, Automation API, policy packs - [reference.md](reference.md) - Decision frameworks, resource options table, API reference, CLI commands --- **Auto-detection:** Pulumi, @pulumi/pulumi, @pulumi/aws, @pulumi/gcp, @pulumi/azure, @pulumi/kubernetes, pulumi.ComponentResource, pulumi.CustomResource, pulumi.Output, pulumi.Config, pulumi.interpolate, pulumi.all, registerOutputs, StackReference, ComponentResourceOptions, CustomResourceOptions, dynamic.Resource, dynamic.ResourceProvider, LocalWorkspace, InlineProgramArgs, Automation API, CrossGuard, PolicyPack **When to use:** - Defining cloud infrastructure in TypeScript with type-safe resource APIs - Creating reusable infrastructure components with `ComponentResource` - Managing multi-stack architectures with stack references - Handling secrets and environment-specific configuration - Building self-service infrastructure platforms with the Automation API - Writing compliance policies with CrossGuard policy packs **When NOT to use:** - One-off shell scripts that create a single resource (use the cloud CLI directly) - Projects where the team has no TypeScript experience (consider other IaC language options) **Key patterns covered:** - Resource definitions with typed inputs and auto-naming - ComponentResource encapsulation (parent, naming, registerOutputs) - Outputs: `apply`, `all`, `interpolate`, and lifting - Config and secrets management (`Config.require`, `Config.requireSecret`, `pulumi.secret`) - Stack references for cross-stack data sharing - Resource options (`dependsOn`, `protect`, `aliases`, `ignoreChanges`, `transforms`) - Dynamic providers for custom CRUD resources - Automation API for programmatic stack management - CrossGuard policy packs for compliance enforcement --- <philosophy> ## Philosophy Pulumi treats infrastructure as real code, not configuration files. TypeScript gives you type safety, IDE autocompletion, refactoring tools, and the full Node.js ecosystem. Resources are objects, dependencies are automatic, and reuse happens through functions and classes -- not a custom module language. **Core principles:** - **Resources are objects**: Every cloud resource is a TypeScript class instance with typed inputs and outputs - **Dependencies are automatic**: When you pass one resource's output as another's input, Pulumi infers the dependency graph - **Reuse through components**: `ComponentResource` encapsulates multiple resources into a single logical unit with its own inputs and outputs - **Outputs are promises**: `Output<T>` represents a value that may not be known until after deployment -- use `apply`, `all`, or `interpolate` to work with them, never unwrap manually - **State is managed**: Pulumi tracks every resource in state -- changing a logical name or moving a resource between files triggers a delete-and-recreate unless you use `aliases` </philosophy> --- <patterns> ## Core Patterns ### Pattern 1: Resource Definitions Every resource takes a logical name (used for state tracking), an args bag (typed inputs), and optional resource options. ```typescript const bucket = new aws.s3.Bucket( "data-bucket", { versioning: { enabled: true }, lifecycleRules: [ { enabled: true, expiration: { days: BUCKET_EXPIRY_DAYS } }, ], }, { protect: true }, ); // Prevent accidental deletion ``` **Key points:** Logical names must be unique per type within a stack. Pulumi auto-appends a random suffix to the physical name to prevent collisions. Use `protect: true` on critical resources. Export outputs for cross-stack consumption. See [examples/core.md](examples/core.md) for resource naming, auto-naming configuration, and provider options. --- ### Pattern 2: ComponentResource Encapsulation Wrap related resources in a `ComponentResource` to create reusable infrastructure units. ```typescript import * as pulumi from "@pulumi/pulumi"; import * as aws from "@pulumi/aws"; interface StaticSiteArgs { indexDocument?: string; errorDocument?: string; } class StaticSite extends pulumi.ComponentResource { public readonly bucketName: pulumi.Output<string>; public readonly websiteUrl: pulumi.Output<string>; constructor( name: string, args: StaticSiteArgs, opts?: pulumi.ComponentResourceOptions, ) { super("myinfra:web:StaticSite", name, args, opts); const bucket = new aws.s3.BucketV2(`${name}-bucket`, {}, { parent: this }); const website = new aws.s3.BucketWebsiteConfigurationV2( `${name}-website`, { bucket: bucket.id, indexDocument: { suffix: args.indexDocument ?? "index.html" }, errorDocument: { key: args.errorDocument ?? "error.html" }, }, { parent: this }, ); this.bucketName = bucket.id; this.websiteUrl = website.websiteEndpoint; this.registerOutputs({ bucketName: this.bucketName, websiteUrl: this.websiteUrl, }); } } ``` **Why good:** child resources use `{ parent: this }`, name is prefixed from parent, `registerOutputs` is called, type token follows `pkg:module:Type` format See [examples/core.md](examples/core.md) for complete component patterns with provider inheritance and multi-resource components. --- ### Pattern 3: Working with Outputs Outputs represent values resolved after deployment. Never use string concatenation -- use `interpolate`, `apply`, or `all`. ```typescript // interpolate -- tagged template for string building (preferred) const url = pulumi.interpolate`https://${bucket.bucketRegionalDomainName}/index.html`; // apply -- transform a single output const upper = bucket.id.apply((id) => id.toUpperCase()); // all -- combine multiple outputs const endpoint = pulumi .all([lb.dnsName, listener.port]) .apply(([dns, port]) => `http://${dns}:${port}`); // Lifting -- access properties directly on resource outputs const subnetId = vpc.subnets[0].id; // No apply needed for known properties ``` **Why good:** `interpolate` handles Output values transparently, `all` waits for multiple values, lifting avoids unnecessary `apply` calls **Gotcha:** Resources created inside `.apply()` will not appear in `pulumi preview` and may cause ordering issues. Always pass Outputs directly as inputs to other resources. See [examples/core.md](examples/core.md) for Output patterns, `pulumi.output()` wrapping, and the apply anti-pattern. --- ### Pattern 4: Config and Secrets Use `pulumi.Config` for stack-specific values. Use `requireSecret` for sensitive data -- it encrypts the value in state. ```typescript const config = new pulumi.Config(); // Plain config values const region = config.require("region"); // Fails if missing const nodeCount = config.getNumber("nodeCount"); // Returns undefined if missing // Secret values -- encrypted in state const dbPassword = config.requireSecret("dbPassword"); const apiKey = config.getSecret("apiKey"); // Mark programmatic values as secret const connectionString = pulumi.interpolate`postgres://admin:${dbPassword}@${db.endpoint}/mydb`; // connectionString is automatically secret because dbPassword is secret // Explicitly mark a value as secret const token = pulumi.secret(generateToken()); ``` **Key point:** Any Output derived from a secret is automatically marked secret. You do not need to re-mark derived values. See [examples/core.md](examples/core.md) for namespaced config, secret outputs, and config set CLI commands. --- ### Pattern 5: Resource Options Resource options control lifecycle behavior. The most important ones: ```typescript const db = new aws.rds.Instance( "primary-db", { /* ... */ }, { protect: true, // Prevent accidental deletion dependsOn: [vpc, securityGroup], // Explicit ordering ignoreChanges: ["tags"], // Ignore drift on specific props aliases: [{ name: "old-db-name" }], // Rename without recreating retainOnDelete: true, // Keep cloud resource on pulumi destroy deleteBeforeReplace: true, // For resources that must be unique replaceOnChanges: ["engine"], // Force replace on specific changes provider: usEastProvider, // Explicit provider (region, account) }, ); ``` **Gotcha:** Changing a resource's logical name or parent causes Pulumi to delete and recreate it. Use `aliases` to rename safely. See [reference.md](reference.md) for the complete resource options table. --- ### Pattern 6: Stack References Share outputs between stacks using `StackReference`. ```typescript // In the networking stack: export outputs export const vpcId = vpc.id; export const subnetIds = subnets.map((s) => s.id); // In the application stack: consume outputs const networkStack = new pulumi.StackReference("myorg/networking/prod"); const vpcId = networkStack.getOutput("vpcId"); const subnetIds = networkStack.getOutput("subnetIds"); // requireOutput fails if the output doesn't exist (safer than getOutput) const vpcIdRequired = networkStack.requireOutput("vpcId"); ``` See [examples/advanced.md](examples/advanced.md) for stack reference patterns and `getOutputDetails`. --- ### Pattern 7: Transforms Apply transformations to resources and their children. Use `transforms` (not the deprecated `transformations`). Transforms receive an args object with `type`, `props`, and `opts`, and return a modified result or `undefined` to skip. ```typescript // Resource-level: apply tags to all taggable children of a component const vpc = new MyVpcComponent( "vpc", {}, { transforms: [ (args) => { if (isTaggable(args.type)) { return { props: { ...args.props, tags: { ...args.props["tags"], ManagedBy: "pulumi" }, }, opts: args.opts, }; } return undefined; }, ], }, ); ``` **Key difference from deprecated `transformations`:** `transforms` support modifying packaged component children (awsx, eks), support async callbacks, and do not pass a Resource object. See [examples/advanced.md](examples/advanced.md) for stack-level transforms, option modification, and migration from `transformations`. --- ### Pattern 8: Automation API Run Pulumi programmatically without the CLI -- for self-service platforms, integration tests, or custom deployment tooling. ```typescript const stack = await LocalWorkspace.createOrSelectStack({ stackName: "dev", projectName: "my-platform", program: async () => { const bucket = new aws.s3.Bucket("auto-bucket"); return { bucketName: bucket.id }; }, }); const upResult = await stack.up({ onOutput: console.log }); ``` **Key point:** The Automation API requires the Pulumi CLI to be installed and on PATH -- it uses the CLI's engine under the hood. See [examples/advanced.md](examples/advanced.md) for preview, destroy, local program mode, config setup, and stack output retrieval. </patterns> --- <red_flags> ## RED FLAGS **High Priority:** - **Creating resources inside `.apply()`** -- They won't appear in `pulumi preview`, cause ordering issues, and break the dependency graph. Pass Outputs directly as resource inputs. - **Missing `{ parent: this }` in ComponentResource children** -- Resources appear at the root of the state tree, breaking logical grouping and component delete cascading. - **String concatenation with Outputs** -- `"https://" + bucket.id` produces `[object Object]`. Use `pulumi.interpolate` instead. - **Using `Config.require()` for passwords/keys** -- Stores the value as plaintext in state. Use `Config.requireSecret()` to encrypt. - **Forgetting `this.registerOutputs()`** -- Component outputs won't be tracked properly in state or available via stack references. - **Changing logical names without aliases** -- Pulumi deletes and recreates the resource. Use `aliases: [{ name: "old-name" }]` to rename safely. **Medium Priority:** - **Relying on default providers in multi-region setups** -- Use explicit providers per region. Set `pulumi:disable-default-providers` in config to enforce. - **Starting with functions instead of ComponentResource** -- Migrating later requires aliases or resource recreation. Use components from the start. - **Not using `dependsOn` for non-obvious dependencies** -- Pulumi infers dependencies from Input/Output wiring, but side effects (IAM propagation, DNS) need explicit ordering. - **Using deprecated `transformations`** -- Use `transforms` instead. `transforms` also support modifying child resources of packaged components. - **Hardcoding region/account in resource args** -- Use `pulumi.Config` and providers for environment-specific values. **Gotchas & Edge Cases:** - Auto-naming appends a random suffix -- never rely on exact physical resource names in external systems - Pulumi does not refresh state by default -- use `pulumi refresh` to detect drift - `pulumi.secret()` wraps a value so it's encrypted in state -- any derived Output is automatically secret too - `Output.apply()` runs during `pulumi up`, not during `preview` for unknown values -- conditional logic based on unknown outputs may not evaluate during preview - Component type tokens must follow `pkg:module:Type` format (e.g., `myinfra:network:Vpc`) to avoid conflicts - Dynamic providers serialize the provider class -- closures over external state, functions, or DOM nodes will fail - `protect: true` only prevents `pulumi destroy` deletion, not manual cloud console deletion - Stack names in `StackReference` are fully qualified: `org/project/stack` </red_flags> --- <critical_reminders> ## CRITICAL REMINDERS > **All code must follow project conventions in CLAUDE.md** **(You MUST pass `{ parent: this }` to ALL child resources inside a ComponentResource -- omitting it breaks the resource tree and state tracking)** **(You MUST use `pulumi.interpolate` for string building with Outputs -- string concatenation silently produces `[object Object]`)** **(You MUST NEVER create resources inside `.apply()` -- they will not appear in `pulumi preview` and cause ordering issues)** **(You MUST use `Config.requireSecret()` for sensitive values -- `Config.require()` stores values as plaintext in state)** **(You MUST call `this.registerOutputs()` at the end of every ComponentResource constructor -- omitting it prevents output tracking)** **Failure to follow these rules will cause broken state tracking, silent data exposure, and unpredictable deployment behavior.** </critical_reminders>
Comments (0)
Sign in to join the conversation.
Reviews (0)
No reviews yet.
No comments yet.