linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
Intro
Lessons
Footer
linuxlab-TutorialsPricingAboutPrivacy & cookies
Copyright © 2026 LinuxLab. All rights reserved.
linuxlab.io
Tutorials▾
  • Linux & networking
    File system, processes, TCP/IP, BGP and OSPF
    →
  • Terraform & IaC
    HCL, state, plan/apply on a LocalStack sandbox
    →
  • Git & GitHub
    Object model, plumbing, branching, GitHub Actions
    →
All tutorials →
PricingAboutSign inCreate account
/
  • Introduction
  • Lessons
  • How it works
  • Knowledge base
  • Cheat sheet
  • Capstone
  • Interview prep
home/terraform/lessons/tf-intermediate-10-preconditions

lesson ── terraform-intermediate ── ~14 мин ── 5 шагов

preconditions, postconditions, and the check block

Terraform plan shows you what will happen, but it does not check whether that makes sense. Say a bucket gets created in us-west-2 while your HCL says it should be in us-east-1: nothing catches that on its own.

Since TF 1.2+ you have precondition and postcondition inside lifecycle, declarative assertions. If they fail, apply stops. Since TF 1.5+ you have check blocks, soft checks that do not block apply.

▶ интерактивный sandbox

Поднимется пара контейнеров: terraform 1.9 и localstack 3.8 в одной сети. В браузере откроется терминал, можно сразу terraform init. Каждый шаг проверяется автоматически. TTL 45 минут, без регистрации.

запустить sandbox →

stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя

Шаги

  1. 01

    A base bucket to experiment with

    bash
    cd /home/student/tf-checks
    cat > main.tf <<'EOF'
    variable "bucket_name_prefix" {
      type    = string
      default = "linuxlab-checks"
    }
    resource "random_id" "suffix" {
      byte_length = 4
    }
    resource "aws_s3_bucket" "demo" {
      bucket = "${var.bucket_name_prefix}-${random_id.suffix.hex}"
    }
    EOF
    terraform init
    terraform apply -auto-approve

    ✓ The base is created. Now add invariants.

  2. 02

    precondition: check the inputs

    Add a lifecycle with preconditions to aws_s3_bucket.demo:

    hcl
    resource "aws_s3_bucket" "demo" {
      bucket = "${var.bucket_name_prefix}-${random_id.suffix.hex}"
      lifecycle {
        precondition {
          condition     = length(var.bucket_name_prefix) >= 3 && length(var.bucket_name_prefix) <= 40
          error_message = "bucket_name_prefix must be 3-40 chars (to fit total bucket name into S3 63-char limit)."
        }
      }
    }

    To see it work, change default to something short:

    bash
    sed -i 's/default = "linuxlab-checks"/default = "x"/' main.tf
    terraform plan

    You should get an error:

    Error: Resource precondition failed
      on main.tf line ..., in resource "aws_s3_bucket" "demo":
       ...: condition = ...
    bucket_name_prefix must be 3-40 chars ...

    The plan stops before reaching the cloud. This is validation at plan time. See tf-resource-lifecycle and tf-variable.

    Put the value back:

    bash
    sed -i 's/default = "x"/default = "linuxlab-checks"/' main.tf

    ✓ precondition fired. The plan is now guarded against bad inputs.

  3. 03

    postcondition: check the output

    A postcondition runs after the attributes come back from the provider. Let's check that the bucket was created in the right region:

    hcl
    resource "aws_s3_bucket" "demo" {
      bucket = "${var.bucket_name_prefix}-${random_id.suffix.hex}"
      lifecycle {
        precondition {
          condition     = length(var.bucket_name_prefix) >= 3 && length(var.bucket_name_prefix) <= 40
          error_message = "bucket_name_prefix must be 3-40 chars."
        }
        postcondition {
          condition     = self.region == "us-east-1"
          error_message = "Expected region us-east-1, got ${self.region}."
        }
      }
    }

    self inside a postcondition is the resource itself, with all of its attributes available. This is the main difference from precondition (there is no self there, the resource does not exist yet).

    Apply it:

    bash
    terraform apply -auto-approve

    Plan and apply both pass (LocalStack returns us-east-1, as configured).

    ✓ postcondition fired. The resource attribute is verified.

  4. 04

    the check block: a soft check

    check. TF 1.5+, a separate top-level block. On failure it does not block apply, it only prints a warning:

    Add this to the end of main.tf:

    hcl
    check "bucket_versioning" {
      assert {
        condition     = aws_s3_bucket.demo.versioning != null && length(aws_s3_bucket.demo.versioning) > 0
        error_message = "Bucket has no versioning configured: warning, not blocker."
      }
    }
    bash
    terraform apply -auto-approve

    It should print a warning (our bucket has no versioning configured):

    Warning: Check block assertion failed
      on main.tf line ...:
      ...
    Bucket has no versioning configured...

    Apply went through, just a warning. That is the difference from precondition (there the apply would have failed): check is for monitoring drift and unwanted states that should not block a deployment. For example: the bucket should be versioning'ed, but if it is not yet, do not break the pipeline, send a warning to alerting.

    ✓ the check block did its job with a warning. It does not block apply.

    The same thing on OpenTofu

    OpenTofu keeps its CLI and state compatible with Terraform for the commands in this step: migration usually goes through mv .terraform .terraform.bak; tofu init -upgrade. On your first move, though, back up the state and do a run on a feature branch. Differences cluster in the newer features (variables in the backend, state encryption, OCI registry-backed modules). See tf-opentofu-parity for the full matrix.

    • → OpenTofu parity
  5. 05

    When to use which

    All three mechanisms look similar. The differences:

    MechanismWhen it runsBehavior on failure
    variable.validationOn the variableplan error, before anything
    lifecycle.preconditionBefore the resource is created or changedplan error
    lifecycle.postconditionAfter refresh, before writing stateapply error
    check { assert }Alongside applywarning, apply continues

    The rules:

    • User inputs (the variable contents) → variable.validation.
    • Logic of "this resource depends on an external invariant" → precondition.
    • "After creation the value must be this" → postcondition.
    • Drift monitoring and desired states → check.

    Do not overuse them. Every check is weight on the plan. If you can make an invalid state impossible through types, prefer that. For example type = number already rules out "create the bucket -1 times" without validation.

    ✓ The differences are clear. Use the right one.

    preconditions between resources

    The most practical case for preconditions is the link between resources:

    hcl
    resource "aws_iam_role" "lambda" {
      name = "demo-lambda"
      # ...
    }
    resource "aws_lambda_function" "demo" {
      function_name = "demo"
      role          = aws_iam_role.lambda.arn
      lifecycle {
        precondition {
          condition     = startswith(aws_iam_role.lambda.assume_role_policy, "{")
          error_message = "IAM role must have a valid JSON assume_role_policy."
        }
      }
    }

    This is a safety belt. Even if the IAM role was created wrong, the lambda will not chase after it and hit some cryptic AWS error.

    Antipattern: writing a precondition for something Terraform already checks at the type stage. If you write a precondition with condition = var.foo != null, but variable "foo" { type = string } has no default, Terraform will not let you pass null anyway. A duplicate.

    • → Resource lifecycle
    • → Variable and validation

Что ты узнал

precondition runs before the resource is created; postcondition runs after state knows the attributes; check { assert { } } is a separate validator block that only warns on failure and does not block. All three are declarative and catch errors before the cloud.

команды

  • terraform plana failed precondition stops the plan
  • terraform applypostcondition is checked after refresh, before it is committed to state

концепции

  • · precondition blocks: apply will not go through
  • · the check block only warns: for monitoring drift, not for blocking
  • · postcondition with self.X checks computed attributes: what variable validation cannot do

← предыдущий

locals and functions: removing duplication in HCL

следующий →

Drift detection, scheduled plan, and alerting

Footer
linuxlab-
Copyright © 2026 LinuxLab. All rights reserved.
Tutorials
Pricing
About
Privacy & cookies