Adopt Pulumi ESC for config and secrets
Most Pulumi projects start with configuration and secrets stored directly in each stack’s Pulumi.<stack>.yaml file. That works, but the values are duplicated across every stack and project that needs them, and each copy has to be updated by hand. Moving those values into a Pulumi ESC environment gives you one source of truth that every stack imports.
This guide walks through moving a single stack’s configuration and secrets into an environment, then shows how to roll the change out across many stacks without a risky big-bang migration.
Prerequisites
- Pulumi CLI v3.99.0 or later installed
- A Pulumi account — ESC requires the Pulumi Cloud backend
- An existing Pulumi project whose stack uses the Pulumi Cloud backend
The starting point
Imagine a small project, myapp, that runs a containerized workload. Its dev stack needs one plaintext value (the region) and one secret (a database connection string). Today those live in Pulumi.dev.yaml:
config:
aws:region: us-west-2
myapp:containerImage: nginx:1.27
myapp:dbConnectionString:
secure: v1:9RmvK2v6Nlc8Xtbp:0Vq2b...
The program reads them with the standard Configuration API:
import * as pulumi from "@pulumi/pulumi";
const config = new pulumi.Config();
const containerImage = config.require("containerImage");
const dbConnectionString = config.requireSecret("dbConnectionString");
import pulumi
config = pulumi.Config()
container_image = config.require("containerImage")
db_connection_string = config.require_secret("dbConnectionString")
conf := config.New(ctx, "")
containerImage := conf.Require("containerImage")
dbConnectionString := conf.RequireSecret("dbConnectionString")
var config = new Pulumi.Config();
var containerImage = config.Require("containerImage");
var dbConnectionString = config.RequireSecret("dbConnectionString");
var config = ctx.config();
var containerImage = config.require("containerImage");
var dbConnectionString = config.requireSecret("dbConnectionString");
config:
containerImage:
type: string
dbConnectionString:
type: string
secret: true
Nothing in your program changes when you move these values to ESC: they still arrive as stack configuration.
Move config and secrets into an environment
The Pulumi CLI can convert a stack’s configuration into a new environment in one step. From your project directory, run:
pulumi config env init --stack dev
This creates an environment named after the stack (myapp/dev by default), copies the stack’s configuration and secrets into its pulumiConfig block, and replaces the values in Pulumi.dev.yaml with a reference to the new environment. Secrets stay encrypted the whole way. They’re re-encrypted under ESC rather than exposed.
Afterward, Pulumi.dev.yaml contains only the environment reference:
environment:
- myapp/dev
And the new myapp/dev environment holds the values:
values:
pulumiConfig:
aws:region: us-west-2
myapp:containerImage: nginx:1.27
myapp:dbConnectionString:
fn::secret:
ciphertext: ZXNjeAAAAAEAAAEAe3jKq...
--keep-config if you’d rather leave the values in the stack file while you test the environment, and remove them yourself once you’re confident. Use --env <name> to choose an environment name other than <project>/<stack>.To do the same thing by hand (for example, when you want the values in a shared environment rather than a per-stack one), create the environment with pulumi env init, add the values under pulumiConfig with pulumi env edit, and add the environment block to your stack file yourself.
Pin the environment
By default, a stack imports the latest revision of an environment. That means a change to the environment takes effect on your next pulumi up with no change to the stack itself. That’s convenient, but it also means an unrelated edit can alter a stack you weren’t touching.
For anything beyond experimentation, pin the import to a specific version so updates are deliberate. Append a revision number or a version tag with @:
environment:
- myapp/dev@1
Using a named tag (for example @production) instead of a raw revision number lets you move the tag forward when you’re ready to promote a new revision, without editing every stack that imports it. See Environment versioning for how revisions and tags work.
Verify end to end
Confirm the values resolve as expected before and after wiring them into the stack:
Open the environment directly and check the resolved values, including the decrypted secret:
pulumi env open myapp/devPreview the stack and confirm Pulumi sees the same configuration it did before the move:
pulumi preview --stack devThe plan should be a no-op: the program receives identical inputs, just sourced from ESC instead of the stack file.
Run
pulumi upto apply.
Adopt safely at scale
Once a single stack works end to end, resist the urge to convert everything at once. The reliable pattern is get one path working, then fan out:
Prove it on one non-production stack. Convert a single low-risk stack (like
dev), verify a no-op preview, and deploy. This validates your environment structure and backend setup with nothing important on the line.Factor shared values into a common environment. Values that are identical across stacks (a region, a registry, a shared API endpoint) belong in one environment that the others import, so you define them once:
# myapp/common values: pulumiConfig: aws:region: us-west-2 myapp:containerImage: nginx:1.27Each stack’s environment imports
commonand adds only what’s specific to it:# myapp/prod imports: - myapp/common values: pulumiConfig: myapp:dbConnectionString: fn::secret: ${prodDbConnectionString}Promote to production as a series of no-ops. For each remaining stack, make the switch a preview-clean change: move its values into ESC, confirm
pulumi previewshows no diff, then deploy. A migration that never changes a plan is one you can roll out with confidence.Pin every production import. Non-production stacks can track
@latestfor convenience; production stacks should pin to a version or tag so environment changes reach them only when you promote.
Following this order (one stack, then shared values, then a no-op rollout), you can move an entire portfolio onto ESC incrementally, with a working, verifiable state at every step.
Next steps
- Migrate from stack references to
pulumi-stacks- Apply the same incremental approach to cross-stack outputs - Importing environments - Compose shared and per-stack configuration
- Environment versioning - Pin and promote environment revisions
- Dynamic secrets - Replace static secrets with values pulled from external stores at open time