Configuration and Secrets
Configuration and Secrets
A stack differs from its siblings in exactly two places: its state and its configuration. Configuration is how one program becomes a t3.micro in dev and a m7i.large in production without a single if. Some of that configuration is a password, and Pulumi’s secrets system makes sure the password is encrypted in every place it travels to, not just the file you typed it into.
pulumi was not available on the authoring box and installs were not allowed, so no commands here were executed. Commands, file layouts and listings follow the current Pulumi documentation. Listings such as [secret] masking are reproduced from the docs and marked as such. Examples use Python.
Learning goals
- Set, read and structure stack configuration, and explain key namespaces
- Choose between
get,requireand the*_secretvariants, and between stack-level and project-level config - Explain how secret-ness propagates through outputs, and where it does not (resource IDs,
applycallbacks) - Pick and change a secrets provider, including for DIY backends
Configuration: per-stack key/value pairs
Values live in Pulumi.<stack>.yaml next to Pulumi.yaml, and you commit that file. The values drive the program, so they belong in version control alongside it.
pulumi config set aws:region us-west-2 # provider namespace
pulumi config set instanceCount 3 # project namespace (implicit)
pulumi config get instanceCount
pulumi config # list; --json for machines
cat id_ed25519.pub | pulumi config set publicKey # stdin: no shell escapingconfig set overwrites an existing value without warning. Review the diff of Pulumi.<stack>.yaml before committing.
Keys and namespaces
Keys are [namespace:]name. With no colon, the namespace is the project name. That is how aws:region and your own region coexist. In the stack file the result looks like:
config:
aws:region: us-west-2
webserver:instanceCount: "3"In code, pulumi.Config() reads the project namespace and pulumi.Config("aws") reads another one. Reusable components should read their own namespace (pulumi.Config("mylib")) so they never collide with the consuming project.
Reading from code
import pulumi
config = pulumi.Config()
name = config.require("name") # missing -> deployment stops with an explanatory error
count = config.get_int("instanceCount") or 1 # missing -> None, so default it
db_password = config.require_secret("dbPassword") # Output, marked secret
aws_region = pulumi.Config("aws").require("region")Use require for anything without a sensible default. A typo in a key should fail the preview, not quietly create one instance in production. Configuration is read-only at run time. Writing config from code is the job of the Automation API.
Environment variables also reach the program (os.environ), but configuration is per-stack, versioned and visible to collaborators, so prefer it.
Structured values
--path treats the key as a path into an object:
pulumi config set --path 'data.active' true
pulumi config set --path 'data.nums[0]' 1
pulumi config set --path 'data.nums[1]' 2config:
proj:data:
active: true # booleans stay booleans
nums: [1, 2] # integer-like values become integersdata = pulumi.Config().require_object("data")
active = data["active"] # a plain dict, not a Configrequire_object returns a plain dictionary. Index into it. You cannot call require on a nested value.
Project-level defaults
Values every stack shares go in Pulumi.yaml, and any stack can override them:
# Pulumi.yaml
config:
aws:region: us-east-1
name: BroomeLLC
data:
value: # structured project-level values need this wrapper
active: true# Pulumi.dev.yaml: overrides for one stack
config:
aws:region: us-east-2
myproject:name: MopLLCWatch the two syntaxes. The stack file uses project:key with structures nested directly. The project file uses bare key and wraps structures in value:. Project-level config is edited by hand (config set doesn’t write it) and holds plaintext only.
The project file can also type-check stack values:
config:
name:
type: string
description: Base name for resources.
default: BroomeLLC
subnets:
type: array
items:
type: stringA stack that sets name to a number then fails at pulumi preview. Type specifications don’t yet cover structured config.
Provider configuration
A provider takes settings from three places: pulumi config set aws:region ..., its environment variables, or constructor arguments. Only the default provider reads stack config. An explicit aws.Provider(...) you create reads nothing from config, so pass its arguments explicitly. To forbid implicit default providers altogether:
config:
pulumi:disable-default-providers: ["aws", "kubernetes"] # or ["*"]pulumi:tags in config applies stack tags on every up or refresh. Removing a key from config does not delete the tag from the stack.
Secrets
Setting
pulumi config set --secret dbPassword 'S3cr37'The stack file now holds ciphertext:
config:
myproject:dbPassword:
secure: AAABAIIlW0ewSuZ1FJxw/+Rpw6BNqTUvGJ30O8WkpL2hB4aPyS7UU68=Quote values containing $, ! or #, or pipe them from a file, so the shell doesn’t change them first. Secrets work inside structured config too: pulumi config set --path --secret 'endpoints[0].token' ....
The inverse: the CLI tries to detect values that look like tokens and stops you. When it guesses wrong, use --plaintext.
As documented (not run here), listings and accidental prints are masked:
$ pulumi config
KEY VALUE
aws:region us-west-1
dbPassword [secret]
Secrets are outputs that remember
A secret is an Output with a flag. Pulumi encrypts it wherever it is persisted, and propagates the flag. Anything computed from a secret with apply or Output.all is also secret. Resource properties built from it are encrypted in state.
import pulumi
import pulumi_aws as aws
cfg = pulumi.Config()
param = aws.ssm.Parameter(
"a-secret-param",
type="SecureString",
value=cfg.require_secret("my-secret-value"), # encrypted in state too
)Ways to make a value secret:
| Source | How |
|---|---|
| Config | get_secret / require_secret |
| An existing value | pulumi.Output.secret(value) |
| A resource’s outputs | ResourceOptions(additional_secret_outputs=["result"]) |
| Derived values | Automatic via apply / Output.all on a secret |
Reading a --secret key with plain require hands you the plaintext and drops the marking downstream. Always use the secret getters.
Where secret-ness ends
- Inside
apply. The callback receives plaintext. Pulumi marks the return value secret, but cannot stop the callback from printing or writing it to a file. - Resource IDs. A resource’s
idis always stored in plaintext, andadditional_secret_outputs=["id"]does nothing.random.RandomStringexposes itsresultas its ID too, so its value lands unencrypted in state. Userandom.RandomPasswordand markresultsecret. - Outside the stack. Secret-ness covers one stack’s state. Sharing the same credential across stacks or teams is a job for Pulumi ESC environments, which a stack imports with an
environment:list in its stack file. Explicit stack config wins over imported values.
Secrets providers
By default with Pulumi Cloud, each stack gets its own Cloud-managed key and each value a nonce. Choose another provider when you use a DIY backend or your organisation mandates its own KMS:
pulumi stack init prod --secrets-provider="awskms://alias/pulumi-prod?region=us-east-1"
pulumi stack init prod --secrets-provider="gcpkms://projects/P/locations/L/keyRings/R/cryptoKeys/K"
pulumi stack init prod --secrets-provider="azurekeyvault://myvault.vault.azure.net/keys/pulumi"
pulumi stack init prod --secrets-provider="hashivault://pulumi-prod"
pulumi stack init prod --secrets-provider=passphraseawskmsaccepts an ID, alias or ARN, an optionalprofile=, and (CLI v3.41.1+)context_<k>=<v>encryption context you can condition IAM policies on.hashivaultreadsVAULT_SERVER_URLandVAULT_SERVER_TOKEN.passphrasederives the key from a passphrase you supply (typically throughPULUMI_CONFIG_PASSPHRASE). Lose it and the stack’s secrets are unrecoverable.
The provider’s public bits (encryptionsalt for passphrase, encryptedkey for KMS-style providers) are written into Pulumi.<stack>.yaml and are safe to commit. To switch an existing stack:
pulumi stack change-secrets-provider "awskms://alias/pulumi-prod?region=us-east-1"
pulumi preview # expect: no changesThis re-encrypts config and state. If you’d rather not commit stack files at all, pulumi config refresh rebuilds them from the last deployment.
Diagnosis
| Symptom | Likely cause | Fix |
|---|---|---|
| “Missing required configuration variable” | Key not set for this stack, or wrong namespace | pulumi config -s <stack>; check the project: prefix |
| Value set, program says unset | Set under a different stack, or project renamed | Confirm the active stack; rename keys in the stack file |
Password visible in stack export |
Read with require instead of require_secret, or carried in an id |
Use the secret getters; avoid ID-bearing resources |
| CLI refuses a harmless value as “looks like a secret” | Secret detection heuristic | --plaintext |
| “passphrase must be set” or decrypt fails in CI | Passphrase or KMS access missing in the runner | Provide PULUMI_CONFIG_PASSPHRASE or KMS credentials |
| Preview fails with a type error | Project-level type spec | Fix the stack value’s type |
Operational care
- Commit
Pulumi.yamlandPulumi.<stack>.yaml. Ciphertext is meant to be committed. Never commit plaintext you meant to mark secret, because git history keeps it. - Treat
pulumi config setin pipelines as a code change. Prefer reviewing the stack file in a PR to mutating config during deploys. - Use one KMS key or ESC environment per trust boundary (prod versus non-prod), and restrict decrypt rights accordingly.
- Rotate by setting the new value with
--secretand runningup. Changing the provider is a separate operation (change-secrets-provider).
Practice
- Design config for a service with region, instance count, a list of allowed CIDRs, and a database password. Which keys go project-level, which stack-level, and which are secret?
- A colleague creates a token with
random.RandomStringand marksresultsecret. What is still exposed, and what do you change? - Your team moves from Pulumi Cloud to an S3 DIY backend. What happens to secrets, and which provider would you pick?
Recall card
Pulumi.<stack>.yamlholds per-stack config (commit it).Pulumi.yamlconfig:holds shared, plaintext, typed defaults.- No-colon keys belong to the project.
Config("ns")reads other namespaces. requirefails loudly,getreturns None, and*_secretkeeps the marking.- Secret-ness propagates through outputs, except inside
applycallbacks and resource IDs. - Use
--secrets-provideratstack initandstack change-secrets-providerlater. A passphrase is a key you must not lose.
Sources
- Pulumi, Configuration
- Pulumi, Secrets
- Pulumi, State and backends