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.
On this page
The rule
We use Pulumi stack config for environment-specific values, not for describing the behavior of the system.
That distinction matters more as a project grows. Early on, config tends to hold harmless inputs. Region. Domain. Sizing. Retention. Then the stack starts getting more complicated and config begins absorbing things that are not really values anymore. Feature toggles. Branches. Resource switches. Special cases. The repo still looks neat. The actual deployment model gets harder to see.
We do not want stack config turning into a second place where the program is defined.
A useful line to keep in mind is this:
- config answers what value this environment uses
- code answers what this environment does
What belongs in stack config
Stack config is a good place for values that are specific to one stack and easy to understand as inputs.
That usually includes domain names, CIDR ranges, regions, instance sizes, retention periods, billing labels, and other small environment-specific limits. These values may differ between dev, stg, and prd, but they do not change the shape of the program in a way that needs explanation.
Good stack config reads like data. You open it and immediately understand what is different about that stack without having to infer a whole control flow from the keys.
One simple test works well here: if the key means “this stack uses a different value for the same thing,” it probably belongs in the stack config.
What stays in code
Behavior.
If one environment creates an extra integration, applies stricter protection, uses a different workflow, or changes what gets deployed, that should usually be visible in the program itself. Not because code is somehow virtuous. Because behavior is easier to review when it lives where the execution shape is actually defined.
A real language only pays off if the important behavior is still visible in code, not pushed into a growing pile of config keys that all influence the outcome indirectly.
Production should not be “special” only because some YAML file quietly turned on an extra branch nobody noticed in review.
What belongs in secret systems instead
Not every environment-specific value belongs in stack config.
Secrets usually belong in the secret system that already owns them, not in an expanding pile of stack YAML. Pulumi’s secret support is useful, but that does not mean stack config should become the default home for credentials, tokens, connection strings, or every other sensitive value the system touches.
The question is not only whether Pulumi can encrypt the value. The question is whether the value belongs in the config surface of the stack at all.
If a secret is managed by the cloud platform, rotated there, consumed by multiple systems, or already handled through provider-native tooling, we usually keep it there and reference it from the program. That keeps the ownership model clearer and avoids turning stack config into a secret dump with nicer formatting.
What we avoid
We avoid using config as a substitute for making a decision in code.
We avoid config keys that really mean “create this extra thing” or “change the behavior of the system in this stack” when that behavior is important enough that it should be visible in the program. We avoid huge stack files full of vague keys that only make sense if you already know how the stack evolved. We avoid hiding structure behind flags. That is often a sign repeated Pulumi code was abstracted before the shared boundary materialized.
We also avoid repeating shared defaults in every stack file by hand. If the value is genuinely common, it usually belongs in shared code or a shared component, not copied across dev, stg, and prd forever just because YAML is cheap to duplicate.
And we avoid stack config that starts behaving like a product settings panel. Once config feels more expressive than the code, the boundary is usually wrong.
A split that stays readable
A simple split works well most of the time:
- stack config holds values
- code holds behavior
- shared modules hold reusable patterns
- secret systems hold actual secrets
That is not some universal law. It is just a boundary that keeps the repo readable.
If prd uses a larger database size, that fits naturally in stack config. If prd gets deletion protection, stricter backup policy, or an extra integration, that should usually be visible in the environment entry point or the shared code it calls. If a credential is already owned somewhere else, it should usually stay there.
When those boundaries stay clear, the repo is easier to read. Stack config tells you what varies by environment. Code tells you what the system actually does.
What this looks like in practice
A healthy stack config file is usually small:
config:
gcp:project: anriku-public-dev
gcp:region: europe-west1
app:domain: dev.anriku.com
app:dbInstanceClass: db-custom-1-3840
app:backupRetentionDays: 7
app:budgetMonthlyEur: 50 That is straightforward. It tells you which values this stack uses.
What we do not want is something more like this:
config:
app:enableProdOnlyIntegration: true
app:useStrictDeletionProtection: true
app:createFailoverReplica: false
app:extraMonitoringMode: full
app:workflowShape: extended
app:networkModel: isolated At that point config is no longer just supplying inputs. It is starting to define how the system behaves.
That does not mean every boolean is bad. It means the more stack config starts reading like a hidden control plane, the more likely it is that the boundary has drifted.
How this fits with directory per environment
This matters even more once we keep a directory per environment.
That structure is supposed to make the environment boundary easy to see. If the directory looks clean but the real differences are buried in stack config, then the structure is only doing half the job. The filesystem says “the environment is defined here” while the actual behavior is still scattered.
That is why we keep stack config narrow. The environment entry point should still be able to tell the story of that environment. Config should support that with values, not replace it with indirection.
The point
Pulumi stack config is useful when it stays boring.
It should carry inputs, not hidden behavior. It should make environment-specific values easy to override without making the stack harder to understand.
We keep values in config. We keep behavior in code. We keep secrets where they are actually owned.
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.
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.
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.
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.