Skip to main content
  1. Docs
  2. Secrets & Configuration
  3. Guides
  4. Sync secrets to external platforms

Sync ESC secrets to external platforms

    Traditional secret management leads to secret sprawl: the same secret is duplicated across repositories, cloud providers, and CI/CD pipelines, and every change has to be applied by hand in each place. This guide shows a pattern that uses Pulumi ESC together with Pulumi IaC and Pulumi Deployments to define secrets and configuration once in ESC and automatically push them out to the external platforms where they’re consumed.

    This guide is for existing Pulumi IaC and ESC users. If you’re new to ESC, start with the ESC Get Started guide. To consume an environment from a Pulumi program, see Integrate ESC with Pulumi IaC.

    How it works

    You define the values to sync in an ESC environment, then run a Pulumi program on a schedule (or in response to a webhook) that reads those values and writes them into the target platform. ESC is the single source of truth; Pulumi IaC does the syncing.

      flowchart LR
        A[Imported ESC environment] --> B["Sync ESC environment<br/>(sync block)"]
        B --> C{Trigger}
        C -->|Scheduled| D[Pulumi Deployment]
        C -->|On change| E[Webhook]
        E --> D
        D --> F[AWS Secrets Manager]
        D --> G[Azure Key Vault]
        D --> H[GCP Secret Manager]
        D --> I[GitHub secrets]
        D --> J[HashiCorp Vault]
    

    Prerequisites

    Define the secrets to sync

    Create an ESC environment that imports the environment holding your source values and declares a sync block describing what to push and where. In this example, the values are synced to AWS Secrets Manager:

    imports:
      - my-project/my-imported-env@stable
    values:
      sync:
        awsSecretsManager:
          value:
            myConfigKey: ${my-imported-env-foo}
            myNestedKey:
              haha: ${my-imported-env-bar}
            mySecret: ${my-imported-env-password}
          name: name-in-secrets-manager
    

    The value field contains the data to sync, and name is the name of the secret to create in the target platform.

    Automate the sync

    Next, define a Pulumi program that provisions the ESC environment, the target stack, and the Pulumi Deployments settings for that stack. The pre-run commands extract the values from the environment and set them as stack configuration, and a deployment schedule runs the sync on a recurring basis (hourly by default):

    import * as pulumi from "@pulumi/pulumi";
    import * as service from "@pulumi/pulumiservice";
    
    const config = new pulumi.Config();
    const orgName = pulumi.getOrganization();
    
    // projectName is the name of the project where the target stack is located
    const projectName = config.require("projectName");
    
    // stackName is the name of the target stack
    const stackName = config.get("stackName") || "dev";
    
    // repository is the GitHub repository where the target stack is located (for deployment settings)
    const repository = config.require("repository");
    
    // how often you want to sync. Default is hourly
    const syncCronSchedule = config.get("syncCronSchedule") || "0 * * * *";
    
    // envPath is the path to the environment file that contains the secrets or configuration to be synced
    const envPath = config.get("envPath") || "syncEnv.yaml";
    
    const env = new service.Environment("env", {
        organization: orgName,
        project: projectName,
        name: stackName,
        yaml: new pulumi.asset.FileAsset(envPath),
    });
    
    const stack = new service.Stack("esc-sync-aws-secretsmanager", {
        organizationName: orgName,
        projectName,
        stackName,
    });
    
    const fullyQualifiedStackName = pulumi.interpolate`${orgName}/${projectName}/${stackName}`;
    const fullyQualifiedEnvName = pulumi.interpolate`${orgName}/${projectName}/${env.name}`;
    
    const settings = new service.DeploymentSettings("deployment_settings", {
        organization: orgName,
        project: stack.projectName,
        stack: stack.stackName,
        github: {
            repository,
        },
        sourceContext: {
            git: {
                branch: "main",
                repoDir: "sync/target",
            },
        },
        operationContext: {
            preRunCommands: [
                "pulumi login",
                pulumi.interpolate`pulumi config env add ${projectName}/${env.name} -s ${fullyQualifiedStackName} --yes`,
                pulumi.interpolate`pulumi env open ${fullyQualifiedEnvName} sync.awsSecretsManager.value > sync.json`,
                pulumi.interpolate`pulumi config set -s ${fullyQualifiedStackName} secretName $(pulumi env open ${fullyQualifiedEnvName} sync.awsSecretsManager.name)`,
            ],
        },
    });
    
    const schedule = new service.DeploymentSchedule("update_schedule", {
        organization: orgName,
        project: settings.project,
        stack: settings.stack,
        scheduleCron: syncCronSchedule,
        pulumiOperation: "update",
    });
    

    Create the external secret

    The target stack is a separate Pulumi program that reads the synced values and writes them to the external platform. Here, it creates an AWS Secrets Manager secret from the sync.json file produced by the pre-run commands above:

    import * as pulumi from "@pulumi/pulumi";
    import * as aws from "@pulumi/aws";
    import * as fs from "fs";
    
    const config = new pulumi.Config();
    const name = config.require("secretName");
    
    // Read a json file from the local filesystem using node.js fs module
    const json = fs.readFileSync("sync.json", "utf8");
    
    const secret = new aws.secretsmanager.Secret(name, {
        recoveryWindowInDays: 0,
    });
    
    const secretVersion = new aws.secretsmanager.SecretVersion(`${name}-version`, {
        secretId: secret.id,
        secretString: json,
    });
    
    // Export the name of the secret
    export const secretName = secret.name;
    

    Because the deployment runs on a schedule, the secret stays up to date in AWS Secrets Manager whenever the source values change in ESC.

    Sync on change with webhooks

    Instead of a fixed schedule, you can sync in near real time by triggering a deployment whenever the imported ESC environment changes. Replace the DeploymentSchedule above with a webhook that fires on environment changes:

    const webhook = new service.Webhook("webhook", {
        organizationName: orgName,
        projectName: settings.project,
        stackName: settings.stack,
        displayName: "Sync to AWS Secrets Manager",
        environmentName: pulumi.interpolate`${env.project}/${env.name}`,
        filters: [service.WebhookFilters.ImportedEnvironmentChanged],
        format: service.WebhookFormat.PulumiDeployments,
        payloadUrl: pulumi.interpolate`${stack.projectName}/${stack.stackName}`,
        active: true,
    });
    

    When the imported environment is updated, the webhook triggers a deployment that syncs the new values to the target platform.

    Sync to other platforms

    The same pattern works for other targets — adjust the sync block and the target stack’s resources accordingly. The Pulumi ESC examples repository includes working examples for:

    Next steps