← Back to Patterns

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.

By Ivan Richter LinkedIn

Last updated: Mar 23, 2026

5 min read

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 all

Related patterns