Skip to main content
  1. Docs
  2. Secrets & Configuration
  3. Guides
  4. Use with Pulumi IaC
  5. Adopt ESC for config and secrets

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.

    This guide is for existing Pulumi IaC users. If you’re new to the ESC/Pulumi IaC integration, read Use with Pulumi IaC first for the mental model, and the ESC Get Started guide for a from-scratch walkthrough.

    Prerequisites

    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...
    
    Pass --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:

    1. Open the environment directly and check the resolved values, including the decrypted secret:

      pulumi env open myapp/dev
      
    2. Preview the stack and confirm Pulumi sees the same configuration it did before the move:

      pulumi preview --stack dev
      

      The plan should be a no-op: the program receives identical inputs, just sourced from ESC instead of the stack file.

    3. Run pulumi up to 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:

    1. 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.

    2. 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.27
      

      Each stack’s environment imports common and adds only what’s specific to it:

      # myapp/prod
      imports:
        - myapp/common
      values:
        pulumiConfig:
          myapp:dbConnectionString:
            fn::secret: ${prodDbConnectionString}
      
    3. 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 preview shows no diff, then deploy. A migration that never changes a plan is one you can roll out with confidence.

    4. Pin every production import. Non-production stacks can track @latest for 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