Blog Series: Enforcing Policy as Code in Terraform (Part 2 of 5)
Getting Started: Native Validation and Dedicated Tools
Welcome back to our series on Policy as Code (PaC) with Terraform! In Part 1, we explored what PaC is, why it's crucial for modern infrastructure management, and the core components of a PaC framework. We established that PaC helps ensure your infrastructure is secure, compliant, cost-effective, and operationally consistent by codifying your governance rules.
Now, let's roll up our sleeves and see how we can start implementing some of these checks. We'll begin by looking at the validation capabilities built directly into Terraform. While these native features offer a good starting point, we'll also see why more specialized tools are often needed for comprehensive PaC.
Terraform's Built-in Guards: Native Validation Features
Terraform itself isn't just about provisioning; it also provides mechanisms to validate your configurations directly within your HCL (HashiCorp Configuration Language) code. These features act as an initial line of defense, helping module authors enforce contracts and users catch basic errors early.
1. Custom Conditions: precondition
and postcondition
Terraform allows you to define custom validation rules for your resources, data sources, and outputs using precondition
and postcondition
blocks.
precondition
Blocks: These are evaluated before Terraform processes the object they're attached to (like a resource or an output). Think of them as checks to ensure certain assumptions about your configuration or environment are true before an action is taken.- Usage: Ideal for validating input variables to a module or checking if an AMI has the correct architecture before an instance is created.
- Syntax: Defined within a
lifecycle
block for resources/data sources, or directly in anoutput
block. Each requires acondition
(an HCL expression that must betrue
) and anerror_message
(displayed iffalse
).
postcondition
Blocks: These are evaluated after an object has been processed (e.g., a resource is provisioned or a data source is read). They verify guarantees about the resulting state or attributes.If aprecondition
fails, Terraform halts further processing for that object. A failedpostcondition
indicates the outcome didn't meet expectations.- Usage: Useful for ensuring a created resource has specific properties, like an S3 bucket having versioning enabled after creation.
- Syntax: Similar to preconditions.
Example (Conceptual):
resource "aws_s3_bucket" "example" {
bucket = "my-unique-bucket-example-12345"
lifecycle {
postcondition {
condition = self.versioning[0].enabled == true
error_message = "S3 bucket versioning was not enabled as expected."
}
}
}
Example (Conceptual):
resource "aws_instance" "example" {
ami = var.ami_id
instance_type = var.instance_type
lifecycle {
precondition {
condition = can(regex("^ami-", var.ami_id))
error_message = "The provided AMI ID must start with 'ami-'."
}
precondition {
condition = var.instance_type == "t3.micro" || var.instance_type == "t2.micro"
error_message = "Only t3.micro or t2.micro instance types are allowed for this module."
}
}
}
2. check
Blocks and Assertions
Introduced in Terraform v1.5.0, check
blocks offer another way to validate your infrastructure, primarily focused on the overall state rather than individual resource lifecycles.
- Usage: Defined at the root level of your configuration,
check
blocks contain one or moreassert
blocks. Eachassert
has acondition
and anerror_message
. - Key Difference: Unlike preconditions/postconditions that can halt operations,
check
blocks typically produce warnings if an assertion fails and don't automatically stop aplan
orapply
. This makes them suitable for ongoing validation or health checks of existing infrastructure. Terraform Cloud can continuously validate these checks.
Example (Conceptual):
check "all_s3_buckets_have_logging" {
assert {
condition = alltrue([
for bucket in aws_s3_bucket.all : bucket.logging != null && bucket.logging[0].target_bucket != null
]) // This is a simplified example; actual access to all S3 buckets might require data sources or more complex iteration
error_message = "Not all S3 buckets have server access logging configured."
}
}
These native features are great for enforcing module contracts and catching specific misconfigurations directly within your HCL code, embodying a "fail-fast" principle.
The Limits of Native Powers
While valuable, Terraform's built-in validation mechanisms have limitations when it comes to implementing a comprehensive, organization-wide Policy as Code strategy:
- Limited Scope and Expressiveness: Native features are best for validating individual resources or simple conditions. They aren't designed for complex, cross-cutting organizational rules that might involve intricate logic, span multiple resources, or require external data. HCL is an infrastructure declaration language, not a general-purpose policy language.
- Lack of Centralized Management: Policies defined using these native features are embedded within the Terraform files themselves. This makes it hard to manage, version, audit, and apply policies consistently across many modules, projects, or teams. True PaC thrives on centralized policy libraries.
- Blurred Separation of Concerns: Effective PaC often separates infrastructure logic from policy logic. Native conditions mix these, as policy assertions become part of the infrastructure code.
- Inability to Prevent Operations Based on Holistic Checks: While
precondition
can block individual resource operations, andcheck
blocks provide warnings, there isn't a robust native way to halt an entireapply
based on a holistic evaluation of the overall proposed infrastructure state against a complex suite of organizational policies.
These limitations highlight the need for more powerful, dedicated tools to achieve mature Policy as Code.
Leveling Up: The World of Dedicated PaC Tools
When your policy needs outgrow Terraform's native capabilities, dedicated PaC tools step in. These tools offer:
- Expressive Policy Languages: Designed specifically for writing complex rules (e.g., Rego, Sentinel HSL, Python).
- Centralized Policy Management: Allowing you to store, version, and distribute policies independently of your Terraform code.
- Sophisticated Evaluation Engines: Capable of analyzing Terraform plans (often in JSON format) to understand the full context of proposed changes.
- Integration Flexibility: Designed to plug into various stages of your CI/CD pipeline.
These tools generally fall into a few categories:
- General-Purpose Policy Engines: Like Open Policy Agent (OPA), which can enforce policies across a wide variety of software and systems, not just Terraform.
- Integrated Frameworks: Like HashiCorp Sentinel, which is tightly integrated into Terraform Cloud and Enterprise.
- Static Analysis Tools: Like tfsec and Checkov, which specialize in scanning IaC files for security misconfigurations and compliance violations, often with large libraries of pre-built rules.
These dedicated tools empower you to define far more granular and context-aware policies, truly embedding governance into your automated workflows.
In Part 3 of our series, we'll begin our deep dive into these dedicated PaC tools, starting with a closer look at Open Policy Agent (OPA) and how it can be used with conftest
to validate your Terraform plans. Stay tuned!