Skip to content

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:

environment:
  - platform/aws-prod

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

Sources