Pulumi secrets vs stack state
Two different stores. Mixing them up is why "why do I need S3/Drive?" keeps coming up.
What lives in the infra git repo
| Artifact | Role |
|---|---|
Program ( |
Desired infrastructure as code. |
|
Non-secret config (zone IDs, hostnames) plus encrypted secret ciphertext when you |
Docs / checklists |
Operator runbooks. |
Encrypted secrets in that YAML are unlocked with a secrets provider:
-
Passphrase — set
PULUMI_CONFIG_PASSPHRASE(or enter when prompted). Closest to "master password decrypts the vault in git." -
Pulumi Cloud — default when the stack backend is Pulumi SaaS; Cloud holds the encryption keys for you.
-
Cloud KMS (AWS KMS, etc.) — production teams.
You do not commit plaintext API tokens. You commit ciphertext (and keep the passphrase / KMS / Cloud account safe).
What does not live in git (the state backend)
Pulumi also keeps a stack state checkpoint: resource URNs, provider IDs (Cloudflare record IDs, etc.), outputs, and locks. That answers "what did we already create?" on the next pulumi preview / up.
State backends:
| Backend | Notes |
|---|---|
Pulumi Cloud (default |
Easiest solo. Free Individual = no teammate invites. See Pulumi Cloud org model. |
Object storage ( |
DIY team sharing via bucket IAM. This is the "block/object storage" people mention — it holds state, not a substitute for git secrets. |
|
Yes — state can be a plain file (or directory of files). No daemon. The CLI reads/writes checkpoints under a path you choose (default |
Is the backend a server?
Not necessarily. “Backend” here means where the checkpoint JSON lives:
-
File — static files on disk. Closest to “save state next to config.”
-
Object storage — static objects in S3/B2/GCS (HTTP API, still not a Pulumi daemon you run).
-
Pulumi Cloud — optional hosted service (UI, locks, team features). That is a server, and it is optional.
Why not always commit state beside Pulumi.prod.yaml?
-
State changes on every successful apply (provider IDs, outputs). Config changes when you edit inputs.
-
Two people applying → git merge conflicts in a giant JSON blob.
-
CI and laptops need a shared, lockable copy — buckets/Cloud handle locks; a lone
file://path does not.
So: you can express state as a static file. Teams often keep it out of git for concurrency. Solo orgs that want the simplest mental model (this repo’s nonprofit-resources/infra) can commit file://./state and treat it like any other artifact.
Google Drive is fine as an offline copy of pulumi stack export JSON. It is not a Pulumi login URL. Prefer B2/R2/S3 (or local + rclone sync) when you want a professional state backend without Pulumi Team Edition.
Do you need to “know” the state?
Usually no. You do not edit state by hand. The CLI reads it automatically so preview / up can compute a diff against the cloud.
Could Pulumi skip a backend and rediscover everything each run? In practice no for production stacks:
-
Many resources cannot be uniquely rediscovered (two TXT records with the same name, renamed logical names, provider IDs only Pulumi stored).
-
Full refresh against every API is slower and still needs a place to write the result — that place is the state backend.
-
Without state, a failed mid-apply leaves you guessing; with state, the next run resumes from known URNs.
So state exists for correctness and speed, not because humans enjoy reading JSON. Prefer forgetting it exists except when exporting backups or migrating backends.
Mental model
git repo → code + encrypted config secrets (passphrase / KMS / Cloud keys) backend → live resource inventory + locks (Cloud / S3 / file)
Both are required for a healthy stack. Secrets-without-state forgets what exists in Cloudflare. State-without-secrets cannot decrypt tokens to call APIs.