skip to content

Pulumi

Infrastructure written in a real programming language — TypeScript, Python, Go — over the same provider ecosystem, with its own state and secrets handling. Interviewers ask when loops and types beat HCL and when they simply make a config harder to review.

on this pageshow

questions

15

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

open as a page

In Pulumi, what is a stack, and how does one Pulumi program deploy separate dev, staging and production environments?

level: juniorimportance: must knowfreq 70%

basics

~20 s

A Pulumi stack is one independently configurable instance of a program, with its own config file, its own state and its own outputs. Running the same program against dev, staging and prod stacks gives three isolated deployments and no duplicated code.

open as a page

What is a Pulumi ComponentResource, and what must a ComponentResource subclass do in its constructor for the component to behave correctly?

level: middleimportance: must knowfreq 50%

basics

~20 s

A ComponentResource is a logical grouping of other resources exposed as one reusable class — Pulumi's module equivalent. Its constructor must call super with a type token and name, create every child with the parent option set to itself, and finish with registerOutputs.

open as a page

How does Pulumi keep secret values out of plaintext in stack config and in state, and what is a stack's "secrets provider"?

level: middleimportance: must knowfreq 60%

basics

~20 s

Pulumi encrypts values you mark as secret before they are written anywhere. pulumi config set --secret stores ciphertext in Pulumi.<stack>.yaml, secretness propagates into the state checkpoint, and the secrets provider — a passphrase, a cloud KMS key, or the Pulumi Cloud per-stack key — holds the encryption key.

open as a page

Pulumi lets you define infrastructure in TypeScript, Python, Go or C#, while Terraform uses HCL. What do you actually gain, and what do you give up, by choosing a general-purpose language?

level: middleimportance: must knowfreq 75%

basics

~20 s

A general-purpose language buys real control flow, static types, IDE completion and unit tests over infrastructure logic. It costs reviewability: a pull request now shows the program that produces resources rather than the resources themselves.

open as a page

Coming from Terraform, which Pulumi CLI commands correspond to `terraform plan`, `terraform apply` and `terraform destroy`, and is there anything equivalent to `terraform init`?

level: juniorimportance: should knowfreq 50%

basics

~20 s

pulumi preview corresponds to plan, pulumi up to apply, pulumi destroy to destroy, and pulumi refresh reconciles recorded state with the cloud. There is no provider init step: SDK packages come from the language's package manager and plugins download automatically.

open as a page

When one Pulumi stack needs a value produced by another — say an app stack needing a VPC id from a networking stack — how do you wire that up, and what are the failure modes?

level: middleimportance: should knowfreq 45%

basics

~20 s

The producer stack exports the value as a stack output; the consumer constructs a pulumi.StackReference to the producer's fully qualified name and reads it with getOutput. The consumer needs read access to the producer's stack in the backend, and deployment order between the two stacks is your responsibility, not the engine's.

open as a page

In Pulumi, what happens when you change the string logical name passed to a resource constructor — for example renaming `new aws.s3.Bucket("assets")` to `"assets-bucket"` — and how do you make that rename without destroying the resource?

level: seniorimportance: should knowfreq 42%

basics

~20 s

The logical name is part of the resource's URN, which is its identity in state. Renaming it makes Pulumi see one resource deleted and a different one created. To rename safely, add an aliases resource option naming the old identity so the engine maps the old URN to the new one.

open as a page

During `pulumi preview` of a stack whose resources do not exist yet, what does an Output.apply callback see, and what does that imply about the code you put inside apply?

level: seniorimportance: should knowfreq 40%

basics

~20 s

On a preview, values from not-yet-created resources are unknown, so Pulumi skips those apply callbacks and propagates an unknown instead of running them. Code inside apply therefore may never execute at preview time — keep it a pure transformation with no side effects and no resource creation.

open as a page

Your team wants to move an existing Terraform repository that manages live production infrastructure over to Pulumi. What is the migration path, and what does not carry over automatically?

level: seniorimportance: should knowfreq 38%

basics

~20 s

Two separate jobs: convert the code with pulumi convert --from terraform, then adopt the live resources into the Pulumi stack with imports. Converting code alone is dangerous — without imports the first up recreates everything, because state does not convert.

open as a page

People describe `pulumi preview` as the equivalent of `terraform plan`. Where does that comparison break down?

level: seniorimportance: should knowfreq 32%

basics

~20 s

The comparison holds at the level of intent but leaks in three places: a Pulumi preview executes your program, so code side effects happen at preview time; it does not refresh against the cloud unless asked; and resources created inside a callback on an unknown value are invisible until apply.

open as a page

For a team adopting Pulumi, how would you decide between Pulumi Cloud and a self-managed state backend such as an S3 or GCS bucket, and what do you take on with the self-managed option?

level: principalimportance: should knowfreq 40%

basics

~20 s

Pulumi Cloud is the managed backend: it holds each stack's checkpoint, manages a per-stack encryption key, and provides accounts, permissions and history. Self-managed means pulumi login s3://bucket, and you then own bucket durability, the secrets provider and key distribution, access control, and making sure two pipelines never update one stack at once.

open as a page

How does a Pulumi program read its per-stack configuration values in code, and what is the difference between the require and get families of methods on pulumi.Config?

level: juniorimportance: nice to knowfreq 36%

basics

~20 s

A program constructs new pulumi.Config() and reads values from it. The require methods throw a clear error when a key is missing, so the deployment fails immediately; the get methods return undefined or null, letting you supply a default in code.

open as a page

Pulumi's classic aws, azure and gcp providers are generated from the equivalent Terraform providers via Pulumi's Terraform bridge. What does that mean in practice for resource coverage and for provider bugs?

level: middleimportance: nice to knowfreq 42%

basics

~20 s

Bridged providers are machine-generated from the upstream Terraform providers, so resource and argument coverage is near-identical and documentation maps across almost one to one. The flip side: an upstream provider bug reproduces in Pulumi, because it is the same code.

open as a page

A teammate committed a production database password as an ordinary value in `Pulumi.prod.yaml` rather than with `--secret`. What do you do, and what does `pulumi stack change-secrets-provider` actually re-encrypt?

level: seniorimportance: nice to knowfreq 25%

basics

~20 s

Treat the credential as compromised and rotate it at the source first, then re-set it with pulumi config set --secret. Changing the stack's secrets provider re-encrypts the stack's secret config and the secrets held in its state under a new key — it does nothing about a plaintext value already in the repository's history.

open as a page