lesson ── terraform-intermediate ── ~14 мин ── 5 шагов
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 минут, без регистрации.
stack ── terraform · localstack · 1 GB RAM · самоуничтожается через 45 мин простоя
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.
Add a lifecycle with preconditions to aws_s3_bucket.demo:
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:
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:
sed -i 's/default = "x"/default = "linuxlab-checks"/' main.tf
✓ precondition fired. The plan is now guarded against bad inputs.
A postcondition runs after the attributes come back from the provider. Let's check that the bucket was created in the right region:
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:
terraform apply -auto-approve
Plan and apply both pass (LocalStack returns us-east-1, as configured).
✓ postcondition fired. The resource attribute is verified.
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:
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."
}
}
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.
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.
All three mechanisms look similar. The differences:
| Mechanism | When it runs | Behavior on failure |
|---|---|---|
variable.validation | On the variable | plan error, before anything |
lifecycle.precondition | Before the resource is created or changed | plan error |
lifecycle.postcondition | After refresh, before writing state | apply error |
check { assert } | Alongside apply | warning, apply continues |
The rules:
variable.validation.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.
The most practical case for preconditions is the link between resources:
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.
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 planterraform applypostcondition is checked after refresh, before it is committed to stateконцепции