Mastering Terraform Check Block

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 a check block evaluates to false, Terraform logs a warning but does not halt the plan or apply operation. This set it apart from precondition/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 and apply Evaluation: Checks are evaluated during both plan and apply 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 a check 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 to true for the assertion to pass.
    • error_message: The message displayed as a warning if condition evaluates to false.
    • triggers: (Optional on assert block) A list of values that, if changed, will cause this specific assert 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 an assert block is false:
    • Terraform reports a warning at the end of terraform plan or terraform apply.
    • The plan or apply operation continues to completion.
    • The overall exit code of terraform plan or apply remains 0 (success) by default, even if warnings are present.

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 TypeScopeBehavior on FailureBest ForEvaluation Phase
variable validationInput VariableError (blocks plan)Ensuring variable inputs meet specific criteria.plan (before resource evaluation)
resource preconditionResource-specificError (blocks plan)Validating conditions before a resource is planned/applied.plan and apply
resource postconditionResource-specificError (blocks plan)Validating conditions after a resource is planned/applied.plan and apply
check assertGlobal/System-wideWarning (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 the dig command) to query the DNS name of the newly created load balancer.
  • If dig returns no output (meaning no DNS record found), the assert condition is false, 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 the database_sg has an ingress rule with 0.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

  1. Use Meaningful Names and Descriptions: Make it easy to understand what each check block does.
  2. Focus on Warnings: Remember, check blocks are primarily for warnings. If a condition absolutely must be true for your infrastructure to function, use precondition or postcondition on the relevant resource, or variable validation for inputs.
  3. Scoped Data Sources: Use data blocks inside check blocks for external information that is only relevant to the check itself. This keeps your main configuration clean.
  4. Consider depends_on for Scoped Data Sources: If a scoped data source within a check block depends on a resource created in the same apply (e.g., querying the DNS of a newly created LB), you might need an explicit depends_on meta-argument on the data 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 first plan.
  5. Placement: Place check blocks in logical files (e.g., checks.tf or in module root main.tf).
  6. CI/CD Integration: Integrate check warnings into your CI/CD pipeline’s alerting or failure criteria to act on them proactively.
  7. 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

Debjeet Bhowmik

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

Leave a Comment