Pulumi IaC skill for Codex
Typed infrastructure code with no surprise replaces. Codex writes Pulumi programs that treat Outputs correctly with interpolate and apply, keep secrets encrypted in stack config or ESC, and group resources into ComponentResources with proper parents. Before anything deploys it reads pulumi preview --diff, adds aliases or protect where a rename would replace live resources, runs mocked unit tests, and waits for your go-ahead on pulumi up.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- pulumi-iac Installs to ~/.agents/skills/pulumi-iac/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install pulumi-iac The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project pulumi-iac Installs to .agents/skills/pulumi-iac/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/pulumi-iac.zipunzip -o pulumi-iac.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) pulumi-iac PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $pulumi-iac, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Writes and maintains Pulumi infrastructure programs in TypeScript and Python - project and stack layout, stack config and encrypted secrets, Outputs and apply without leaking unknowns, ComponentResources, StackReferences, aliases for refactors, pulumi import, Pulumi ESC environments, unit tests with mocks and preview-driven CI. Use when creating a Pulumi project, adding resources or stacks, refactoring without replacements, managing secrets in Pulumi config, or reviewing a pulumi preview diff.
Use it when
- Infrastructure is defined, or will be, in Pulumi TypeScript or Python
- A preview shows replacements you did not expect
- Secrets or per-environment values need to live in Pulumi config
- Existing cloud resources should be imported into a Pulumi stack
Use something else when
- The team uses HCL with Terraform or OpenTofu: use terraform-modules
- You are building AWS infrastructure with CDK constructs and CloudFormation: use aws-cdk
How to check the result
pulumi preview --diff shows only intended operations before the update, and pulumi preview --expect-no-changes passes afterwards.
The mistake it prevents
Using a resource Output inside a plain template string, which bakes a placeholder into policies or URLs instead of the real value.
A Pulumi change is ready when pulumi preview --diff shows only the intended creates,
updates and replacements, secrets never appear in plaintext in code, config or logs,
and the program is ordinary well-typed code with tests. Treat every replacement in a
preview as a potential outage until proven otherwise.
Inspect first
- Read
AGENTS.md,Pulumi.yaml(runtime, main entry), and everyPulumi.<stack>.yaml. - Identify the backend:
pulumi whoami -v(Pulumi Cloud, or self-manageds3://,gs://,azblob://,file://). Self-managed backends use a passphrase or cloud KMS secrets provider; know which before touching secrets. - Check the language toolchain and package manager (npm/pnpm/yarn, pip/uv/poetry) and
pinned
@pulumi/*orpulumi-*package versions. pulumi stack lsandpulumi stack select <stack>; never assume the selected stack is the one the user means.- Run
pulumi previewbefore editing to capture the baseline. Pending changes before you start must be understood, not mixed into your change.
Previews and updates need backend access, cloud credentials and network. Without them, write and type-check the code and leave preview and update to the user.
Layout
infra/
├── Pulumi.yaml
├── Pulumi.dev.yaml # per-stack config, secrets encrypted
├── Pulumi.prod.yaml
├── index.ts # composes components; little logic here
├── components/
│ └── webService.ts # ComponentResource
└── tests/
└── webService.test.ts
| Split by | When |
|---|---|
| Stacks of one project | Same infrastructure, different environments (dev, staging, prod) |
| Separate projects plus StackReferences | Different lifecycles or owners (network vs apps), or blast-radius limits |
| ComponentResource | A reusable group of resources inside or across projects |
Config and secrets
pulumi config set aws:region eu-west-1
pulumi config set app:instanceCount 3
pulumi config set --secret app:dbPassword # prompts; value stored encrypted
pulumi config get app:dbPassword # decrypts; do not echo into chat or logs
TypeScript:
import * as pulumi from "@pulumi/pulumi";
const cfg = new pulumi.Config("app");
const instanceCount = cfg.getNumber("instanceCount") ?? 2;
const dbPassword = cfg.requireSecret("dbPassword"); // Output<string>, marked secret
Python:
import pulumi
cfg = pulumi.Config("app")
instance_count = cfg.get_int("instanceCount") or 2
db_password = cfg.require_secret("dbPassword")
- Never
pulumi config seta secret without--secret; it lands in plaintext in Git. - Values derived from secrets stay secret; values you build from plain strings do not. Wrap
with
pulumi.secret(value)when a non-config value is sensitive. - Shared secrets and cloud credentials across stacks belong in Pulumi ESC environments,
imported in stack config with
environment: [org/project/env]; reading them needs Pulumi Cloud.
Outputs, the main trap
Resource properties are Output<T>: values known only after deployment. Do not read them
as plain values.
import * as aws from "@pulumi/aws";
const bucket = new aws.s3.Bucket("assets"); // pulumi-aws v7+; v6 programs use BucketV2
// Wrong: produces "[object Object]" or a warning string
const url = `https://${bucket.bucketRegionalDomainName}/index.html`;
// Right
const url2 = pulumi.interpolate`https://${bucket.bucketRegionalDomainName}/index.html`;
const policyJson = pulumi.all([bucket.arn, role.arn]).apply(([b, r]) =>
JSON.stringify({ Version: "2012-10-17", Statement: [{ Effect: "Allow", Principal: { AWS: r }, Action: "s3:GetObject", Resource: `${b}/*` }] }),
);
Python equivalents: pulumi.Output.concat("https://", bucket.bucket_regional_domain_name),
pulumi.Output.all(bucket.arn, role.arn).apply(lambda args: ...).
- Never create resources inside
apply; they will not show in preview and break diffing. applycallbacks do not run during preview when inputs are unknown; logic there must tolerate that.
Components
export interface WebServiceArgs {
image: pulumi.Input<string>;
replicas?: number;
}
export class WebService extends pulumi.ComponentResource {
public readonly url: pulumi.Output<string>;
constructor(name: string, args: WebServiceArgs, opts?: pulumi.ComponentResourceOptions) {
super("acme:app:WebService", name, {}, opts);
// child resources take { parent: this }
// ...
this.url = pulumi.output("https://example.invalid");
this.registerOutputs({ url: this.url });
}
}
Pass { parent: this } to every child so names, aliases and deletes follow the component.
The type token (acme:app:WebService) is part of each child’s URN: changing it later
requires aliases.
Refactoring without replacement
Renaming a resource, changing its parent, or moving it into a component changes its URN, which Pulumi treats as delete plus create. Declare the old identity:
const logs = new aws.s3.Bucket("app-logs", {}, {
aliases: [{ name: "logs" }], // old logical name
// aliases: [{ parent: pulumi.rootStackResource }] when moving into a component
});
Also guard critical resources: protect: true blocks deletion until removed;
retainOnDelete: true leaves the cloud resource when Pulumi stops managing it.
Properties that force replacement (names, AZs) can be softened with
deleteBeforeReplace only when downtime is acceptable.
Importing existing resources
pulumi import aws:s3/bucket:Bucket legacy-assets acme-legacy-assets
The CLI prints generated code; paste it into the program, then pulumi preview must show
no changes for that resource. In code, { import: "acme-legacy-assets" } as a resource
option does the same through the normal preview and update flow.
Testing
Unit tests with mocks (no cloud calls) for component logic:
import * as pulumi from "@pulumi/pulumi";
pulumi.runtime.setMocks({
newResource: (a) => ({ id: `${a.name}-id`, state: a.inputs }),
call: (a) => a.inputs,
});
it("tags every bucket with the environment", async () => {
const { bucket } = await import("../index");
const tags = await new Promise((res) => bucket.tags.apply(res));
expect(tags).toMatchObject({ env: "test" });
});
Set mocks before importing the program. For policy (no public buckets, required tags),
use Pulumi policy packs: pulumi preview --policy-pack ./policies.
Workflow
- [ ] 1. Select the right stack; capture baseline pulumi preview
- [ ] 2. Write code; type-check (tsc --noEmit / pyright or mypy)
- [ ] 3. Unit tests with mocks
- [ ] 4. pulumi preview --diff: review every replace and delete
- [ ] 5. Add aliases / protect where a replace is not intended
- [ ] 6. Show the preview; with user confirmation, pulumi up on non-prod first
- [ ] 7. pulumi preview again: expect no changes
Hard rules
pulumi up --yes,pulumi destroy,pulumi stack rm,pulumi state deleteandpulumi refresh --yeschange real infrastructure or state: show what will happen and get explicit confirmation. Never run them against production on your own initiative.- Use
pulumi up --targetonly as a confirmed emergency measure; it leaves the stack partially updated. pulumi canceland manual state edits (pulumi stack export/import) are last resorts: export a backup first.- Pin provider package versions; a minor provider bump can change defaults and trigger replaces.
Verification
npx tsc --noEmit # or: pyright / mypy
npm test # mocked unit tests
pulumi preview --diff --stack dev
pulumi preview --expect-no-changes --stack dev # after up: non-zero exit if anything changes
pulumi stack output --stack dev
Output
**Stack:** acme/infra/dev (Pulumi Cloud backend)
**Change:** WebService component; existing bucket moved under it with alias (no replacement)
**Preview:** +4 create, ~1 update, 0 replace, 0 delete
**Secrets:** dbPassword via config --secret; ESC env acme/shared-aws for credentials
**Checks:** tsc clean, 5 mocked tests pass, post-update preview --expect-no-changes ok
**Not done:** prod stack untouched; run the same preview there and confirm before pulumi up What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Pulumi IaC"
short_description: "Typed infrastructure code with no surprise replaces"
default_prompt: "$pulumi-iac Set up a Pulumi TypeScript project with dev and prod stacks for this service" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.