Secret Manager and Sensitive Inputs
Secret material — database passwords, the Hatch enrichment-api-key, third-party tokens — must never sit in plain HCL or land in the state file, and Secret Manager is how you keep it out. You create a google_secret_manager_secret as the container and a google_secret_manager_secret_version as the value, grant the consuming service account roles/secretmanager.secretAccessor on that specific secret, and have the app fetch it at runtime rather than baking it into config.
The goal is sharper than "use Secret Manager." It is to keep the sensitive bytes out of Terraform's hands entirely, because any value Terraform manages is stored unencrypted in state, and any literal in a .tf file is in git history forever. The techniques below all serve that one objective: the material should pass through neither a plan nor a state file.
Secret vs Secret Version
A google_secret_manager_secret is the named container with a replication policy — it has a name and metadata but no value. A google_secret_manager_secret_version holds an actual value. Versions are what make rotation clean: you add a new version with the new value while old versions stay readable until you disable them, so a rotation is additive rather than a destructive in-place edit.
Keeping the two distinct is what enables the out-of-band pattern later in this topic — you can manage the container, its IAM, and its replication in Terraform while the version values come from somewhere else entirely.
resource "google_secret_manager_secret" "enrichment_api_key" { project = "hatch-app-prod" secret_id = "enrichment-api-key" # the container — no value here replication { auto {} } } resource "google_secret_manager_secret_iam_member" "run_access" { project = "hatch-app-prod" secret_id = google_secret_manager_secret.enrichment_api_key.secret_id role = "roles/secretmanager.secretAccessor" # on THIS secret only member = "serviceAccount:hatch-app-run@hatch-app-prod.iam.gserviceaccount.com" }
Granting Access on the Specific Secret
Grant roles/secretmanager.secretAccessor on a single secret to the Cloud Run runtime SA, and that SA can read exactly that secret and nothing else. Scope it tightly: the Hatch frontend reads enrichment-api-key and not every secret in the project. Grant the same role at the project level instead and the SA can read every secret there is — a quiet over-grant that a least-privilege review should catch.
This is the same per-resource IAM discipline as the subnet grants in Topic 57 and the cloudsql.client grant in Topic 61. The pattern repeats because it is correct: grant on the specific resource, to the specific identity, for the specific role.
Why Plain HCL and State Are the Thing to Avoid
A password written as a literal in a .tf file is in git history forever — rotating it later does not erase the old value from the history. Worse, any sensitive value Terraform manages is stored unencrypted in the state file, so even a value passed cleanly through a variable still lands in state in the clear.
That second fact is the one people miss. The goal is not merely to keep secrets out of the HCL you commit — it is to keep them out of Terraform's hands altogether, because state is just as much an exposure as source control. Anyone with read access to the state bucket can read every sensitive value Terraform touched.
Deploy-Time vs Runtime References
There are two ways the app gets the secret. Cloud Run can mount a secret version as an env var or file at deploy, or the app can call the Secret Manager API at runtime. A runtime fetch keeps the value out of the service's stored config entirely; a deploy-time mount is simpler, but the resolved value then sits in the Cloud Run revision configuration where anyone with read access to the revision can see it.
For the most sensitive values, prefer the runtime fetch. The few extra lines of app code are worth keeping the material out of a stored revision, especially for anything whose exposure would force a rotation.
Creating the Container in Terraform, the Value Out of Band
The cleanest pattern manages the secret — the container, its IAM, its replication — in Terraform, while the actual secret_version value is written with gcloud or a pipeline. The sensitive bytes then never pass through a Terraform plan or land in state at all. Terraform owns the structure and the access policy; something outside Terraform owns the value.
Put the value in a secret_version resource in HCL and you have written plaintext straight into state, defeating the entire exercise. Manage the container in code, set the value out of band, and the secret stays where it belongs — in a purpose-built, IAM-gated, audit-logged store, and nowhere else.
Literal in HCL — lands in git history and in state unencrypted, readable by anyone with repo or state access. Rotating it does not erase the old value from history.
Secret Manager reference — keeps the value in a purpose-built, IAM-gated, audit-logged store and hands the app only an accessor grant. There is no defensible reason to put a real password in HCL when the alternative is a few extra resources.
- Writing a real password or API key as a literal in a
.tffile — it is in git history permanently, and rotating it does not erase the old value from history. - Passing a secret through a Terraform variable and assuming
sensitive = trueprotects it —sensitiveonly hides it from plan output; the value is still stored unencrypted in the state file. - Granting
secretAccessorat the project level instead of on the specific secret, so the runtime SA can read every secret in the project rather than the one it needs. - Putting the actual secret value in a
secret_versionresource in HCL, which writes the plaintext into state — manage the container in Terraform but set the value out of band. - Mounting a secret at deploy-time and then being surprised the resolved value is visible in the Cloud Run revision config, when a runtime fetch would have kept it out.
- Keep secret values out of HCL and out of state — manage the
secretcontainer and IAM in Terraform and writesecret_versionvalues with a pipeline orgcloud. - Grant
roles/secretmanager.secretAccessoron the specific secret to the specific runtime service account, never project-wide. - Prefer runtime Secret Manager API fetches for the most sensitive values so the material never lands in a stored service config.
- Rotate by adding a new secret version and disabling the old one, keeping the rotation auditable instead of editing a value in place.
- Treat the state file as an exposure equal to source control, and protect read access to the state bucket accordingly.
Knowledge Check
A teammate sets sensitive = true on a variable holding the DB password and considers it safe. What did they miss?
sensitiveonly hides the value from plan output; it is still stored unencrypted in the state file- It encrypts the value in state but not in plan output
- It blocks the value from being committed to git by stripping the literal out of any tracked
.tffile automatically - Nothing —
sensitive = truefully protects the value everywhere
Why manage the secret container in Terraform but write the version value out of band?
- So the sensitive bytes never pass through a plan or land in the state file
- Because Terraform cannot create a
google_secret_manager_secretat all - To make the secret cheaper to store
- Because individual secret versions cannot be granted IAM, so the value has to be written by a separate out-of-band process
What is the downside of mounting a secret at deploy-time rather than fetching it at runtime?
- The resolved value sits in the Cloud Run revision config, visible to anyone with read access to the revision
- Deploy-time mounts cannot use a scoped secretAccessor grant and instead force a broad project-level binding on the runtime SA
- The app can never rotate the secret afterward
- It writes the value into git history
Why grant secretAccessor on the specific secret rather than at the project level?
- A project-level grant lets the runtime SA read every secret in the project, not just the one it needs
- Project-level grants are not supported for Secret Manager
- Per-secret grants are required for the value to be encrypted
- A project-level grant locks the secret to a single version and prevents the app from rotating it to a new one later
You got correct