What goes in Pulumi stack config and what doesn't
We use Pulumi stack config for environment-specific values, not as a hiding place for infrastructure logic.
Pulumi stack config should supply values to a program whose behavior is already legible in code.
If understanding what production creates requires interpreting dozens of config switches, the YAML file has become an untyped second program. Pulumi can execute that design perfectly. That doesn’t make it a good design.
Put environment values in config
Good stack values vary within one known infrastructure shape:
- project, account, subscription, and region identifiers
- domains and DNS zones
- CIDR ranges
- machine sizes and counts
- retention periods
- concurrency or scaling limits
- budgets and labels
- references to externally owned resources
- feature versions when version is genuinely an input
config:
gcp:project: anriku-public-prd
platform:region: europe-west1
platform:domain: anriku.com
platform:databaseTier: db-custom-1-3840
platform:backupRetentionDays: 30
platform:maxInstances: 10A reader can compare stacks and understand the values that differ without simulating topology.
Use typed accessors and validation. Required production values should fail closed rather than inherit a convenient development default.
Keep behavior in code
Code should show which resources exist, how they relate, and which policies apply.
If production creates a replica, uses deletion protection, or connects to an external integration, make that decision visible in the environment entry point or a named policy function. Avoid config keys such as:
platform:enableSeriousProductionMode: true
platform:useAlternateNetworkShape: true
platform:createExtraSecurityThings: trueThe names are absurd, but only slightly more honest than the flags they parody.
A Boolean can be valid when it represents one bounded capability with a clear lifecycle. It becomes suspicious when toggling it changes several unrelated resources or compensates for a component that never found a stable contract.
Prefer an explicit policy object or separate entry point when environments have materially different shapes.
Put shared defaults in code
A value identical across stacks is usually a default or invariant, not stack configuration.
Define common labels, naming rules, log retention, baseline IAM, supported regions, and standard resource settings in reviewed code. Override them only when a stack has a real reason. Copying the same value through every YAML file creates manual drift and makes an invariant look optional.
The default should remain visible and domain-specific. Hiding every provider option in a generic config layer merely recreates the provider schema with worse documentation.
Secrets follow ownership, not encryption capability
Pulumi secret config encrypts values in stack state and configuration. Use it when the secret is a deployment input owned by that stack.
Prefer provider-native secret systems when the secret has its own rotation, audit, access, or multi-consumer lifecycle. Pulumi often needs only the secret resource ID and permission binding, not the payload.
config:
platform:paymentSecretId: projects/example/secrets/payment-api-keyThe application reads the secret at runtime. Pulumi grants access. The payload doesn’t need to pass through deployment outputs or state.
Don’t place credentials in plain config because the repository is private. Private repositories have an impressive history of becoming less private at inconvenient times.
Avoid derived values in config
Resource names, URLs, service-account emails, and connection strings derived from other inputs should usually be computed in code or exported from the resource that owns them.
Duplicating a derived value in config lets it disagree with reality. Cross-stack references or explicit provider lookups are preferable when another state owns the resource.
Keep cross-stack dependencies narrow. A shared network stack can export subnet IDs. It shouldn’t expose a sprawling object that couples consumers to its internal implementation.
Version config contracts
Renaming a key, changing units, or altering allowed values is an interface change. Validate it and migrate stacks deliberately.
Use domain namespaces, descriptive names, and units:
platform:requestTimeoutSeconds
platform:backupRetentionDays
platform:monthlyBudgetEurAvoid ambiguous keys such as size, timeout, and mode. They save characters and spend review time indefinitely.
CI should load and validate every production stack. Unused keys should fail linting or be removed. Dead config is dangerous because readers assume it still controls something.
Don’t let stack names become hidden config
Using the stack name for naming is reasonable. Using it deep inside components to infer protection, networking, replicas, and access policy isn’t.
Pass consequential values from the environment boundary. This keeps a preview understandable and permits a production-like temporary stack without tricking components through its name.
Keep responsibilities separate
Stack config answers which value this stack uses. Code answers what the platform does with that value. Shared modules own stable mechanics. Secret systems own credentials with independent lifecycle.
Move a setting into config when operators need to vary it without changing infrastructure behavior. Move it back into code when the value has become policy or when several flags jointly define another topology.
Stack config is most useful when it’s boring enough to compare at a glance. Once it becomes more expressive than the program, reviewers can no longer see the behavior the program was supposed to own.
More in this domain: Infrastructure
Browse allHow 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.
Safe scaling defaults for Cloud Run + Postgres
Cloud Run autoscaling is not a database strategy. Safe defaults keep the application from scaling itself into a Postgres incident before the team understands the workload.
IAM DB auth for Cloud SQL: when it simplifies security and when it complicates delivery
IAM DB auth can reduce password sprawl and make revocation cleaner, but it also turns database access into an identity operating model that depends on disciplined service-account boundaries.
Cloud Run request timeouts don't kill your code (so your architecture has to)
A Cloud Run request timeout ends the request, not necessarily the work. If the operation can outlive its caller, the system needs explicit job semantics instead of hope.
Cloud Run scaling from zero is a feature until it isn't
Scale to zero is a good default for request-driven services, until startup delay, warm-capacity needs, or instance caps turn it into user-visible reliability behavior instead of a pricing feature.
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.