Fix: Pulumi Not Working — Output<T>, Stack References, Secrets, State Backend, and Preview vs Up
Part of: JavaScript & TypeScript Errors
Quick Answer
How to fix Pulumi errors — Output<T> can't be unwrapped synchronously, stack reference not found, secret leaks in stack outputs, state backend lock, ResourceOptions parent missing, and refresh drift.
The Error
You log a Pulumi Output<string> and get a Promise-like thing:
const bucket = new aws.s3.Bucket("my-bucket");
console.log(bucket.id);
// OutputImpl<id> { ... } — not the actual string.Or you reference a stack output and it’s undefined:
const ref = new pulumi.StackReference("org/network/prod");
const vpcId = ref.getOutput("vpcId");
// undefined at preview time.Or pulumi up complains the state is locked:
error: getting secrets manager: passphrase must be set with
PULUMI_CONFIG_PASSPHRASE or PULUMI_CONFIG_PASSPHRASE_FILEOr a deploy partially succeeds and you can’t redeploy:
error: failed to register new resource ... -- conflicting resource registrationsWhy This Happens
Pulumi differs from Terraform in important ways:
Output<T>is async. Pulumi resource properties returnOutput<T>, not the raw value. They resolve duringpulumi upafter dependencies create. You can chain with.apply(), interpolate withpulumi.interpolate, but you can’t directly read them in your TypeScript flow.- Stack references read another stack’s outputs. They work — but
getOutputreturnsOutput<T | undefined>. The “undefined” you see during preview is genuine; the value is known only after the target stack has been deployed. - State backend is configurable. Pulumi Cloud (default), S3, Azure Blob, GCS, file backend. Each has different locking and secret encryption. The “passphrase” prompt is from file/cloud backends that encrypt your stack secrets.
- Resource URN identifies a resource. Renaming a logical name (
new aws.s3.Bucket("my-bucket")→new aws.s3.Bucket("renamed")) creates a new resource and tries to destroy the old. Use aliases to handle renames safely.
The deeper conceptual issue is that Pulumi programs run twice in a sense. First they execute as ordinary TypeScript/Python/Go to register a graph of resources with the Pulumi engine; then the engine resolves the graph, makes API calls in dependency order, and feeds resolved values back into the Output<T> objects. The fact that you can write what looks like imperative code while it actually constructs a declarative graph is the source of every “why doesn’t this value print” question. Once you internalize that anything derived from a resource property must go through .apply or pulumi.interpolate, the model clicks.
The state backend choice also has knock-on effects most people underestimate. Pulumi Cloud handles encryption, locking, and history automatically; switch to an S3 backend and you become responsible for KMS-backed secret encryption, DynamoDB-style lock coordination is replaced by S3 object versioning, and the audit log lives wherever you point it. Self-hosted backends are fine for compliance reasons, but they shift operational burden from Pulumi to your team. Most new projects should start on Pulumi Cloud and migrate only when there is a concrete reason to do so.
Fix 1: Handle Output<T> Correctly
Three primary patterns:
.apply() — transform inside the callback:
import * as aws from "@pulumi/aws";
const bucket = new aws.s3.Bucket("my-bucket");
// WRONG — bucket.id is Output<string>, not string:
const arn = `arn:aws:s3:::${bucket.id}`;
// RIGHT — apply runs after bucket.id resolves:
const arn = bucket.id.apply((id) => `arn:aws:s3:::${id}`);pulumi.interpolate — template literal that handles Outputs:
import * as pulumi from "@pulumi/pulumi";
const arn = pulumi.interpolate`arn:aws:s3:::${bucket.id}`;
// Returns Output<string> with the full ARN once resolved.pulumi.interpolate is essentially apply + string concat in a cleaner form. Prefer it for simple string composition.
pulumi.all([...]) — combine multiple Outputs:
const policyArn = pulumi.all([bucket.arn, bucket.region]).apply(
([arn, region]) => `${arn}-${region}-policy`,
);Common Mistake: Trying to await an Output<T>. It’s not a Promise; awaiting silently produces wrong values. Use .apply() instead.
For logging during development:
bucket.id.apply((id) => console.log(`Bucket ID: ${id}`));The log appears during pulumi up after the bucket is created.
Fix 2: Configure the State Backend
Pulumi has multiple backend choices. View / change:
pulumi login # Default — Pulumi Cloud (app.pulumi.com)
pulumi login s3://my-bucket # S3 backend
pulumi login azblob://... # Azure
pulumi login gs://my-bucket # GCS
pulumi login file://~/.pulumi # Local fileFor non-Cloud backends, Pulumi encrypts secrets with a passphrase. Set:
export PULUMI_CONFIG_PASSPHRASE=your-secret-passphrase
# Or:
export PULUMI_CONFIG_PASSPHRASE_FILE=~/.pulumi-passphraseFor Pulumi Cloud, secrets are managed by the service — no passphrase needed.
For CI/CD with self-hosted backends:
# GitHub Actions example
- env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}
PULUMI_CONFIG_PASSPHRASE: ${{ secrets.PULUMI_CONFIG_PASSPHRASE }}
run: pulumi up --yesPro Tip: For new teams, start with Pulumi Cloud (free tier covers most usage). Switch to S3 backend only if you need to run fully self-hosted.
Fix 3: Use Secrets Correctly
For values that shouldn’t appear in plaintext (passwords, API keys):
pulumi config set --secret db_password "s3cr3t"This encrypts and stores in the stack config. Read in code:
const config = new pulumi.Config();
const dbPassword = config.requireSecret("db_password");
// dbPassword is Output<string> with the secret flag set.When you apply over a secret, the result inherits the secret flag — so derived outputs are also encrypted at rest:
const connectionString = dbPassword.apply(
(pw) => `postgres://user:${pw}@host:5432/db`,
);
// connectionString is also marked secret.Common Mistake: Logging a secret to console.log. Pulumi tries to redact secrets in output (replaces with [secret]), but if you transform the secret into a non-secret string (e.g. with .apply() that drops the secret flag), it can leak.
To make any computed value secret:
const safe = pulumi.secret(someValue);This explicitly marks someValue as secret so logs and state file encrypt it.
Fix 4: Stack References
To consume outputs from another stack:
const network = new pulumi.StackReference("myorg/network/prod");
// Get a specific output (returns Output<unknown>):
const vpcId = network.getOutput("vpcId");
const subnetIds = network.getOutput("subnetIds");
// Use it:
const sg = new aws.ec2.SecurityGroup("app-sg", {
vpcId: vpcId.apply((id) => id as string),
});Type-safety:
const vpcId = network.requireOutput("vpcId") as pulumi.Output<string>;
const subnetIds = network.requireOutput("subnetIds") as pulumi.Output<string[]>;requireOutput errors if the output doesn’t exist; getOutput returns Output<unknown | undefined>.
For the target stack to expose outputs:
// In the network stack:
export const vpcId = vpc.id;
export const subnetIds = subnets.map((s) => s.id);Anything exported from the program’s entry point becomes a stack output, readable by StackReference.
Common Mistake: Referencing a stack that’s never been deployed. getOutput returns undefined until the target stack has had at least one successful pulumi up. Run the network stack first.
Fix 5: Resource Renames With Aliases
Renaming a logical name creates a new resource:
// Before:
const bucket = new aws.s3.Bucket("data");
// After (logical rename):
const bucket = new aws.s3.Bucket("data-prod");
// Pulumi sees: delete "data", create "data-prod"For S3 buckets this destroys all your data. Add an alias to preserve the resource identity:
const bucket = new aws.s3.Bucket("data-prod", {
// properties...
}, {
aliases: [{ name: "data" }], // Tells Pulumi this is the same resource
});aliases is in ResourceOptions. Pulumi uses the alias to match against the existing state and updates in place instead of recreating.
For deeper structural renames (parent resource changed, name in a different module):
{
aliases: [
{ name: "data", parent: oldParent },
{ name: "data", type: "aws:s3/bucket:Bucket" },
],
}Pro Tip: When refactoring, do aliases first as a separate pulumi up, verify everything still maps correctly with pulumi state, then remove the alias in a future iteration.
Fix 6: pulumi preview vs pulumi up
Always preview before applying:
pulumi preview # Show what would change
pulumi preview --diff # Show resource-level diffs
pulumi up # Apply
pulumi up --yes # Apply without confirmationPreview catches mistakes before they hit production. The diff shows:
+create-delete~update in place+-replace (delete + create)
Watch for +- on critical resources — replacement of a database, DNS record, or load balancer is downtime.
For untargeted “what about everything?”:
pulumi refresh # Read live state, update Pulumi state to matchRefresh detects drift — resources changed outside of Pulumi (e.g. manual AWS console edits). Run before up if you suspect external changes.
Fix 7: ComponentResource for Abstraction
Group related resources into a reusable component:
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";
interface AppServiceArgs {
imageUri: pulumi.Input<string>;
envVars?: pulumi.Input<Record<string, string>>;
}
export class AppService extends pulumi.ComponentResource {
public readonly url: pulumi.Output<string>;
constructor(name: string, args: AppServiceArgs, opts?: pulumi.ComponentResourceOptions) {
super("myorg:app:AppService", name, {}, opts);
const task = new aws.ecs.TaskDefinition(`${name}-task`, {
// ... task config ...
}, { parent: this });
const service = new aws.ecs.Service(`${name}-svc`, {
taskDefinition: task.arn,
// ... service config ...
}, { parent: this });
this.url = service.loadBalancer.apply((lb) => lb[0].targetGroupArn);
this.registerOutputs({
url: this.url,
});
}
}
// Usage:
const app = new AppService("my-app", { imageUri: "..." });Three rules for ComponentResources:
- Pass
{ parent: this }on every child resource — keeps the tree structured. - Call
super(...)with a unique type token (myorg:app:AppService). - Call
registerOutputs(...)at the end to expose component-level outputs.
Pro Tip: ComponentResources don’t add to Pulumi Cloud’s resource count for billing — only the underlying cloud resources do. Use them liberally to encapsulate patterns.
Fix 8: Handle Locks and Failures
If pulumi up was interrupted mid-deploy:
error: the stack is currently locked by another updateCheck for a hanging process:
pulumi cancelThis forcibly releases the lock. Only do this if you’re sure no other deployment is running — if you cancel an in-flight deploy, the state may be inconsistent with reality.
For partial failures (some resources created, others failed):
pulumi up # Try again — Pulumi resumes from where it left off
pulumi refresh # If state and reality diverged, refresh firstTo remove a problematic resource from state without deleting it:
pulumi state delete <urn>This is the escape hatch when Pulumi’s view of a resource conflicts with reality (e.g. someone deleted it in the console).
Common Mistake: Running pulumi destroy to clean up “just one resource.” destroy removes everything in the stack. To delete a single resource, remove it from your code and pulumi up.
Pulumi vs Terraform, AWS CDK, and Crossplane: Programming-Language IaC
Pulumi’s headline feature — write infrastructure in a real programming language — puts it in direct competition with three other IaC approaches, each with different trade-offs.
Terraform is the incumbent. You write HCL, a domain-specific language designed exclusively for infrastructure. HCL is purpose-built so most modules read identically across teams, the provider ecosystem is enormous, and tooling like terraform plan is the gold standard for showing impact before apply. The cost is that HCL is intentionally limited — loops, conditionals, and functions exist but feel awkward, and code reuse boils down to modules that pass strings around. Use Terraform when team familiarity dominates and you value the predictability of a constrained language. Pulumi imports Terraform providers via tfbridge, so the resource coverage gap is smaller than it looks.
AWS CDK is the AWS-native answer to programming-language IaC. You write TypeScript, Python, or Java, and CDK synthesizes CloudFormation templates that CFN deploys. The win is that CDK piggybacks on CloudFormation’s drift detection, rollback semantics, and stack policies; you also get high-level constructs (“L2/L3”) that bake in AWS best practices. The lock-in is real: CDK only targets AWS (CDK for Terraform and CDK for Kubernetes exist but are derivative projects), and the synthesized CloudFormation is what actually deploys, so you inherit CloudFormation’s limits (e.g. 500 resources per stack, eventual consistency in updates). Choose CDK when you are AWS-only and value CloudFormation’s mature rollback behavior.
Pulumi’s distinct angle is multi-cloud with a single programming model. The same TypeScript file can mix AWS, GCP, and Cloudflare resources, with full IDE support, refactoring tools, and unit testing. Component resources let you build reusable abstractions that survive across providers. Choose Pulumi over Terraform when your team is more comfortable in TypeScript/Python than HCL and you actively use the language’s expressiveness (real loops, conditionals, libraries). Choose Pulumi over CDK when you are multi-cloud or want to avoid CloudFormation as the deployment engine.
Crossplane is the Kubernetes-native option. You declare cloud resources as Kubernetes custom resources, and Crossplane controllers reconcile them against the cloud provider. The model fits when you already run Kubernetes and want GitOps-style infrastructure reconciliation through kubectl apply. The cost is operational: you take on the burden of running Crossplane controllers, and the diff/preview workflow is less mature than terraform plan or pulumi preview. Choose Crossplane when Kubernetes is the platform abstraction and infrastructure is just another set of CRDs.
Practical guidance: stay on Terraform if your team is large and HCL is the lingua franca; pick Pulumi for new projects where the team writes TS/Python daily; pick CDK if you are AWS-only and want CloudFormation as the engine; pick Crossplane only inside a Kubernetes-first platform team.
Still Not Working?
A few less-obvious failures:
error: program exited unexpectedly. Your program threw before completing. Check the line before the error — often a missing required input or unhandled promise rejection.pulumi upshows the same diff on every run. A property’s value depends on a non-deterministic computation (timestamp, random). Either pin it or useignoreChangesinResourceOptions.- Cross-region resources fail to create. Pulumi infers region from provider config. For multi-region, create explicit providers:
new aws.Provider("us-west", { region: "us-west-2" })and pass via{ provider }ResourceOption. Getresources (data sources) return stale. Pulumi cachesGetcalls. Refresh withpulumi refreshor restructure to use the resource directly.- TypeScript build errors after Pulumi upgrade. SDK types changed. Run
pulumi plugin installandnpm installfor the matching versions. Pin@pulumi/pulumiand provider packages inpackage.json. - Stack output not appearing after
up. You need toexportit at module top level:export const url = .... Returning from a function or wrapping in a class doesn’t expose it. Resource still has dependentsduring destroy. Pulumi destroys leaf-first; if a resource has unmanaged dependents (created outside Pulumi), they block deletion. Either delete them manually or import them into Pulumi.- Drift after manual console edits.
pulumi refreshcatches it. To prevent future drift, lock down IAM so only Pulumi can edit managed resources. pulumi importproduces ugly code. The auto-generated import code is verbose because Pulumi has no way to know which properties are defaults. Runpulumi importonce to capture state, then hand-clean the generated TypeScript before committing.- Concurrent
pulumi upin CI races on stack locks. Two pull requests both targeting the same stack will queue or fail. Serialize the stack in CI (one runner, one stack) or split into per-PR ephemeral stacks for review environments. - Policy as Code (
pulumi policy) does not block deploys. Policy packs must be enabled in the stack’s settings or viapulumi up --policy-pack. Without enabling, policies are written but never enforced.
For related infrastructure-as-code and deployment issues, see Terraform error acquiring state lock, Terraform failed to install provider, AWS CDK not working, and AWS CloudFormation rollback complete.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: AWS CDK Not Working — Bootstrap Error, ROLLBACK_COMPLETE, and Deploy Failures
How to fix AWS CDK errors — cdk bootstrap required, stack in ROLLBACK_COMPLETE, asset bundling failed, CLI/library version mismatch, VPC lookup failing, and cross-stack export conflicts.
Fix: AWS Amplify Not Working — Gen 2 Backend, defineData, Auth, Storage, and Sandbox Deployments
How to fix AWS Amplify Gen 2 errors — backend.ts file structure, defineData schema authorization, defineAuth flow, defineStorage bucket access, sandbox vs branch deploy, generated outputs, and Cognito triggers.
Fix: AWS Lambda SnapStart Not Working — Version vs Alias, Restore Hooks, and Uniqueness Bugs
How to fix Lambda SnapStart errors — feature requires published version, $LATEST not supported, restore hook for stale connections, UUID collisions after snapshot, time-based state staleness, and pricing surprises.
Fix: Moto Not Working — Mock Decorator, Real AWS Calls Leaking, and v4 to v5 Migration
How to fix Moto errors — mock not activating, real AWS credentials used in tests, ImportError mock_s3 removed in v5, fixtures with multiple services, NoCredentialsError despite mock, and standalone server mode.