← Back to Patterns

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.

By Ivan Richter LinkedIn

Last updated: Oct 2, 2026

5 min read

On this page

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

A practical split for infrastructure projects
Value or ruleUsual homeReason
Region, project, capacity limitStack config.The deployment owner can vary it within the chosen design.
Resource relationships, availability, and required protectionsInfrastructure code.The environment's architecture and operating requirements need to be explicit.
Identifier exported by another stackA stack reference or another explicitly defined interface.Avoid copying a value whose owner can change it.
Application credential rotated outside deploymentThe 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: 8

Read 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 all

Related patterns