Skip to content

Fix: Pulumi Not Working — Output<T>, Stack References, Secrets, State Backend, and Preview vs Up

FixDevs · (Updated: )

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_FILE

Or a deploy partially succeeds and you can’t redeploy:

error: failed to register new resource ... -- conflicting resource registrations

Why This Happens

Pulumi differs from Terraform in important ways:

  • Output<T> is async. Pulumi resource properties return Output<T>, not the raw value. They resolve during pulumi up after dependencies create. You can chain with .apply(), interpolate with pulumi.interpolate, but you can’t directly read them in your TypeScript flow.
  • Stack references read another stack’s outputs. They work — but getOutput returns Output<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 file

For non-Cloud backends, Pulumi encrypts secrets with a passphrase. Set:

export PULUMI_CONFIG_PASSPHRASE=your-secret-passphrase
# Or:
export PULUMI_CONFIG_PASSPHRASE_FILE=~/.pulumi-passphrase

For 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 --yes

Pro 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 confirmation

Preview 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 match

Refresh 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:

  1. Pass { parent: this } on every child resource — keeps the tree structured.
  2. Call super(...) with a unique type token (myorg:app:AppService).
  3. 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 update

Check for a hanging process:

pulumi cancel

This 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 first

To 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 up shows the same diff on every run. A property’s value depends on a non-deterministic computation (timestamp, random). Either pin it or use ignoreChanges in ResourceOptions.
  • 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.
  • Get resources (data sources) return stale. Pulumi caches Get calls. Refresh with pulumi refresh or restructure to use the resource directly.
  • TypeScript build errors after Pulumi upgrade. SDK types changed. Run pulumi plugin install and npm install for the matching versions. Pin @pulumi/pulumi and provider packages in package.json.
  • Stack output not appearing after up. You need to export it at module top level: export const url = .... Returning from a function or wrapping in a class doesn’t expose it.
  • Resource still has dependents during 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 refresh catches it. To prevent future drift, lock down IAM so only Pulumi can edit managed resources.
  • pulumi import produces ugly code. The auto-generated import code is verbose because Pulumi has no way to know which properties are defaults. Run pulumi import once to capture state, then hand-clean the generated TypeScript before committing.
  • Concurrent pulumi up in 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 via pulumi 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.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles