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?
answer
- parameterise the program, not edit it
- require throws, get returns undefined
- values are strings until parsed
- typed variants for number, boolean, object
- namespaced by project name
basics
~20 sA 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.
solid answer
~50 sConfiguration is how a program is parameterised per stack, and the program side of it is one object: `const config = new pulumi.Config()`, then `config.require("dbName")` or `config.get("region")`. `require` fails the run with a message naming the missing key — the right choice for anything without a sensible default. `get` returns undefined so you can fall back in code. Both come in typed variants — `requireNumber`, `getBoolean`, `requireObject<T>` — because the underlying config values are strings, and the typed variants parse and validate rather than leaving you to coerce. A bare `new pulumi.Config()` is namespaced to the project name, so it reads keys like `myproject:dbName`; `new pulumi.Config("aws")` reads the `aws:` namespace, which is how provider settings such as `aws:region` are exposed. Because these are plain values available the instant the program starts, they are the right thing to branch on — unlike an Output.
code
typescript · 13 linesimport * as pulumi from "@pulumi/pulumi";
const config = new pulumi.Config();
const dbName = config.require("dbName"); // throws if missing
const nodeCount = config.getNumber("nodeCount") ?? 3; // default in code
const region = new pulumi.Config("aws").require("region");
if (nodeCount < 1 || nodeCount > 20) {
throw new Error(`nodeCount must be between 1 and 20, got ${nodeCount}`);
}
export const summary = `${dbName} x${nodeCount} in ${region}`;go deeper
Be able to write new pulumi.Config() and read a value, and say plainly that require fails the run on a missing key while get returns undefined so you can default it.
Explain the typed variants and why they beat hand parsing, how the namespace defaults to the project name, and why requireSecret is the one family returning an Output.
Show where the boundary sits in practice: which values belong in config versus derived in code, and validating at the top of the program so a bad value stops the run before any resource is registered.
Own the parameterisation contract across an estate — which keys every stack must define, how defaults in code interact with per-stack overrides, and how that shape stays reviewable as the number of stacks grows.
## Config is the program's parameters A Pulumi program is code, but it should not be edited to move between environments. Configuration is the parameterisation layer: the same program, different values, one set per stack. This entry is about the *program* side — how code reads those values. ```typescript import * as pulumi from "@pulumi/pulumi"; const config = new pulumi.Config(); const dbName = config.require("dbName"); const nodeCount = config.getNumber("nodeCount") ?? 3; ``` Python is the same shape: `config = pulumi.Config()`, `config.require("db_name")`, `config.get_int("node_count")`. Go uses `config.New(ctx, "")` with `cfg.Require` and `cfg.GetInt`. ## require vs get The distinction is about what happens when the key is absent. - **`require(key)`** throws a `ConfigMissingError` naming the key and the stack. The deployment stops before anything is created. Use it for every value that has no defensible default — the database name, the domain, the account you are targeting. - **`get(key)`** returns `undefined` (`None` in Python). Use it when the program has a real default and you want that default expressed in code, where it is reviewable, rather than duplicated into every stack's config. Failing loudly on a missing required value is worth more than it sounds: the alternative is a program that silently deploys with an empty string in a name, and you find out from the cloud's error message ten resources later. ## The typed variants Config values are stored as strings. Every method therefore has typed siblings that parse: - `requireNumber` / `getNumber` - `requireBoolean` / `getBoolean` - `requireObject<T>` / `getObject<T>` — for structured values, parsed from JSON or from structured config - `requireSecret` / `getSecret` — returns the value as a secret `Output<string>` rather than a plain string, so it stays marked secret through everything you derive from it Use the typed form rather than `parseInt(config.require("nodeCount"))`. The typed form gives you a clear error naming the key when the value is not a number; hand-rolled coercion gives you `NaN` and a confusing failure three resources downstream. Note that the secret variants are the one family that returns an `Output`, not a plain value — because the secret marker has to travel with the value, and Output is what carries that marker. ## Namespaces A bare `new pulumi.Config()` is namespaced to the current project's name, so it reads `myproject:dbName`. Constructing it with an explicit namespace reads someone else's: ```typescript const region = new pulumi.Config("aws").require("region"); ``` The `aws:` namespace is how the AWS provider's own settings are exposed, and the same pattern applies to other providers. The namespace also means two projects sharing a stack config file cannot collide on a key like `region`. ## Why plain values matter here Config values are known before any resource is registered. That makes them the correct thing to drive program structure with: ```typescript const env = config.require("environment"); if (env === "prod") { // extra replica, stricter retention, whatever prod needs } ``` This is legal and idiomatic precisely because `env` is a string, not an Output. The same `if` over a deployed resource's attribute would be a bug — the condition would be evaluating an Output object rather than its value. "Branch on config, never on Output" is one of the load-bearing rules of writing Pulumi programs, and it works because config lands on the plain-value side of the line. ## Validation There is no declarative constraint syntax on config the way there is on typed variable blocks in some other tools. You validate in code — which, since it is a real language, means a check and a thrown error, or a schema library if you want more: ```typescript const size = config.requireNumber("nodeCount"); if (size < 1 || size > 20) { throw new Error(`nodeCount must be between 1 and 20, got ${size}`); } ``` Throwing at the top of the program stops the deployment before a single resource is registered, which is exactly the behaviour you want from validation.
- Why is it fine to write an if statement over a config value but not over a resource attribute?Config is a plain value the program has before any resource is registered, so the condition evaluates normally. A resource attribute is an Output — a placeholder object — so `if (someOutput === "prod")` compares an object to a string and is never true. Branch on config; transform Outputs with apply.
- What does requireSecret return that require does not?A secret `Output<string>` rather than a plain string. The secret marker has to travel with the value so anything derived from it stays secret, and Output is the type that carries markers. That is why it is the one config family returning an Output rather than a plain value.
saying these in an interview costs you the question
- Hardcodes environment values in the program instead of config
- Uses get and then assumes the value is present
- Parses numbers by hand instead of using requireNumber
- Thinks a bare Config reads every namespace
- Believes config values are Outputs and need apply