Sensitive Data and Team Workflows
The idea in one sentence
Section titled “The idea in one sentence”Marking a variable or output sensitive = true hides its value from CLI output, but the value is still stored in plaintext inside the state file, so protecting state itself is a separate, and more important, problem.
What sensitive = true actually does
Section titled “What sensitive = true actually does”You can mark a variable or an output as sensitive:
variable "db_password" { type = string sensitive = true}
output "db_password" { value = var.db_password sensitive = true}With this in place, terraform plan and terraform apply print (sensitive value) instead of the real password in the terminal, and it is redacted from CI logs the same way. This is a genuinely useful feature — it stops a secret from ending up in a build log that a dozen people can read, or on a screen during a pairing session.
It is also frequently misunderstood. sensitive = true is a display-redaction feature, not an encryption feature. The actual value is still written into terraform.tfstate in plaintext, exactly as covered in the first lesson of this module. Anyone who can read the raw state file can read the password regardless of how many resources or variables were marked sensitive along the way.
Actually protecting the state file
Section titled “Actually protecting the state file”Since redaction does not protect the file at rest, protection has to happen at the storage layer instead:
- Rely on Cloud Storage’s server-side encryption for the state bucket. GCS encrypts objects at rest by default, and you can layer a customer-managed Cloud KMS key on top via the backend’s
kms_encryption_keyargument if your organization requires it. - Restrict who can read the bucket with IAM, scoped as tightly as possible. Grant
roles/storage.objectVieweron the specific state bucket to the specific principals who need it — a CI service account, a small platform team — rather than granting broad access at the project level. Treat the state file itself as a secret, because for practical purposes it is one.
Where possible, avoid putting long-lived secrets into Terraform configuration or state at all. Instead of a variable holding a raw password, pull the value in at apply time from a secrets manager:
data "google_secret_manager_secret_version" "db_password" { secret = "db-password"}
resource "google_sql_user" "app" { name = "app_admin" instance = google_sql_database_instance.main.name password = data.google_secret_manager_secret_version.db_password.secret_data}This does not make the value vanish from state entirely — the resource attribute Terraform sets still gets recorded the same way it always does. What it does change is where the secret’s long-term source of truth lives: Secret Manager, with its own access policies, audit logging, and rotation story, instead of a Terraform variable that someone has to remember to pass in securely every single run.
Where a shared backend stops being enough
Section titled “Where a shared backend stops being enough”Earlier lessons in this module got state onto a shared GCS backend with built-in locking, which already fixes the worst team problems — divergent local files and corrupted concurrent writes. But it does not fully solve how a growing team should work together. If everyone still runs terraform apply from their own laptop whenever they feel like it, concurrent applies simply queue up behind the lock instead of colliding, and there is still no natural point where a second person reviews a change before it takes effect against real infrastructure.
That gap — a reviewable, auditable place for changes to be approved before apply runs against shared state — is exactly what CI/CD for infrastructure closes, and it is covered later in this course.
flowchart LR varInput["variable db_password (sensitive = true)"] -->|redacted in CLI output| cli["terraform plan / apply output"] varInput -->|value still written| state["terraform.tfstate (plaintext)"] secretManager["Secret Manager secret version"] -->|read via data source at apply time| resource["google_sql_user.app"] resource --> state