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.