Pulumi stack config: values, code, and secrets
Stack config supplies values within an infrastructure design. Keep topology and operating policy explicit in code, validate the inputs, and manage secrets according to who owns their lifecycle.
We use stack config for values operators need to vary within an infrastructure design. Keep topology and operating policy explicit in code, close to the environment that owns them. Reviewers should be able to see which resources exist and which protections apply without interpreting a growing set of configuration switches.
A production stack may need a regional database while development uses a zonal one. We’d put those choices in their environment entry points or named policy functions. Shared resource construction can still live in a helper; the caller makes the consequential choices.
References to resources owned by other stacks come from explicit outputs where practical. Secrets follow their own ownership and rotation requirements rather than being grouped with ordinary environment values.
Choose where a value belongs
| Value or rule | Usual home | Reason |
|---|---|---|
| Region, project, capacity limit | Stack config. | The deployment owner can vary it within the chosen design. |
| Resource relationships, availability, and required protections | Infrastructure code. | The environment's architecture and operating requirements need to be explicit. |
| Identifier exported by another stack | A stack reference or another explicitly defined interface. | Avoid copying a value whose owner can change it. |
| Application credential rotated outside deployment | The application's secret-management process. | Rotation shouldn't depend on editing unrelated infrastructure inputs. |
Expose an architectural choice in config only when operators need to select independently between supported, tested alternatives. A single bounded capability can justify an option. Several flags that jointly define the resource graph usually call for an explicit environment definition instead.
Keep the namespace and validation obvious
Pulumi stores per-stack configuration in files such as Pulumi.prod.yaml. Configuration keys have a namespace; in TypeScript, new pulumi.Config() uses the project namespace by default, while an explicit argument selects another one. Pulumi’s configuration guide explains that behavior.
For example, this stack uses the GCP provider namespace for its project and a platform namespace for application-specific deployment inputs:
config:
gcp:project: example-production
platform:region: europe-west1
platform:minInstances: 1
platform:maxInstances: 8Read and validate those inputs once near the program’s entry point. This TypeScript example validates configuration only; resource construction would use the exported object:
import * as pulumi from '@pulumi/pulumi';
const gcpConfig = new pulumi.Config('gcp');
const config = new pulumi.Config('platform');
const project = gcpConfig.require('project');
const region = config.require('region');
const minInstances = config.requireNumber('minInstances');
const maxInstances = config.requireNumber('maxInstances');
if (!project.trim() || !region.trim()) {
throw new Error('Project and region must not be blank');
}
if (!Number.isSafeInteger(minInstances) || minInstances < 0) {
throw new Error('minInstances must be a nonnegative integer');
}
if (!Number.isSafeInteger(maxInstances) || maxInstances < 1) {
throw new Error('maxInstances must be a positive integer');
}
if (minInstances > maxInstances) {
throw new Error('minInstances must not exceed maxInstances');
}
export const deploymentConfig = {
project,
region,
minInstances,
maxInstances,
};A numeric getter establishes a number, not every property the deployment needs. Check valid ranges, integer requirements, supported combinations, and any environment policy. If a deployment requires regional availability, a configurable availability setting must reject zonal there. Accepting both strings wouldn’t enforce that requirement.
For structured inputs, requireObject<T>() parses JSON; the TypeScript generic doesn’t validate an arbitrary object against your interface at runtime. Validate the returned structure. Pulumi’s Config API reference describes the available getters and constraints.
Don’t add the same validation throughout the resource definitions. Normalize the input once, then let the rest of the program work with the validated shape.
Defaults should reflect a deliberate operating choice
A shared default can keep several stacks consistent. Requiring every environment to repeat a value that nobody should vary creates unnecessary places for drift. Put that default in code when it genuinely belongs to the shared implementation, and expose an override only when someone needs to own it.
Require inputs explicitly when a missing value could select the wrong project or remove a capacity limit. Put required availability and protection settings in the environment’s code. Don’t silently fall back to development settings because a production key was misspelled.
Pass the environment’s policy explicitly to shared resource code. Scattered checks such as getStack() === 'prod' make required protections harder to trace and complicate a temporary stack that needs the same protections under a different name. Keep any profile selection at the environment entry point, then preview the resulting infrastructure.
Encryption doesn’t settle secret ownership
Pulumi supports encrypted secret configuration and tracks secret outputs through derived values. That is useful when the infrastructure program needs a credential to provision a resource. Read such inputs with requireSecret or the corresponding secret getter and preserve their secret status. Pulumi’s secrets documentation explains the encryption and propagation model.
An encrypted value in Pulumi.prod.yaml can be version-controlled as part of that design; it isn’t the same as committing a plaintext credential. Access to the repository, state backend, and secrets provider still needs to match the sensitivity of the deployment.
For an application API key rotated by a separate operational process, we’d often have Pulumi manage the secret resource and access policy while that process manages the payload. The runtime then reads the secret through its supported integration. This keeps routine credential rotation from requiring a change to unrelated infrastructure code or config.
Be explicit about which system owns the payload. Creating the secret version through Pulumi can put the value into Pulumi’s state, even when it is encrypted there. And a secret inside an apply callback is still available as plaintext to that code: logging it or writing it to an unprotected file defeats output redaction.
Don’t copy values with an existing owner
Use resource outputs for identifiers created in the same program. For resources managed by another stack, Pulumi stack references can expose a small, intentional set of outputs. Copying a generated endpoint into three stack files creates three additional updates the next time it changes.
Some external identifiers have no suitable automated source and belong in config. Record what owns them and when they must change. Avoid exposing an entire neighboring stack’s internals as a convenient integration: the dependency should be on the few outputs the consumer actually needs.
Review configuration changes through the resource preview as well as the YAML diff. One changed word can replace infrastructure or alter availability. Remove obsolete keys when their code disappears, and add an explicit unknown-key check where silent leftovers have caused problems.
More in this domain: Infrastructure
Browse allCloud Tasks vs. Pub/Sub for Cloud Run: routing, retries, and rate limits
Use Cloud Tasks for a specific operation that needs scheduling or controlled dispatch. Use Pub/Sub when independent consumers need the same event. Either HTTP handler must tolerate retries.
Cloud Run scale to zero: cold starts and minimum instances
Keep scale to zero when the first request can wait. Pay for minimum instances when measured startup latency hurts the service, and decide background CPU separately.
Direct VPC egress vs. Serverless VPC Access for Cloud Run
Use Direct VPC egress for most new Cloud Run connections to a VPC. Check address capacity, startup behavior, and outbound routing before retiring a working connector.
Internal-only Cloud Run: ingress, identity, and the caller’s network path
An internal Cloud Run service needs an allowed request path and an authorized caller. The correct setup differs for Cloud Tasks, another Cloud Run service, and human access.
How we decide between Cloud SQL connectors, Auth Proxy, and private IP
Cloud SQL connectors, the Auth Proxy, and private IP are not interchangeable secure connection options. They change identity, routing, deployment shape, and how much network plumbing the team actually owns.
Related patterns
How we structure a directory per environment in Pulumi
When we keep Pulumi environments separate, we make the environment boundary obvious in the filesystem and keep shared logic outside it.
When repeated Pulumi code earns abstraction and when it doesn't
We don't abstract repeated Pulumi code just because it shows up more than once. We do it when the shared shape is real, the behavior is stable enough to deserve a boundary, and the result is easier to read than the duplication it replaces.
How we decide between directory per environment and shared stacks in Pulumi
We do not force DRY across environments by default. We keep Pulumi environments separate until shared code, shared rules, and drift risk make consolidation cheaper than duplication.
Why we usually choose Pulumi over Terraform
Pulumi is our default when infrastructure starts behaving like software. Existing Terraform estates can still be the better decision when the migration cost is higher than the operational gain.