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
assert
condition within acheck
block evaluates tofalse
, Terraform logs a warning but does not halt theplan
orapply
operation. This set it apart fromprecondition
/postcondition
. - System-Wide Validation:
check
blocks 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.
plan
andapply
Evaluation: Checks are evaluated during bothplan
andapply
phases, 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
data
blocks directly within acheck
block. 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 totrue
for the assertion to pass.error_message
: The message displayed as a warning ifcondition
evaluates tofalse
.triggers
: (Optional onassert
block) A list of values that, if changed, will cause this specificassert
block to get re-evaluated.
check Block Behavior: Warnings vs. Errors
The key differentiator for check
blocks is their default non-blocking nature.
- If a
condition
in anassert
block isfalse
:- Terraform reports a warning at the end of
terraform plan
orterraform apply
. - The
plan
orapply
operation continues to completion. - The overall exit code of
terraform plan
orapply
remains0
(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
external
data source (running thedig
command) to query the DNS name of the newly created load balancer. - If
dig
returns no output (meaning no DNS record found), theassert
condition 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
check
will issue a warning if thedatabase_sg
has an ingress rule with0.0.0.0/0
on 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,
check
blocks are primarily for warnings. If a condition absolutely must be true for your infrastructure to function, useprecondition
orpostcondition
on the relevant resource, orvariable validation
for inputs. - Scoped Data Sources: Use
data
blocks insidecheck
blocks for external information that is only relevant to the check itself. This keeps your main configuration clean. - Consider
depends_on
for Scoped Data Sources: If a scoped data source within acheck
block depends on a resource created in the sameapply
(e.g., querying the DNS of a newly created LB), you might need an explicitdepends_on
meta-argument on thedata
block 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
check
blocks in logical files (e.g.,checks.tf
or in module rootmain.tf
). - CI/CD Integration: Integrate
check
warnings into your CI/CD pipeline’s alerting or failure criteria to act on them proactively. - Review Warnings Regularly: Do not ignore
check
warnings. 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