Skip to content
ExponaBLOG
TERRAFORM, IAC & CLOUD AUTOMATION · DAY 096 / 100

Terraform check blocks: a practical guide for developers

Continuous assertions about infrastructure.

Expona Admin7 min read

Introduction

As infrastructure grows across environments, we often find ourselves wanting to validate that our Terraform deployments actually work—not just that they syntax-check, but that the real-world resources behave as expected. Terraform check blocks: a practical guide for developers explores a feature that lets us embed continuous assertions directly into our infrastructure code. Unlike traditional testing frameworks that run separately, check blocks execute during terraform plan and terraform apply, giving us immediate feedback on whether our infrastructure meets critical requirements.

Check blocks don't enforce hard constraints—they warn instead. This makes them ideal for smoke checks, health validations, and quality gates that should alert the team without stopping the deployment pipeline.

Check blocks run on plan and apply

Terraform check blocks are a relatively recent addition to the language, designed to run assertions at two critical moments: when you run terraform plan and again during terraform apply. Unlike validation blocks or preconditions, check blocks evaluate after resources are created or updated, giving them access to computed attributes and real output values.

A check block has a simple structure: it takes a condition expression and an optional error message. When the condition evaluates to false, Terraform logs a warning during planning and applies. The deployment continues regardless—this is intentional and important.

Consider a scenario where we provision an S3 bucket and want to verify it has versioning enabled:

hcl
resource "aws_s3_bucket" "app_data" {
  bucket = "my-app-data-${data.aws_caller_identity.current.account_id}"
}

resource "aws_s3_bucket_versioning" "app_data" {
  bucket = aws_s3_bucket.app_data.id
  versioning_configuration {
    status = "Enabled"
  }
}

check "s3_versioning_enabled" {
  assert {
    condition     = aws_s3_bucket_versioning.app_data.versioning_configuration[0].status == "Enabled"
    error_message = "S3 bucket versioning must be enabled for data integrity."
  }
}

When you run terraform plan, Terraform will evaluate the check. If versioning is enabled, it passes silently. If it's disabled, you see a clear warning in the plan output. On apply, the check runs again with the final state of resources.

Multiple checks can be defined in the same configuration file or across modules. Each runs independently, and the team can see all results in a single plan output.

Warn when endpoints are unhealthy

One of the most practical uses for check blocks is validating that newly deployed endpoints are actually healthy and reachable. After Terraform creates an application load balancer or API Gateway, we can use a check block to hit the endpoint and verify it responds.

Imagine deploying a containerized service behind an Application Load Balancer. We want to ensure the ALB DNS endpoint is responsive before marking the deployment complete:

hcl
resource "aws_lb" "api" {
  name               = "api-lb"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = aws_subnet.public[*].id
}

resource "aws_lb_target_group" "api" {
  name        = "api-targets"
  port        = 80
  protocol    = "HTTP"
  vpc_id      = aws_vpc.main.id
  health_check {
    healthy_threshold   = 2
    unhealthy_threshold = 2
    timeout             = 3
    interval            = 30
    path                = "/health"
    matcher             = "200"
  }
}

check "alb_endpoint_healthy" {
  assert {
    condition = (
      try(
        data.http.alb_health.status_code == 200,
        false
      )
    )
    error_message = "ALB endpoint is not returning a 200 status. Check target group health."
  }
}

data "http" "alb_health" {
  url = "http://${aws_lb.api.dns_name}/health"
  
  depends_on = [aws_lb_target_group_attachment.api]
}

This check block fetches the health endpoint after the load balancer is created. If the endpoint is unhealthy or unreachable, the team gets a warning during the plan phase itself. In many organizations, this early feedback prevents wasted time troubleshooting after apply.

We use try() to gracefully handle transient network errors or timeouts—if the HTTP request fails, the condition evaluates to false, triggering the warning.

Do not block applies

A critical distinction between check blocks and other validation mechanisms is that check blocks never halt the Terraform apply process. This is by design. While a resource validation that fails will prevent apply, a failed check only produces a warning.

This non-blocking nature makes check blocks suitable for advisory validations—scenarios where we want to alert the team to a potential issue but trust the team to make the final decision. For example, a check might warn that an RDS instance is not encrypted, but the apply continues. The engineer then sees the warning, reviews the configuration, and can manually remediate or accept the risk.

Here's an example where a check warns about unencrypted RDS storage:

hcl
resource "aws_db_instance" "main" {
  identifier     = "main-db"
  engine         = "postgres"
  instance_class = "db.t3.micro"
  username       = "admin"
  password       = random_password.db_password.result
  allocated_storage = 20
  storage_encrypted = var.enable_encryption ? true : false
}

check "rds_encryption_enabled" {
  assert {
    condition     = aws_db_instance.main.storage_encrypted == true
    error_message = "RDS instance is not encrypted. Enable storage_encrypted for production workloads."
  }
}

When storage_encrypted is false, the check warns but the database is still created. The team reviews the warning, understands the context (perhaps this is a non-production sandbox), and can proceed or adjust their variables for the next run.

In contrast, if we used a resource validation or precondition, the apply would fail immediately and Terraform would not proceed. Check blocks give us finer control over the severity of each assertion.

Great for smoke checks

Smoke tests are quick sanity checks that verify the most critical paths work. In infrastructure automation, smoke checks validate that core resources exist, are configured correctly, and are accessible. Check blocks are tailor-made for this use case.

A typical smoke check might verify:

  • A DNS record resolves to the correct IP
  • A firewall rule allows traffic on the expected port
  • An environment variable is set on a deployed container
  • A database connection string is correct
  • An API key is provisioned and has the right scopes

Let's build a realistic example where we deploy a containerized API and verify basic connectivity:

hcl
resource "aws_ecs_cluster" "app" {
  name = "app-cluster"
}

resource "aws_ecs_service" "api" {
  name            = "api-service"
  cluster         = aws_ecs_cluster.app.id
  task_definition = aws_ecs_task_definition.api.arn
  desired_count   = 2
  launch_type     = "FARGATE"
  network_configuration {
    subnets          = aws_subnet.private[*].id
    security_groups  = [aws_security_group.ecs.id]
    assign_public_ip = false
  }
  load_balancer {
    target_group_arn = aws_lb_target_group.api.arn
    container_name   = "api"
    container_port   = 8080
  }
}

check "api_service_running" {
  assert {
    condition     = aws_ecs_service.api.desired_count == aws_ecs_service.api.running_count
    error_message = "ECS service desired count does not match running count. Deployment may be unstable."
  }
}

check "api_dns_resolves" {
  assert {
    condition = (
      try(
        data.dns.api.addrs[0] != "",
        false
      )
    )
    error_message = "API DNS name does not resolve. Check Route53 configuration."
  }
}

data "dns_a_record_set" "api" {
  host = aws_lb.api.dns_name
}

These smoke checks run immediately after the infrastructure is created. They catch obvious issues—like a service failing to reach desired count or DNS misconfiguration—before the team depends on the infrastructure.

Smoke checks are especially valuable in CI/CD pipelines. A Terraform apply that succeeds but reports check block warnings can be surfaced to the team as a "success with caveats," allowing them to decide whether to investigate further or proceed with monitoring.

Common mistakes

Over-relying on check blocks instead of preconditions. Check blocks run after resources are created, while preconditions run before. If you need to prevent a deployment due to an invalid input variable, use a precondition, not a check block. Check blocks are for post-deployment assertions.

Using checks for hard constraints. If a condition must be true to proceed, don't use a check block. It will emit a warning but not stop the apply. Use resource validation or a policy-as-code tool like Sentinel or OPA if you need enforcement.

Writing overly complex assertions. Check blocks should validate simple, clear conditions. If your assertion needs nested try blocks, multiple data sources, or heavy computation, it belongs in a separate testing framework, not in your Terraform configuration.

Ignoring network flakiness in health checks. If your check block makes HTTP calls, always handle transient failures gracefully with try() and reasonable timeouts. A temporary network glitch shouldn't break the plan output.

Not documenting what checks mean. Make error messages explicit and actionable. Instead of "Check failed," write "Database replica lag exceeds 100ms. Review replication settings before proceeding to production."

Key takeaways

  • Check blocks evaluate after resources are created and run during both terraform plan and terraform apply.
  • They emit warnings but do not block deployments, making them ideal for advisory validations and quality gates.
  • Use check blocks for smoke tests: verifying endpoints are reachable, DNS resolves, targets are healthy, and critical configurations are in place.
  • Handle network operations in checks with try() and proper error handling to avoid false negatives.
  • Check blocks are complementary to preconditions and validation blocks—use the right tool for the right constraint.
  • Write clear, actionable error messages so the team understands what failed and why.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles