skip to content

In a Pulumi program, why does reading a property off a resource — say a bucket's arn — give you an Output<T> rather than a plain string, and how do you build other values from it?

level: juniorimportance: must knowfreq 72%

answer

  1. program runs before anything is created
  2. placeholder plus a dependency edge
  3. transform, never unwrap
  4. apply, all, interpolate
  5. concatenating one is the classic bug

basics

~20 s

Pulumi runs your program before the cloud has created anything, so resource attributes are Output<T> placeholders for values known only after deployment. Derive new values with apply(), pulumi.all() or pulumi.interpolate — never by concatenating an Output as a string.

solid answer

~40 s

A Pulumi program executes first and deploys second. When the program runs, `new aws.s3.Bucket("assets")` has not called AWS yet, so `bucket.arn` cannot be a string — it is an `Output<string>`, a promise-like placeholder that also carries dependency information telling the engine which resource this value came from. You never read the value directly; you transform it. `bucket.arn.apply(arn => ...)` returns a new Output computed from it once it is known, `pulumi.interpolate` builds a string from Outputs, and `pulumi.all([a, b]).apply(([a, b]) => ...)` combines several. In Python the equivalents are `output.apply(lambda v: ...)`, `Output.concat` and `Output.all(...).apply(...)`; in Go, `ApplyT` and `pulumi.Sprintf`. Passing an Output straight into another resource's args is fine and is the common case — resource inputs accept `Input<T>`, and that is exactly how Pulumi discovers the dependency edge between the two resources.

code

typescript · 13 lines
typescript
import * as pulumi from "@pulumi/pulumi";
import * as aws from "@pulumi/aws";

const bucket = new aws.s3.Bucket("assets");

// Wrong: an Output is not a string; Pulumi throws on toString to catch this.
// const bad = "arn is " + bucket.arn;

// Right: derive new Outputs from it.
export const policy = bucket.arn.apply(arn =>
    JSON.stringify({ Version: "2012-10-17", Statement: [{ Resource: [arn, `${arn}/*`] }] }));

export const message = pulumi.interpolate`bucket arn is ${bucket.arn}`;

go deeper

for a junior

Be able to say that resource attributes are Output<T> because the program runs before the cloud creates anything, and show apply or pulumi.interpolate to build a string from one.

for a middle

Explain that an Output carries a dependency set as well as a future value, that resource args accept Input<T> so no apply is needed to pass one through, and how pulumi.all combines several.

for a senior

Show judgment about where transformation belongs: keep apply callbacks pure, avoid side effects and resource creation inside them, and prefer configuration over deployed values when logic must branch.

for a principal

Own the API-design angle: a codebase that awaits or stringifies Outputs quietly loses dependency edges and produces destroy-order bugs, so lint rules, review habits and component interfaces that take and return Output<T> matter more than any individual fix.

## Why the value cannot be there yet A Pulumi program is ordinary code in TypeScript, Python, Go, C# or Java. Running it does not create infrastructure directly: it *registers* resources with the Pulumi engine, which then talks to the provider. So when the line `const bucket = new aws.s3.Bucket("assets")` executes, nothing exists in AWS. The ARN, the generated bucket name, the endpoint — none of them are knowable at that moment. Anything the cloud decides can only be filled in later. A language with real types has to represent "a value that will exist after deployment" somehow. Pulumi's representation is `Output<T>`. ## What an Output actually is An `Output<T>` carries two things: 1. **A value that arrives later** — conceptually a promise for a `T`. 2. **A dependency set** — which resources this value was derived from. The second part is the one people miss, and it is why Output cannot simply be replaced by `await`. When you pass `bucket.arn` into another resource's arguments, the engine records an edge: the new resource depends on the bucket. That edge is what orders the deployment graph and what makes deletes happen in the right order. An awaited raw string would lose it. Outputs are also the carrier for two other bits of metadata: whether the value is **secret** (secretness propagates through applies, so a value derived from a secret stays secret) and whether it is currently **unknown** (which is what happens during a preview of a resource that does not exist yet). ## The three ways to consume one **Pass it along untouched.** Resource argument types are `Input<T>`, which means "a `T`, a promise for one, or an `Output<T>`". So this is legal and is what you want most of the time: ```typescript const bucket = new aws.s3.Bucket("assets"); const obj = new aws.s3.BucketObject("index", { bucket: bucket.id, key: "index.html" }); ``` No `apply` needed — the SDK resolves it and the dependency is recorded. **Transform it with `apply`.** When you need to compute something from the value, `apply` maps `Output<T>` to `Output<U>`: ```typescript const policy = bucket.arn.apply(arn => JSON.stringify({ Version: "2012-10-17", Statement: [{ Resource: [arn, `${arn}/*`] }] })); ``` The result is still an Output. There is no operation that unwraps an Output into a bare value inside your program — that is deliberate, not a missing feature. **Combine several.** `pulumi.all` lifts a list or object of Outputs into one Output of a list or object: ```typescript const conn = pulumi.all([db.address, db.port]).apply(([host, port]) => `${host}:${port}`); ``` For the very common string case, `pulumi.interpolate` is the readable form and needs no callback: ```typescript const conn2 = pulumi.interpolate`${db.address}:${db.port}`; ``` The other languages have the same trio: Python's `apply`, `Output.all`, `Output.concat`; Go's `ApplyT`, `pulumi.All`, `pulumi.Sprintf`; C#'s `Apply`, `Output.Tuple`, `Output.Format`. ## The classic beginner mistakes **String concatenation.** `"arn is " + bucket.arn` compiles, because JavaScript will stringify anything. Pulumi deliberately makes `toString` on an Output throw a loud error explaining what to use instead, precisely because this bug would otherwise ship a literal placeholder into a policy document. **Branching on an Output.** `if (bucket.arn) { ... }` is always true, and `if (someOutput === "prod")` is never true. Control flow that must depend on a deployed value has to live *inside* an apply, or — much better — depend on configuration instead, which is a plain value your program knows immediately. **Trying to await it.** `Output` is not a `Promise` and awaiting your way around the model throws away the dependency edge even where a runtime hack makes it appear to work. **Doing side effects in the callback.** An apply callback is not a general-purpose hook; it can be skipped when the value is unknown, and it can run more than once across preview and update. Keep it a pure transformation of the value. ## Wiring it to stack outputs Exporting an Output is the normal way to publish a value from a stack: in TypeScript `export const url = ...`, in Python `pulumi.export("url", ...)`. The engine resolves it at the end of the deployment and stores the concrete value in the stack's outputs, which is why exports show real strings even though your program only ever handled placeholders.

  • If Outputs are promise-like, why not just make them Promises and use await?
    Because an Output carries more than a future value: it carries the set of resources the value came from, plus secretness and unknown-ness. Passing an Output into another resource's args is how the engine learns the dependency edge that orders create and delete. Awaiting collapses it to a bare value and that information is lost.
  • Does passing an Output into another resource's arguments require an apply?
    No. Resource argument types are `Input<T>`, which accepts a raw value, a promise, or an Output, so `{ bucket: bucket.id }` is the idiomatic form. Reach for `apply` only when you need to compute something new from the value — build a JSON policy, slice a string, pick a field.
  • What happens to secretness when you apply over a secret Output?
    It propagates. A value derived from a secret Output is itself marked secret, so it stays encrypted in state and is masked in CLI output. That is intentional — it means you cannot accidentally launder a password into a plaintext export by passing it through a transformation.

An Output is like a shipping tracking number rather than the parcel: you can write instructions about what to do when it arrives, but you cannot open it at the counter.

saying these in an interview costs you the question

  • Says you can await an Output to get the value
  • Builds strings with + instead of interpolate or apply
  • Thinks Output exists because Pulumi is asynchronous JavaScript
  • Uses an Output in an if condition to branch the program
  • Believes apply returns the raw unwrapped value

context