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.
Use separate environment programs while differences are important enough to review directly. Consolidate into shared stacks when the common shape is stable and duplicate change has become the larger risk.
Review visibility matters more than DRY. Choose the structure that exposes environment behavior with the least hidden state.
Separate directories preserve local truth
A directory per environment gives dev, stg, and prd their own entry points. A reviewer can open production and see what it creates, how it’s protected, and which integrations exist there without evaluating conditionals for every other stack.
This is useful when environments are intentionally asymmetric:
- development omits expensive persistent services
- staging exists only for selected products
- production has stricter recovery, deletion, or networking policy
- one environment integrates with a real external system while another uses a stub
- environments are created or retired independently
Some duplication is the price of making those differences explicit. For a small estate, that price is often lower than a generalized program filled with if (stack === 'prod') branches and configuration switches.
Separate code also narrows blast radius. A production change can remain a production change instead of modifying a shared function consumed by every stack.
Shared stacks preserve invariants
One Pulumi program with several stacks becomes useful when environments are meant to instantiate the same platform shape with different values.
The common model may include:
- the same service and identity relationships
- the same networking topology
- the same observability and policy baseline
- the same database lifecycle
- the same deployment sequence
- environment variance limited mostly to size, names, domains, retention, and counts
At that point, separate programs create another risk: one fix lands in production but not staging, an IAM rule diverges, or a new resource is copied with slightly different defaults. Review becomes manual comparison across directories.
Shared stacks centralize the invariant. Stack config supplies the values. A change to the platform shape is made once and previewed against every affected stack.
Count semantic branches, not repeated lines
A shared program is healthy when differences can be stated as a small set of typed inputs. It’s unhealthy when config and code jointly form an environment rule engine.
Watch for:
- Boolean options that create materially different topologies
- resources present only in one stack through scattered conditionals
- stack names used deep inside shared components
- defaults that silently weaken non-production environments
- arrays of overrides whose meaning depends on caller history
- a preview that can’t be understood without knowing every branch
The line count may be lower while the cognitive load is higher. That’s compression, not simplification.
Likewise, separate directories are failing when every meaningful change must be copied into several files and reviewers regularly miss one. Visible duplication has stopped buying local clarity and started manufacturing drift.
Cloud cost affects the environment model
Don’t create full infrastructure parity solely because shared-stack code makes it easy.
A small company may need a production database, ephemeral development resources, and no permanent staging database. The environment contract should reflect the testing and release requirement, not symmetry for its own sake.
Code architecture and deployed topology are separate decisions. Shared modules can support an intentionally smaller development program. A shared stack can take a size of zero only when zero remains a legible, supported state rather than a forest of skipped resources.
Repository symmetry isn’t worth paying for infrastructure the release process doesn’t need.
Migration in either direction should be mechanical
To consolidate separate directories:
- identify behavior that is genuinely invariant
- extract small shared components without changing resources
- align naming and configuration contracts
- move one environment into the shared program
- verify previews preserve resource identity
- move the remaining environments one at a time
Don’t combine consolidation with broad resource replacement. Use aliases, imports, or state moves where identities change.
To split a shared program, first isolate stack-specific branches into named boundaries. Create separate entry points that call the same components, then allow them to diverge deliberately. Preserve state ownership during the move.
The infrastructure result should remain stable while the code representation changes.
Review every environment affected by shared code
A shared component change has a multi-stack blast radius even when the pull request was motivated by one environment. CI should compile and preview every relevant stack, summarize replacements and deletes, and make stack-specific differences visible.
Separate directories need a different guardrail: automation that detects versions or policies intended to stay aligned. Explicit code doesn’t excuse accidental drift.
How we choose
Choose a directory per environment when there are few environments, their topologies differ meaningfully, and local review matters more than centralization.
Choose shared stacks when environments implement the same platform contract, common changes are frequent, and duplicate maintenance creates measurable drift or review cost.
Revisit the choice when branches or copying start dominating changes. The representation should follow the real invariant. DRY code is useful only after the environments have agreed on what they are.
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.
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.
Related patterns
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.
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.
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.
How we treat Terraform state in team environments
Terraform starts feeling fragile in teams when state is treated like a backend setting instead of a shared dependency for safe change.