Terraform’s lifecycle hooks, precondition and postcondition, aren’t just about validation; they’re critical for ensuring your infrastructure’s state aligns with your expectations before and after Terraform modifies it.

Let’s see this in action. Imagine you’re provisioning an AWS S3 bucket and want to ensure it’s not publicly accessible and that its versioning is enabled.

resource "aws_s3_bucket" "example" {
  bucket = "my-unique-terraform-example-bucket"
  acl    = "private" # Explicitly setting private, but we'll verify.

  versioning {
    enabled = true # Explicitly enabling versioning, but we'll verify.
  }

  precondition {
    condition     = self.acl == "private"
    error_message = "Bucket ACL must be private before creation."
  }

  postcondition {
    condition     = self.versioning[0].enabled == true
    error_message = "Bucket versioning must be enabled after creation."
  }
}

Here, self refers to the resource being configured. The precondition checks the acl attribute before Terraform attempts to create the aws_s3_bucket. If self.acl is anything other than "private", Terraform will halt with the specified error message. The postcondition similarly checks the versioning attribute after the resource has been created (or updated). The [0] is used because versioning can be a list, and we’re interested in the first (and typically only) block.

These hooks allow you to encode business logic and operational requirements directly into your Terraform code, making your infrastructure more robust and less prone to configuration drift or unintended side effects. They operate at the resource level, meaning each precondition and postcondition block is associated with a specific resource instance.

The power of precondition lies in preventing potentially destructive actions. For instance, before deleting a critical database, you could add a precondition to check a specific tag indicating it’s a production resource, preventing accidental deletion. Conversely, postcondition acts as a gatekeeper, verifying that the desired state has been achieved. If an external process or a subsequent Terraform operation inadvertently changes a critical attribute, the postcondition will catch it.

Consider a scenario where you’re managing a Kubernetes cluster. You might use a precondition to ensure that the cluster’s API server is reachable and healthy before attempting to deploy applications. A postcondition could then verify that essential cluster add-ons, like a network policy enforcer, are running and healthy.

resource "kubernetes_namespace" "app_ns" {
  metadata {
    name = "my-application"
  }

  # Precondition to ensure the Kubernetes provider is configured correctly
  # and can reach the API server before attempting to create the namespace.
  precondition {
    condition = kubernetes_provider.default.host != null && kubernetes_provider.default.host != ""
    error_message = "Kubernetes API server host is not configured or is empty."
  }

  # Postcondition to ensure the namespace has been created and is visible.
  # This is a simplified check; in reality, you might query for the namespace's status.
  postcondition {
    condition = self.id != null && self.id != ""
    error_message = "Kubernetes namespace was not successfully created or identified."
  }
}

# Assume kubernetes_provider.default is defined elsewhere and configured.
# provider "kubernetes" {
#   host = "https://your-k8s-api:6443"
#   # ... other config
# }

The precondition in this Kubernetes example checks if the host attribute of the Kubernetes provider is set. If it’s not, Terraform won’t even try to talk to the Kubernetes API to create the namespace. The postcondition checks if the resource has been assigned an id after creation, a basic indicator of success.

A subtle but crucial aspect of precondition and postcondition is their interaction with Terraform’s state management. precondition checks are evaluated against the current state of the resource before any modifications are planned or applied. postcondition checks are evaluated against the state of the resource after Terraform has successfully applied changes to it, but before Terraform writes the new state to the backend. This means if a postcondition fails, Terraform will roll back the changes to that specific resource and report an error, leaving the resource in its previous state (if possible) or an inconsistent state if rollback isn’t fully achievable.

The self keyword is immensely powerful here. It allows you to reference attributes of the resource being defined, enabling self-referential validation. For example, you could have a postcondition that checks if a newly created EC2 instance has a specific tag that was dynamically generated or assigned during creation.

When troubleshooting, remember that precondition failures occur during the terraform plan phase if the condition is not met based on existing state or the planned changes. postcondition failures occur during the terraform apply phase, after the resource has been modified. The error messages you define are your primary tool for understanding why a hook failed.

The next concept you’ll likely encounter after mastering lifecycle hooks is the use of custom providers and the tfexec library for programmatic Terraform execution and validation, which allows for even more sophisticated pre- and post-execution checks.

Want structured learning?

Take the full Terraform course →