Sensitive Values, Preconditions, Postconditions, and Checks

Terraform can provision infrastructure only as safely as its configuration expresses assumptions. Sensitive values, preconditions, postconditions, and check blocks give you language-level tools for three related jobs: hiding secret material from routine output, rejecting invalid operations before provider calls, and validating that planned or created infrastructure still satisfies an expected property.

The outcome for this chapter is practical. You should be able to decide whether a rule belongs on an input variable, inside a resource lifecycle block, on an output, or in a separate check block. You should also understand what Terraform does and does not protect when you mark a value as sensitive.

Purpose and Outcome

A sensitive value is a value Terraform redacts in CLI output and in most human-readable plan displays. It is commonly used for passwords, tokens, private keys, database connection strings, and generated credentials. It is not encryption. Terraform still needs the real value in memory, provider requests, plan files when present, and state if the value is stored there.

A precondition is a rule attached to a resource, data source, or output that must be true before Terraform continues with that object. A postcondition is a rule that must be true after Terraform can evaluate the object, usually after provider planning or apply has produced attributes. A check block is a top-level validation that reports whether an assertion holds, often for ongoing infrastructure health or cross-resource expectations.

In a Terraform course, these features matter because they move important assumptions from tribal knowledge into configuration. They do not replace reviews, policy engines, or provider-side security controls, but they catch mistakes close to the code that creates infrastructure.

How Terraform Handles These Mechanisms

Terraform evaluates configuration in phases. It loads expressions, validates variable values, builds a dependency graph, asks providers to plan resource changes, and then applies the selected actions. Sensitive markings and conditions participate in that evaluation, but they serve different purposes.

Sensitivity is metadata that flows with expression results. If a variable is declared with sensitive = true, expressions derived from that variable usually become sensitive too. Terraform then replaces the displayed value with a redacted marker in plan and apply output. If you combine a sensitive password with a non-sensitive username to build a URL, the whole URL becomes sensitive because revealing it would reveal the password.

Preconditions and postconditions live in lifecycle blocks. They use condition and error_message. A precondition is useful when a resource argument is syntactically valid but operationally unsafe for this module. A postcondition is useful when a provider-computed attribute must meet an expectation before downstream resources rely on it. Terraform can use self inside these conditions to refer to the object being validated.

Check blocks are top-level blocks containing one or more assert blocks. A failed check reports a warning rather than stopping the whole operation. That distinction is deliberate: checks are for validation signals that should be visible during plan or apply without necessarily preventing all infrastructure changes. If a rule must block creation, use variable validation, a precondition, or a postcondition instead.

Syntax Anatomy

Use sensitive = true on variables and outputs when routine CLI output should not reveal the value. If an output references sensitive data, Terraform requires the output itself to be marked sensitive. This prevents accidental disclosure through module outputs.

Use lifecycle { precondition { ... } } when the object should not be planned or applied unless the rule is true. Use postcondition when the rule depends on attributes known only after Terraform evaluates the object. Use check when the assertion is an operational or advisory validation rather than a hard creation rule.

terraform {
  required_version = ">= 1.5.0"
}

variable "db_password" {
  type        = string
  sensitive   = true
  description = "Password supplied by a secret manager or secure pipeline variable."
}

output "configured_password" {
  value     = var.db_password
  sensitive = true
}

This first example shows sensitivity propagation. If you run a plan with db_password set, Terraform will not print the password value in the output section. The expected displayed behavior is redaction, while the actual value remains available to expressions that need it. The important trade-off is that users get safer terminal output, but state and saved plan handling still require secure storage and access control.

Worked Example: Blocking a Weak Secret

Sensitive redaction alone does not say whether a value is acceptable. A weak password can be hidden just as successfully as a strong one. The next example adds a precondition to a built-in terraform_data resource so the module refuses short values before treating the secret as configured data.

terraform {
  required_version = ">= 1.5.0"
}

variable "db_password" {
  type      = string
  sensitive = true
}

resource "terraform_data" "database_secret" {
  input = {
    name     = "application-db"
    password = var.db_password
  }

  lifecycle {
    precondition {
      condition     = length(var.db_password) >= 16
      error_message = "db_password must contain at least 16 characters."
    }
  }
}

With db_password = "short", Terraform fails during planning with the configured error message. With a password of 16 or more characters, planning can continue. This rule belongs near the resource because it describes a requirement for this particular secret consumer. If every caller of a module must meet the same rule, variable validation can also be appropriate.

Worked Example: Verifying a Computed Result

A postcondition is most useful when the final attribute is not just copied from input. Providers often normalize, default, or compute values. With terraform_data, the output attribute mirrors the stored input, so the example is deterministic and local while still showing the shape of the feature.

terraform {
  required_version = ">= 1.5.0"
}

variable "service_user" {
  type = string
}

resource "terraform_data" "service_identity" {
  input = {
    username = lower(var.service_user)
  }

  lifecycle {
    postcondition {
      condition     = self.output.username != "root"
      error_message = "The service identity must not use the root account."
    }
  }
}

output "service_username" {
  value = terraform_data.service_identity.output.username
}

If service_user is APPUSER, the output is deterministically appuser. If service_user is root or ROOT, the postcondition fails because the normalized result is root. The key lesson is placement: the rule checks the final value that downstream configuration would consume, not merely the caller’s original spelling.

Worked Example: Advisory Checks

Some validations should be visible without blocking every run. For example, a platform team may want a warning when a rotation interval is longer than recommended, while still allowing a separate emergency change to apply. A check block expresses that advisory signal.

terraform {
  required_version = ">= 1.5.0"
}

variable "rotation_days" {
  type    = number
  default = 120
}

check "password_rotation_window" {
  assert {
    condition     = var.rotation_days <= 90
    error_message = "Password rotation should be scheduled every 90 days or less."
  }
}

With the default value of 120, Terraform reports a failed check as a warning. It does not behave like a failing precondition. If rotation_days = 60, the check passes. This makes check blocks suitable for health and compliance visibility, while hard safety requirements should use blocking validation.

Design Choices and Trade-offs

The first design choice is whether secrecy, validity, or health is the problem. Marking a value sensitive protects human-facing output from accidental disclosure. It does not prove the value is strong, current, or authorized. Preconditions and postconditions enforce correctness for a specific object and can stop a run. Checks communicate validation status and can be useful during migration, auditing, or monitoring-oriented runs.

The second choice is where to place a rule. Put format rules that apply to all callers in variable validation. Put object-specific assumptions in lifecycle conditions. Put published-output guarantees on outputs. Put advisory or cross-cutting observations in check blocks. This keeps error messages close to the decision a user can correct.

The third choice is how strict to be. Blocking every imperfect condition can make urgent repair work difficult. Allowing every condition as a warning can normalize drift and weak practices. A useful rule of thumb is simple: if continuing can create an invalid or dangerous object, block it; if continuing is safe but the team needs evidence, use a check.

Failure Modes and Troubleshooting

A common symptom is a plan that prints (sensitive value) where an operator expected to inspect the exact string. The cause is sensitivity propagation. Diagnose by tracing which variable or output is marked sensitive and whether the expression combines secret and non-secret parts. Correct it by separating non-secret fields into separate outputs, never by removing sensitivity from the actual secret.

Another symptom is Output refers to sensitive values. Terraform raises this when an output exposes data derived from a sensitive expression but the output lacks sensitive = true. The fix is to mark the output sensitive or redesign the output to expose only non-secret metadata, such as a secret name rather than the secret value.

A precondition failure usually shows the custom error message and the object that failed. Diagnose by evaluating the exact expression with the supplied variable values. If the rule depends on an unknown value, consider whether it belongs as a postcondition instead. Correct the input, move the rule to the proper phase, or split the resource so the validated value is known at the right time.

A failed check can be missed because it is a warning, not a hard failure. The symptom is a successful apply with warning text in the output. The cause is using check for a rule the team actually intended to enforce. Correct it by converting the assertion to variable validation, a precondition, or a postcondition when the condition must block infrastructure changes.

Security, Reliability, and Performance Implications

Sensitive values reduce accidental exposure in terminal logs and review screens, but the state backend is still a security boundary. Store state in a backend with encryption, access controls, locking, audit logs, and narrowly scoped credentials. Treat saved plan files as sensitive artifacts because they can contain cleartext values needed for apply.

Conditions improve reliability by failing early with domain-specific messages. A provider API error such as a generic validation failure may be harder to understand than db_password must contain at least 16 characters. Clear conditions shorten diagnosis and prevent downstream resources from depending on invalid assumptions.

Expression-heavy checks can add plan-time work, especially if they depend on data sources or large collections. Keep assertions direct and deterministic. If a validation requires live service probing, decide whether Terraform is the right place for it or whether a separate monitoring system should own the ongoing check.

Hands-on Lab

Prerequisites: a local Terraform CLI version that supports check blocks, an empty working directory, and no cloud credentials. The lab uses only built-in Terraform behavior, so it does not create external infrastructure.

  1. Create a file named main.tf containing the password, service identity, and rotation examples from this chapter.
  2. Run terraform init. Verification: initialization completes without installing a cloud provider.
  3. Run terraform plan -var='db_password=short' -var='service_user=app'. Verification: the plan fails with the password length precondition message.
  4. Run terraform plan -var='db_password=long-enough-secret' -var='service_user=ROOT'. Verification: the password rule passes, but the service identity postcondition reports that root is not allowed.
  5. Run terraform plan -var='db_password=long-enough-secret' -var='service_user=APPUSER'. Verification: planning succeeds, the password is redacted where displayed, service_username is appuser, and the rotation check warns when rotation_days remains above 90.
  6. Run the same plan with -var='rotation_days=60'. Verification: the advisory check passes.
  7. Cleanup by deleting the temporary directory. If you ran terraform apply, run terraform destroy first, although this lab should only create local Terraform state for terraform_data.

Assessment Exercises

  1. A module outputs a database connection URL that includes a sensitive password and a non-sensitive hostname. How would you expose the hostname for diagnostics without exposing the password?
  2. A resource argument accepts any string, but your organization allows only names beginning with svc-. Should that rule be variable validation, a precondition, or a check? Explain your placement.
  3. A provider computes a final endpoint after creation, and downstream resources require it to use HTTPS. Write the kind of Terraform condition you would use and explain why it belongs after provider evaluation.
  4. A team uses a check block to warn when public access is enabled, but the apply still succeeds. When is that acceptable, and when should the rule become blocking?
  5. Review a Terraform state storage design. Which parts must be protected even if every secret variable is marked sensitive?

Summary

Sensitive values, preconditions, postconditions, and checks are small Terraform language features with different jobs. Sensitivity controls display, not storage secrecy. Preconditions block unsafe inputs before an object proceeds. Postconditions verify the evaluated result before other configuration relies on it. Check blocks provide advisory validation that can surface health or compliance issues without necessarily stopping the run. Used together, they make Terraform modules clearer, safer, and easier to diagnose.