Secrets and Credential Management
The idea in one sentence
Section titled “The idea in one sentence”Provider authentication should come from Application Default Credentials or an identity mechanism rather than a downloaded key typed into a file, and even pulling an application secret from a real secrets manager at apply time does not fully keep it out of Terraform state.
Never hardcode credentials in a provider block
Section titled “Never hardcode credentials in a provider block”It is possible to authenticate the google provider with a downloaded service account key JSON file, referenced directly from the provider "google" block. It also works, in the narrow sense that terraform plan runs. It is still wrong:
# Anti-pattern — never do thisprovider "google" { project = "acme-prod-123456" region = "us-central1" credentials = file("service-account-key.json")}A key file referenced this way gets committed to git the first time someone forgets it is there, and it lives on in every clone and every commit in history from that point forward, whether or not it is later deleted from the current revision. The fix is to give the provider no credentials at all in configuration, and let it resolve them from the environment it is running in:
# The provider block declares no credentials.# Authentication comes from Application Default Credentials:# a local gcloud auth, an attached GCE/CI service account, or# Workload Identity Federation in CI.provider "google" { project = "acme-prod-123456" region = "us-central1"}Locally, that usually means Application Default Credentials set up through gcloud auth application-default login. On a GCE instance or in most managed compute, it means a service account already attached to the runtime, discovered automatically. In CI, it means the Workload Identity Federation covered in the CI/CD lesson — google-github-actions/auth populates Application Default Credentials before Terraform ever runs, so the provider block finds credentials without a single key file ever being written down anywhere.
Pulling a secret in via a data source, not a variable
Section titled “Pulling a secret in via a data source, not a variable”A separate question from provider authentication is application secrets — a database password, a third-party API key — that a resource needs as an argument. Defining that secret as a Terraform variable makes the variable declaration the thing you have to treat as the source of truth, which means someone still has to type the real value into a tfvars file, a CI secret, or a prompt at apply time.
The better pattern reads the current value directly from a real secrets manager through a data source, at apply time:
data "google_secret_manager_secret_version" "api_key" { secret = "third-party-api-key"}
resource "google_cloud_run_v2_service" "webhook_handler" { name = "webhook-handler" location = "us-central1"
template { containers { image = "us-central1-docker.pkg.dev/acme-prod-123456/app/webhook-handler:latest"
env { name = "API_KEY" value = data.google_secret_manager_secret_version.api_key.secret_data } } }}This is not a free pass, and it is worth being precise about why. The State Management module already covered that marking a variable sensitive = true only redacts it from CLI output — the value still lands in terraform.tfstate in plaintext. Reading a secret through a data source does not fully avoid that either: data source results are cached in state too, so the resolved secret_data still ends up recorded there, the same as a sensitive variable would. What the data-source pattern actually improves is the secret’s lifecycle everywhere else: the real value is never typed into a variable, never sits in a .tfvars file someone might commit by accident, and rotating it in Secret Manager needs no Terraform change at all — the next apply simply reads whatever is current. Secret Manager is the source of truth; Terraform just reads from it. Protecting the state backend, covered in the State Management module, is still the part that cannot be skipped.
A .gitignore checklist for a Terraform/Terragrunt repository
Section titled “A .gitignore checklist for a Terraform/Terragrunt repository”A short, deliberate .gitignore prevents most of the accidents above from ever reaching git in the first place:
# Terraform / Terragrunt .gitignore
# Never commit state — it can contain plaintext secrets and is the# canonical source of truth; committing it invites drift and leaks.*.tfstate*.tfstate.backup
# Local provider and module cache, regenerated by `terraform init`..terraform/
# May contain sensitive values depending on the file — judge case by# case rather than blanket-ignoring every *.tfvars in the repo.*.tfvars
# Terragrunt's local working-directory cache, regenerated automatically..terragrunt-cache/
# Downloaded service account key JSON files — should not exist in this# repo at all if Workload Identity Federation and ADC are used correctly.*-key.json*-sa.jsonA few of these deserve a one-line reason each:
*.tfstateand*.tfstate.backup— never commit state. Beyond the size and merge-conflict problems, state can contain plaintext secrets, as covered above, and it duplicates the job a remote backend already does properly..terraform/— this isinit’s local cache of downloaded providers and modules. It is large, machine-specific, and fully reproducible from.terraform.lock.hcl, so there is nothing worth versioning here.*.tfvars— treat this one case by case rather than blanket-ignoring it. Some teams deliberately commit a non-sensitiveenvironment.tfvarsthat just sets machine types or region names; the rule is “does this specific file contain a secret,” not “all.tfvarsfiles are always secret.”.terragrunt-cache/— Terragrunt’s equivalent of.terraform/, one per unit, regenerated on every run. It never needs to be committed, and committing it by accident tends to bloat a repository fast.- Downloaded service account key JSON files — a repo that follows the previous lesson’s guidance should not have any of these to begin with, but a name-pattern rule is cheap insurance against someone downloading one for a quick local test and forgetting to remove it.
flowchart LR bad["Downloaded key in a .tf / .tfvars file"] -->|committed by accident| leak["Leaked credential in git history"] sm["Secret Manager: source of truth"] -->|data source at apply time| attr["Resource attribute"] attr -->|still recorded| state["terraform.tfstate"]