Pulumi + SOPS

2025/06/17

Secrets can be provided to Pulumi programs via environment variables, configuration files, or Pulumi ESC. However, you might prefer to manage secrets externally for use in other programs. While I use pass for interactive secret management, I use SOPS for automation due to its text-based format and relative simplicity.

One method for passing secrets from SOPS is with the sops exec-env command, which adds secrets to the environment before executing a program. Pulumi can then read them with process.env["TOKEN"]. However, this might return null, causing the program to fail or, worse, producing unintended resource changes (e.g., setting a password to null 😱).

A superior approach leverages the structured (YAML) format in which SOPS stores secrets. Extract a schema from the secret file and convert it to TypeScript definitions:

npm install quicktype
sops -d --output-type json ../secrets.yaml | quicktype -t Secrets --lang ts > secrets.ts

For convenience, add this invocation to package.json:

{
  "scripts": {
     "secrets": "sops -d ..."
  }
}

In your Pulumi program, read secrets from SOPS and parse them into properly typed classes:

import { Convert } from './secrets';
import { execSync } from 'node:child_process';
import * as cloudflare from '@pulumi/cloudflare';

const secretsJson = execSync('sops -d --output-type json ../secrets.yaml');
const secrets = Convert.toSecrets(secretsJson.toString('utf8'));

const cloudflareProvider = new cloudflare.Provider('default', {
  apiToken: secrets.cloudflare.API_TOKEN,
});

Quicktype’s converter also validates the data, raising an error if a field is missing. This eliminates unexpected null values and improves the overall structure of your program.