How-to Guides¶
Scope
Task recipes for Pulumi CLI 3.264.x (2026-09): install and upgrade, backends, stacks, state, secrets and ESC, any Terraform provider, migrating from Terraform, policy packs, Neo, CI with OIDC, and troubleshooting. Look-up tables (flags, env vars, backend URLs, prices) are in Reference. Background is in Explanation.
Deployment¶
Installation & Setup¶
# Linux / macOS install script (installs to ~/.pulumi/bin)
curl -fsSL https://get.pulumi.com | sh
# macOS Homebrew (Pulumi's tap tracks releases fastest)
brew install pulumi/tap/pulumi
# Windows
winget install pulumi
# Check the version (latest as of 2026-09-25: v3.264.0)
pulumi version
Upgrade by re-running the install script or with brew upgrade pulumi. Language SDKs (@pulumi/pulumi, pulumi on
PyPI, and so on) are versioned with the CLI, so upgrade them together. After a CLI upgrade, run pulumi install in each
project.
Runtime minimums changed in 2026
The Node.js SDK needs Node.js 22+ since 3.249.0, and the Python SDK needs Python 3.10+. Upgrade your CI
images before bumping @pulumi/pulumi or pulumi. The full matrix is in
Reference.
Log In to a Backend¶
pulumi login # Pulumi Cloud (browser or PULUMI_ACCESS_TOKEN)
pulumi login --local # Local filesystem (~/.pulumi)
pulumi login s3://my-pulumi-state # S3 bucket (uses AWS SDK credentials)
pulumi login azblob://state # Azure Blob container
pulumi login gs://my-pulumi-state # Google Cloud Storage
pulumi login postgres://user:pass@db.example.com:5432/pulumi # PostgreSQL
pulumi whoami -v # Show the current user and backend URL
Create a Project¶
mkdir myinfra && cd myinfra
pulumi new aws-typescript # or aws-python, aws-go, aws-csharp, aws-java, aws-yaml
pulumi new python # empty Python project
pulumi new java-gradle # Java with Gradle instead of Maven
To use Bun instead of Node.js, set runtime: bun in Pulumi.yaml. Function serialization and dynamic providers are
not available on Bun.
Stack Management¶
# Create environments as stacks
pulumi stack init dev
pulumi stack init staging
pulumi stack init production
pulumi stack select production
pulumi stack ls
# Stack-specific config
pulumi config set aws:region us-east-1
pulumi config set --secret dbPassword "s3cret"
# Preview, deploy, inspect, destroy
pulumi preview --diff
pulumi up --yes
pulumi stack output
pulumi stack --show-urns
pulumi destroy --yes
pulumi stack rm dev # only after destroy
State Operations¶
# Export / import the checkpoint (backup, manual surgery)
pulumi stack export --file state.json
pulumi stack import --file state.json
# Reconcile state with the real cloud (drift)
pulumi refresh
# Adopt an existing resource and generate code for it
pulumi import aws:s3/bucket:Bucket my-bucket my-bucket-name
# Remove a resource from state without deleting it in the cloud
pulumi state remove 'urn:pulumi:prod::app::aws:s3/bucket:Bucket::old-bucket'
# Move a stack from a DIY backend into the backend you are logged in to (3.254.0+)
pulumi login # target: Pulumi Cloud
pulumi stack migrate s3://my-pulumi-state my-app-production
Migrate a DIY Backend off Non-Project Mode¶
Since 3.257.0 the CLI errors on the legacy DIY layout where stacks are not scoped by project:
pulumi login s3://my-pulumi-state
pulumi state upgrade # rewrites the layout to project-scoped stacks
# Temporary bypass only, the layout will be removed:
export PULUMI_DIY_BACKEND_IGNORE_DEPRECATION_ERROR=true
Back up the bucket (or enable versioning) before running pulumi state upgrade.
Secrets Management¶
# Encrypted config value (encrypted with the stack's secrets provider)
pulumi config set --secret dbPassword "s3cr3t"
# Create a stack that uses a KMS key instead of the default provider
pulumi stack init prod --secrets-provider="awskms://alias/pulumi?region=us-east-1"
# Change the secrets provider of an existing stack (re-encrypts config and state)
pulumi stack change-secrets-provider "awskms://alias/pulumi?region=us-east-1"
In code, read secrets with config.requireSecret("dbPassword") (TypeScript) or config.require_secret("dbPassword")
(Python). Wrap computed sensitive values in pulumi.secret(...) so they stay masked.
Use Pulumi ESC¶
Pulumi ESC environments hold config and secrets and can mint short-lived cloud credentials. ESC requires Pulumi Cloud.
# One-shot: create AWS OIDC trust + ESC environments (3.261.0+)
pulumi env setup aws
# Or by hand
pulumi env init myorg/platform/aws-prod
pulumi env edit myorg/platform/aws-prod
pulumi env open myorg/platform/aws-prod # resolve and print values
pulumi env run myorg/platform/aws-prod -- aws sts get-caller-identity
An environment definition that uses OIDC to get AWS credentials:
values:
aws:
login:
fn::open::aws-login:
oidc:
roleArn: arn:aws:iam::123456789012:role/pulumi-esc
sessionName: pulumi-environments-session
environmentVariables:
AWS_ACCESS_KEY_ID: ${aws.login.accessKeyId}
AWS_SECRET_ACCESS_KEY: ${aws.login.secretAccessKey}
AWS_SESSION_TOKEN: ${aws.login.sessionToken}
pulumiConfig:
aws:region: us-east-1
To consume it from a stack, add this to Pulumi.<stack>.yaml:
The standalone esc CLI is retired
Use pulumi env ... (bundled in the Pulumi CLI). esc 0.26.0 prints a retirement notice on every command.
Use Any Terraform Provider¶
# Generates a typed local SDK (TS, Python, Go, .NET, Java) and records it in Pulumi.yaml
pulumi package add terraform-provider hashicorp/random 3.7.1
# A provider not published to any registry
pulumi package add terraform-provider /path/to/terraform-provider-internal
# Teammates and CI regenerate the SDKs from Pulumi.yaml
pulumi install
Providers resolve from the OpenTofu registry (a mirror of the Terraform registry). Pin both the terraform-provider
package version and the wrapped provider version in Pulumi.yaml for reproducible builds.
Migrate from Terraform¶
# Convert HCL to a Pulumi program in your language (review the output, conversion is partial)
pulumi convert --from terraform --language typescript --out ./pulumi-ts
# Or keep HCL and run it on the Pulumi engine (CLI 3.256.0+)
cat > Pulumi.yaml <<'EOF'
name: my-hcl-project
runtime: hcl
EOF
pulumi install
pulumi up
# Adopt already-deployed resources instead of recreating them
pulumi import --file import.json
For live infrastructure, import existing resources rather than letting the first pulumi up create duplicates.
Policy as Code¶
pulumi policy new aws-typescript # scaffold a policy pack
pulumi preview --policy-pack ./policy # run it locally (any edition)
pulumi policy analyze --file state.json # analyze an exported state file without a backend (3.249.0+)
pulumi policy publish myorg # publish for org-wide enforcement (Pro+)
A minimal policy that requires server-side encryption on S3 buckets (with @pulumi/aws v7, encryption is a separate
BucketServerSideEncryptionConfiguration resource, so a stack-level check is used):
import { PolicyPack, validateStackResourcesOfType } from "@pulumi/policy";
import * as aws from "@pulumi/aws";
new PolicyPack("s3-encryption", {
policies: [{
name: "s3-bucket-has-sse",
description: "Every S3 bucket must have a server-side encryption configuration.",
enforcementLevel: "mandatory",
// Note: IDs of not-yet-created buckets are unknown during preview; this check is exact on `pulumi up`.
validateStack: validateStackResourcesOfType(aws.s3.Bucket, (buckets, args, reportViolation) => {
const encrypted = new Set(
args.resources
.filter(r => r.isType(aws.s3.BucketServerSideEncryptionConfiguration))
.map(r => r.props.bucket),
);
for (const b of buckets) {
if (!encrypted.has(b.id)) {
reportViolation(`S3 bucket ${b.bucket} has no server-side encryption configuration.`);
}
}
}),
}],
});
Correction
The earlier example on this page checked the type aws:s3/bucketV2:BucketV2 and an inline
serverSideEncryptionConfiguration. In @pulumi/aws v7 the type is aws:s3/bucket:Bucket, and encryption is
configured with the separate aws.s3.BucketServerSideEncryptionConfiguration resource.
Use Pulumi Neo¶
pulumi neo # interactive agent session in the terminal
pulumi neo -p "Which stacks use aws provider < 7?" # one-shot, prints the final answer
pulumi neo --debug-update # investigate the last failed update (3.251.0+)
pulumi neo acp # run Neo as an Agent Client Protocol agent for editors
Neo requires Pulumi Cloud on Essentials or above and is billed at $3 per 1M tokens (see Reference). Use read-only mode or Plan Mode when you only want investigation.
Run in CI with OIDC (GitHub Actions)¶
Exchange the GitHub OIDC token for a short-lived Pulumi token instead of storing PULUMI_ACCESS_TOKEN:
permissions:
id-token: write
contents: read
jobs:
preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pulumi/auth-actions@v2
with:
organization: myorg
requested-token-type: urn:pulumi:token-type:access_token:organization
- uses: pulumi/actions@v7
with:
command: preview
stack-name: myorg/my-app/prod
Register GitHub as a trusted OIDC issuer in Pulumi Cloud (organization settings) first. Cloud credentials for the
stack come from an ESC environment with aws-login, azure-login or gcp-login.
Commands & Recipes¶
Core Workflow¶
pulumi preview # plan
pulumi up # apply
pulumi refresh # sync state with reality
pulumi destroy # tear down
pulumi stack output # outputs
pulumi about # versions of CLI, plugins, runtime (paste into bug reports)
TypeScript Example (AWS)¶
This example targets @pulumi/aws v7.
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
// VPC
export const vpc = new aws.ec2.Vpc("main-vpc", {
cidrBlock: "10.0.0.0/16",
enableDnsHostnames: true,
tags: { Name: "production" },
});
// Private S3 bucket with default encryption
const bucket = new aws.s3.Bucket("data-bucket");
new aws.s3.BucketPublicAccessBlock("data-bucket-pab", {
bucket: bucket.id,
blockPublicAcls: true,
blockPublicPolicy: true,
ignorePublicAcls: true,
restrictPublicBuckets: true,
});
new aws.s3.BucketServerSideEncryptionConfiguration("data-bucket-sse", {
bucket: bucket.id,
rules: [{
applyServerSideEncryptionByDefault: { sseAlgorithm: "AES256" },
}],
});
// Export outputs
export const vpcId = vpc.id;
export const bucketName = bucket.bucket;
Correction
The previous example set acl and serverSideEncryptionConfiguration inline on aws.s3.Bucket. Those inline
arguments are deprecated in current AWS providers. Use the dedicated resources shown above.
Python Example (K8s)¶
import pulumi
import pulumi_kubernetes as k8s
app_labels = {"app": "nginx"}
deployment = k8s.apps.v1.Deployment("nginx",
spec=k8s.apps.v1.DeploymentSpecArgs(
replicas=3,
selector=k8s.meta.v1.LabelSelectorArgs(match_labels=app_labels),
template=k8s.core.v1.PodTemplateSpecArgs(
metadata=k8s.meta.v1.ObjectMetaArgs(labels=app_labels),
spec=k8s.core.v1.PodSpecArgs(
containers=[k8s.core.v1.ContainerArgs(
name="nginx",
image="nginx:1.27",
ports=[k8s.core.v1.ContainerPortArgs(container_port=80)],
)],
),
),
))
pulumi.export("deployment_name", deployment.metadata.name)
Unit Testing (TypeScript)¶
Mocks must be registered before the program module is imported:
import * as pulumi from "@pulumi/pulumi";
import { expect } from "chai";
pulumi.runtime.setMocks({
newResource: (args) => ({ id: args.name + "_id", state: args.inputs }),
call: (args) => args.inputs,
});
describe("Infrastructure", () => {
it("should create a VPC with correct CIDR", async () => {
const infra = await import("../index");
const cidr = await new Promise((resolve) =>
infra.vpc.cidrBlock.apply(resolve)
);
expect(cidr).to.equal("10.0.0.0/16");
});
});
Troubleshooting¶
| Issue | Diagnosis | Fix |
|---|---|---|
[409] Conflict: Another update is currently in progress |
Concurrent or crashed update on the same stack | Wait, or pulumi cancel (Pulumi Cloud). On DIY backends, remove the stale lock under .pulumi/locks/ only if no update is running |
| DIY backend error about non-project mode | Legacy layout, CLI 3.257.0+ | pulumi state upgrade |
| Drift between state and cloud | pulumi refresh --preview-only |
pulumi refresh, then pulumi up |
| Plugin version mismatch | pulumi plugin list, pulumi about |
pulumi plugin install resource aws v7.48.0, or pulumi install |
| Slow CI from plugin downloads | Plugins fetched each run | Cache ~/.pulumi/plugins keyed on lockfiles |
pulumi up wants to replace renamed resources |
URN changed | Add aliases to the resource options |
| Node.js SDK install fails on Node 18/20 | Node.js SDK 3.249.0+ requires Node 22 | Upgrade Node.js |
| Secrets unreadable after moving a stack | Different secrets provider | pulumi stack migrate (re-encrypts), or pulumi stack change-secrets-provider |