In today’s blog post, we will learn Terraform check block used to define custom conditions that execute on every Terraform plan or apply operation without affecting the overall status of an operation.
Terraform is widely used for defining and managing your infrastructure’s desired state. But what if you need to assert conditions about your infrastructure’s state or external services without halting your terraform plan or apply operations? What if you want continuous validation that provides warnings rather than immediate failures, especially for external dependency check, monitoring or compliance?
Answer, the check block, a new feature introduced in Terraform v1.5. The check block allows you to define custom conditions about your infrastructure, external data, or operational health. Unlike precondition or postcondition blocks (which cause an error if they fail), a failing check block results in a warning, allowing your Terraform operation to proceed while still alerting you for potential issues.
What is the Terraform check Block?
A check block defines an assertion that Terraform evaluates during terraform plan and terraform apply. It is a top-level block (like resource, variable, or output) and contains one or more assert blocks.
Key Characteristics:
- Non-Blocking by Default: If an
assertcondition within acheckblock evaluates tofalse, Terraform logs a warning but does not halt theplanorapplyoperation. This set it apart fromprecondition/postcondition. - System-Wide Validation:
checkblocks are ideal for assessing the overall health or compliance of your infrastructure, potentially spanning multiple resources or external data sources. - Proactive Monitoring: They can be used as a lightweight monitoring mechanism during deployments, flagging issues that might require attention but are not critical enough to stop the entire operation.
planandapplyEvaluation: Checks are evaluated during bothplanandapplyphases, depending on the data they reference.
Syntax of the check Block
The check block has a the following structure:
check "some_check_name" {
description = "A human-readable description of what this check verifies."
# Optional: Define data sources scoped *only* to this check
# data "some_type" "some_name" { ... }
assert {
condition = <boolean_expression>
error_message = "Message to display if the condition is false."
# Optional: A list of string expressions. If they change, the assert re-evaluates.
# If the `check` block itself has a `data` source, this might not be needed.
# triggers = [...]
}
# You can have multiple assert blocks within a single check block
assert {
condition = <another_boolean_expression>
error_message = "Another message."
}
}
check "some_check_name": Defines a check with a unique name.description: (Optional, but recommended) Explains the purpose of the check.- Scoped Data Sources: You can declare
datablocks directly within acheckblock. These data sources are “scoped,” meaning they are only evaluated when the check is run and are not available to other parts of your configuration. This is powerful for fetching external information relevant only to the check. assert: Contains the core validation logic.condition: A boolean expression that must evaluate totruefor the assertion to pass.error_message: The message displayed as a warning ifconditionevaluates tofalse.triggers: (Optional onassertblock) A list of values that, if changed, will cause this specificassertblock to get re-evaluated.
check Block Behavior: Warnings vs. Errors
The key differentiator for check blocks is their default non-blocking nature.
- If a
conditionin anassertblock isfalse:- Terraform reports a warning at the end of
terraform planorterraform apply. - The
planorapplyoperation continues to completion. - The overall exit code of
terraform planorapplyremains0(success) by default, even if warnings are present.
- Terraform reports a warning at the end of
How to make check warnings fail CI/CD:
If you want your CI/CD pipeline to fail when check blocks produce warnings, you can use the terraform plan -compact-warnings flag and check the exit code ($? in Bash). Alternatively, many CI/CD systems have their own mechanisms to detect warning messages in output and treat them as failures. For example, in GitHub Actions, you might use an expression to check the output for warning strings.
check Block vs. Other Validation Methods
Terraform check vs. validation vs. precondition vs. postcondition
| Validation Type | Scope | Behavior on Failure | Best For | Evaluation Phase |
|---|---|---|---|---|
variable validation | Input Variable | Error (blocks plan) | Ensuring variable inputs meet specific criteria. | plan (before resource evaluation) |
resource precondition | Resource-specific | Error (blocks plan) | Validating conditions before a resource is planned/applied. | plan and apply |
resource postcondition | Resource-specific | Error (blocks plan) | Validating conditions after a resource is planned/applied. | plan and apply |
check assert | Global/System-wide | Warning (continues) | Holistic infrastructure health, compliance, external conditions; non-blocking alerts. | plan and apply |
When to choose check blocks:
- You want to get alerts about potential issues but do not want to stop your entire deployment.
- You need to validate conditions that span multiple resources or involve external systems (e.g., checking DNS records, external API availability, certificate expiry).
- You are building a compliance framework where you want to be notified of non-compliant infrastructure configuration without blocking provisioning entirely.
- You want to create a lightweight “health check” that runs with every
plan/apply.
Practical Use Cases and Examples
Let us explore the power of the check block with some examples.
1. Validating External DNS Records
You have provisioned a load balancer, and you want to make sure its associated DNS record is resolvable, perhaps after a propagation delay.
# main.tf
resource "aws_lb" "web_lb" {
name = "my-web-lb"
internal = false
load_balancer_type = "application"
security_groups = [aws_security_group.web_sg.id]
subnets = ["subnet-0abcd", "subnet-0efgh"] # Replace with your subnets
}
resource "aws_security_group" "web_sg" {
name = "web_sg"
description = "Allow HTTP inbound traffic"
ingress {
from_port = 80
to_port = 80
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"]
}
}
# The check block
check "dns_resolution_status" {
description = "Verifies that the Load Balancer's DNS name resolves."
# Scoped data source to fetch DNS records. This data source is only run for this check.
data "external" "dns_lookup" {
program = ["dig", "+short", aws_lb.web_lb.dns_name] # Use 'dig' to lookup DNS
}
assert {
condition = length(data.external.dns_lookup.result.stdout) > 0
error_message = "The Load Balancer's DNS name does not appear to be resolving yet: ${aws_lb.web_lb.dns_name}. This may indicate a propagation delay or misconfiguration."
# Add depends_on if data.external.dns_lookup.result.stdout is "unknown after apply" initially
# depends_on = [aws_lb.web_lb]
}
}
Explanation:
- This check uses an
externaldata source (running thedigcommand) to query the DNS name of the newly created load balancer. - If
digreturns no output (meaning no DNS record found), theassertcondition isfalse, and a warning is displayed. - This would not stop the LB creation but will alert you if DNS is not working as expected.
2. Asserting Security Group Rule Granularity
Make sure security groups do not have overly permissive rules (e.g., 0.0.0.0/0 on common ports).
# main.tf
resource "aws_security_group" "database_sg" {
name = "database_sg"
description = "Security group for database access."
ingress {
from_port = 5432
to_port = 5432
protocol = "tcp"
cidr_blocks = ["0.0.0.0/0"] # This is intentionally permissive for the example
}
}
check "security_group_best_practices" {
description = "Ensures critical security groups do not have excessively open ingress rules."
assert {
condition = !contains(aws_security_group.database_sg.ingress.*.cidr_blocks, "0.0.0.0/0") || (
# Allow 0.0.0.0/0 only if port is outside common sensitive ports
!contains([22, 23, 80, 443, 3306, 5432], aws_security_group.database_sg.ingress[0].from_port)
)
error_message = "Security Group '${aws_security_group.database_sg.name}' has an excessively open ingress rule (0.0.0.0/0) on a sensitive port (${aws_security_group.database_sg.ingress[0].from_port}). Consider restricting CIDR blocks."
}
}
Explanation:
- This
checkwill issue a warning if thedatabase_sghas an ingress rule with0.0.0.0/0on port 5432. - It lets the security group be created (if you intended it temporarily) but flags it for review.
3. Checking Certificate Expiry (using tls_cert_request or similar)
This example is conceptual (to provide you an idea), as checking live certificate expiry often requires an external API or a custom data source.
# check "ssl_certificate_expiry" {
# description = "Checks if any critical SSL certificates are expiring soon."
# # This would typically use an external data source or a provider-specific resource
# # to fetch certificate details from a certificate manager or an endpoint.
# # For illustration, let us assume `data.cert_manager.my_cert` could expose `not_after_timestamp`
# # data "cert_manager" "my_cert" {
# # name = "production-web-cert"
# # }
# # For this example, let us just use a hardcoded value for demonstration:
# # (In real-world, replace with dynamic data like `data.cert_manager.my_cert.expiration_timestamp`)
# locals {
# cert_expiry_timestamp = 1767177600 # Example: Dec 31, 2025 00:00:00 GMT (Unix timestamp)
# days_until_expiry = floor((local.cert_expiry_timestamp - timestamp()) / (24 * 60 * 60))
# }
# assert {
# condition = local.days_until_expiry > 30
# error_message = "SSL certificate for 'production-web-cert' expires in less than 30 days (${local.days_until_expiry} days remaining). Please renew!"
# }
# }
Explanation:
- This conceptual check would use a data source to get certificate expiry dates.
- It would then assert that the certificate expires more than 30 days in the future.
- If it expires sooner, a warning appears, allowing you time to react before an outage.
Best Practices for check Blocks
- Use Meaningful Names and Descriptions: Make it easy to understand what each check block does.
- Focus on Warnings: Remember,
checkblocks are primarily for warnings. If a condition absolutely must be true for your infrastructure to function, usepreconditionorpostconditionon the relevant resource, orvariable validationfor inputs. - Scoped Data Sources: Use
datablocks insidecheckblocks for external information that is only relevant to the check itself. This keeps your main configuration clean. - Consider
depends_onfor Scoped Data Sources: If a scoped data source within acheckblock depends on a resource created in the sameapply(e.g., querying the DNS of a newly created LB), you might need an explicitdepends_onmeta-argument on thedatablock to make sure the resource is available before the data source tries to query it. Otherwise, you might get “known after apply” issues or errors on the firstplan. - Placement: Place
checkblocks in logical files (e.g.,checks.tfor in module rootmain.tf). - CI/CD Integration: Integrate
checkwarnings into your CI/CD pipeline’s alerting or failure criteria to act on them proactively. - Review Warnings Regularly: Do not ignore
checkwarnings. They are signals that something might need attention.
Conclusion
The Terraform check block is a new feature used for non-blocking validation. By asserting conditions about your infrastructure’s health, compliance, or external dependencies, you can build more compliant systems and catch potential issues before they become critical failures. Use the check block to add custom validations and monitoring in your Terraform workflow.
Author

Experienced Cloud & DevOps Engineer with hands-on experience in AWS, GCP, Terraform, Ansible, ELK, Docker, Git, GitLab, Python, PowerShell, Shell, and theoretical knowledge on Azure, Kubernetes & Jenkins.
In my free time, I write blogs on ckdbtech.com