TerraForce is a powerful policy enforcement tool designed for Terraform, enabling teams to maintain consistency and compliance in their infrastructure-as-code practices.
TerraForce is distributed as a single binary. To install:
# Download the appropriate version for your platform
wget https://terraforce-builds-henrybravo-nl.s3.eu-west-1.amazonaws.com/linux/arm64/terraforce-1.0.0-arm64-linux.zip
# Unpack the archive
unzip -p terraforce-1.0.0-arm64-linux.zip | sudo tee terraforce > /dev/null
# Make the binary executable
chmod +x terraforce
# Verify the installation
terraforce --version
TerraForce's pre-plan stage requires your Terraform configuration files (HCL format) to be converted to JSON. The hcl2json tool simplifies this conversion process.
# Download hcl2json binary
wget https://hcl2json-builds-henrybravo-nl.s3.eu-west-1.amazonaws.com/hcl2json-1.0.0-arm64-linux.zip
# Unpack and install
unzip -p hcl2json-1.0.0-arm64-linux.zip | sudo tee hcl2json > /dev/null
chmod +x hcl2json
# Convert Terraform files found in current directory
% hcl2json -D .
# Verify conversion
% cat *.json
{
"resource": {
"aws_instance": {
"example": {
"instance_type": "t4g.medium",
"tags": {
"Name": "example-instance"
}
}
}
}
}
TerraForce policies are defined in YAML format and organized by lifecycle stages. Each policy consists of rules that are evaluated at specific points in the Terraform workflow.
# basic-policy.yml
pre_plan:
- name: "Required Tags"
description: "Ensure all resources have required tags"
condition: |
{{- $valid := true -}}
{{- range $name, $resource := .resource -}}
{{- if not (index $resource "tags") -}}
{{- $valid = false -}}
{{- end -}}
{{- end -}}
{{- $valid -}}
deny_message: "All resources must have tags defined"
# comprehensive-policy.yml
pre_plan:
- name: "Production Resource Policy"
description: "Enforce production environment standards"
condition: |
{{- /* Initialize variables */ -}}
{{- $valid := true -}}
{{- $errors := list -}}
{{- /* Define standards */ -}}
{{- $required_tags := list "Environment" "Owner" "CostCenter" -}}
{{- $allowed_instances := list "t3.medium" "t3.large" "t3.xlarge" -}}
{{- /* Check each resource */ -}}
{{- range $name, $resource := .resource -}}
{{- /* Verify tags exist */ -}}
{{- $tags := $resource.tags | default dict -}}
{{- range $required_tags -}}
{{- if not (index $tags . | default "") -}}
{{- $errors = append $errors (printf "Missing required tag: %s" .) -}}
{{- $valid = false -}}
{{- end -}}
{{- end -}}
{{- /* Check environment-specific rules */ -}}
{{- if eq (index $tags "Environment" | default "") "production" -}}
{{- /* Verify instance types */ -}}
{{- if eq $resource.type "aws_instance" -}}
{{- if not (contains $allowed_instances $resource.instance_type) -}}
{{- $errors = append $errors "Invalid instance type for production" -}}
{{- $valid = false -}}
{{- end -}}
{{- end -}}
{{- /* Verify encryption */ -}}
{{- if not $resource.encrypted -}}
{{- $errors = append $errors "Production resources must be encrypted" -}}
{{- $valid = false -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $valid -}}
deny_message: |
Production environment policy violations:
{{- range $error := $errors }}
- {{ $error }}
{{- end }}
TerraForce operates at four key stages of the Terraform lifecycle, each serving a specific purpose in policy enforcement.
# pre-plan policies validate configuration before planning
pre_plan:
- name: "Resource Naming Convention"
description: "Enforce naming standards before planning"
condition: |
{{- $valid := true -}}
{{- $pattern := "^[a-z]+(-[a-z0-9]+)*$" -}}
{{- range $name, $resource := .resource -}}
{{- if not (regexMatch $pattern $name) -}}
{{- $valid = false -}}
{{- end -}}
{{- end -}}
{{- $valid -}}
deny_message: "Resource names must use kebab-case"
Pre-plan checks validate:
# pre-apply policies validate planned changes
pre_apply:
- name: "Production Security Standards"
description: "Enforce security controls before applying changes"
condition: |
{{- $valid := true -}}
{{- range $name, $resource := .planned_resources -}}
{{- if eq $resource.environment "production" -}}
{{- if not $resource.encrypted -}}
{{- $valid = false -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $valid -}}
deny_message: "Production resources must be encrypted"
Pre-apply checks validate:
# post-apply policies validate the resulting state
post_apply:
- name: "Resource Compliance Check"
description: "Verify resource compliance after deployment"
condition: |
{{- $valid := true -}}
{{- range $name, $resource := .state_resources -}}
{{- /* Verify actual configuration matches expected */ -}}
{{- if not (eq $resource.actual_config $resource.expected_config) -}}
{{- $valid = false -}}
{{- end -}}
{{- end -}}
{{- $valid -}}
deny_message: "Resource configuration drift detected"
Post-apply checks validate:
# pre-destroy policies protect against unauthorized destruction
pre_destroy:
- name: "Protected Resource Check"
description: "Prevent destruction of critical resources"
condition: |
{{- $valid := true -}}
{{- range $name, $resource := .resources_to_destroy -}}
{{- if index $resource.tags "Protected" | default "" | eq "true" -}}
{{- $valid = false -}}
{{- end -}}
{{- end -}}
{{- $valid -}}
deny_message: "Cannot destroy protected resources"
Pre-destroy checks protect:
# Execute stage-specific checks
terraforce pre-plan policy.yml config.json
terraforce pre-apply policy.yml planfile.json
terraforce post-apply policy.yml state.json
terraforce pre-destroy policy.yml state.json
TerraForce uses Go's text/template package for policy conditions. Here are the key concepts and features:
{{- /* Basic template syntax */ -}}
{{ .Field }} # Access a field
{{ $var := .Field }} # Assign to variable
{{ if .Condition }} # Conditional
{{ range .Items }} # Iteration
{{ .SubField }} # Nested field access
{{ end }}
.
(dot) represents the current context$variable
creates a new variable$
accesses the root context from any scopeChain operations allow you to combine multiple functions using pipes (|) for cleaner, more maintainable templates.
condition: |
{{- /* Convert to lowercase, trim, and check pattern */ -}}
{{- $name := .resource_name | toLower | trim -}}
{{- regexMatch "^[a-z]+(-[a-z0-9]+)*$" $name -}}
condition: |
{{- /* Process tag values with multiple operations */ -}}
{{- $tags := .tags | default dict -}}
{{- $env := index $tags "Environment" | default "unknown" | toLower | trim -}}
{{- $owner := index $tags "Owner" | default "" | split "@" | first -}}
{{- /* Validate transformed values */ -}}
{{- $valid := and
(contains (list "prod" "stage" "dev") $env)
(ne $owner "")
-}}
{{- $valid -}}
Proper handling of optional fields and default values is crucial for robust policy conditions.
condition: |
{{- $tags := .tags | default dict -}}
{{- $env := index $tags "Environment" | default "unknown" -}}
{{- $owner := index $tags "Owner" | default "" -}}
{{- /* Validate with defaults */ -}}
{{- and (ne $env "unknown") (ne $owner "") -}}
condition: |
{{- /* Handle deeply nested values */ -}}
{{- $config := .configuration | default dict -}}
{{- $settings := index $config "settings" | default dict -}}
{{- $backup := index $settings "backup" | default dict -}}
{{- $retention := index $backup "retention_days" | default 7 -}}
{{- /* Validate with nested defaults */ -}}
{{- le $retention 30 -}}
Optimize template performance through efficient variable usage and conditional evaluation.
condition: |
{{- /* Cache frequently accessed values */ -}}
{{- $tags := .tags | default dict -}}
{{- $env := index $tags "Environment" | default "" -}}
{{- /* Use cached values */ -}}
{{- if eq $env "production" -}}
{{- /* Multiple checks using $env */ -}}
{{- end -}}
condition: |
{{- /* Pre-compute reusable lists */ -}}
{{- $allowed_types := list "t3.micro" "t3.small" "t3.medium" -}}
{{- /* Single pass validation */ -}}
{{- range $name, $resource := .resource -}}
{{- /* Multiple checks in single iteration */ -}}
{{- if and
(contains $allowed_types $resource.type)
(index $resource.tags "Environment")
(index $resource.tags "Owner")
-}}
{{- end -}}
{{- end -}}
Implement robust error handling in policy conditions to handle edge cases gracefully.
condition: |
{{- /* Initialize error collection */ -}}
{{- $valid := true -}}
{{- $errors := list -}}
{{- /* Safe map access */ -}}
{{- $tags := .tags | default dict -}}
{{- $env := index $tags "Environment" | default "unknown" -}}
{{- /* Type conversion with validation */ -}}
{{- $size := 0 -}}
{{- if $sizeStr := .instance_size -}}
{{- if $converted := toInt $sizeStr | default 0 -}}
{{- $size = $converted -}}
{{- else -}}
{{- $errors = append $errors "Invalid size format" -}}
{{- end -}}
{{- end -}}
{{- /* Validate results */ -}}
{{- if and (ne $env "unknown") (gt $size 0) -}}
{{- $valid = true -}}
{{- else -}}
{{- $errors = append $errors "Missing required fields" -}}
{{- end -}}
{{- $valid -}}
deny_message: |
Validation errors:
{{- range $error := $errors }}
- {{ $error }}
{{- end }}
Break down complex policy conditions into manageable, reusable components.
condition: |
{{- /* Initialize state */ -}}
{{- $valid := true -}}
{{- $errors := list -}}
{{- /* Define reusable validation rules */ -}}
{{- $validateTags := dict
"Environment" (list "prod" "stage" "dev")
"CostCenter" (list "eng" "ops" "infra")
"Owner" (list "team-a" "team-b" "team-c")
-}}
{{- /* Extract commonly used values */ -}}
{{- $resource := . -}}
{{- $tags := $resource.tags | default dict -}}
{{- $env := index $tags "Environment" | default "" | toLower -}}
{{- /* Environment-specific rules */ -}}
{{- $envRules := dict
"prod" (dict
"min_size" "t3.medium"
"encryption" true
"backup" true
)
"stage" (dict
"min_size" "t3.small"
"encryption" true
"backup" false
)
-}}
{{- /* Validation blocks */ -}}
{{- /* 1. Tag validation */ -}}
{{- range $tag, $allowed := $validateTags -}}
{{- $value := index $tags $tag | default "" -}}
{{- if not (contains $allowed $value) -}}
{{- $errors = append $errors (printf "Invalid %s: %s" $tag $value) -}}
{{- $valid = false -}}
{{- end -}}
{{- end -}}
{{- /* 2. Environment-specific validation */ -}}
{{- if $rules := index $envRules $env | default dict -}}
{{- /* Size check */ -}}
{{- if lt $resource.instance_type $rules.min_size -}}
{{- $errors = append $errors (printf "Instance type %s below minimum %s for %s"
$resource.instance_type $rules.min_size $env) -}}
{{- $valid = false -}}
{{- end -}}
{{- /* Security checks */ -}}
{{- if and $rules.encryption (not $resource.encrypted) -}}
{{- $errors = append $errors "Encryption required" -}}
{{- $valid = false -}}
{{- end -}}
{{- end -}}
{{- $valid -}}
deny_message: |
Validation failures:
{{- range $error := $errors }}
- {{ $error }}
{{- end }}
Function | Description | Arguments | Example | Result |
---|---|---|---|---|
add |
Addition | (a, b int) | {{ add 1 2 }} |
3 |
subtract |
Subtraction | (a, b int) | {{ subtract 5 2 }} |
3 |
multiply |
Multiplication | (a, b int) | {{ multiply 2 3 }} |
6 |
divide |
Division | (a, b int) | {{ divide 6 2 }} |
3 |
mod |
Modulo | (a, b int) | {{ mod 7 3 }} |
1 |
Function | Description | Arguments | Example | Result |
---|---|---|---|---|
toLower |
Convert to lowercase | (s string) | {{ toLower "TEST" }} |
"test" |
toUpper |
Convert to uppercase | (s string) | {{ toUpper "test" }} |
"TEST" |
trim |
Remove whitespace | (s string) | {{ trim " test " }} |
"test" |
hasPrefix |
Check string prefix | (s, prefix string) | {{ hasPrefix "test" "te" }} |
true |
hasSuffix |
Check string suffix | (s, suffix string) | {{ hasSuffix "test" "st" }} |
true |
contains |
Check substring | (s, substr string) | {{ contains "test" "es" }} |
true |
split |
Split string | (s, sep string) | {{ split "a,b" "," }} |
["a", "b"] |
join |
Join strings | (elements []string, sep string) | {{ join .tags "," }} |
"tag1,tag2" |
replace |
Replace n occurrences | (s, old, new string, n int) | {{ replace "test" "t" "x" 1 }} |
"xest" |
replaceAll |
Replace all occurrences | (s, old, new string) | {{ replaceAll "test" "t" "x" }} |
"xesx" |
Function | Description | Arguments | Example | Result |
---|---|---|---|---|
toString |
Convert to string | (v interface{}) | {{ toString 123 }} |
"123" |
toInt |
Convert to integer | (v interface{}) | {{ toInt "123" }} |
123 |
toBool |
Convert to boolean | (v interface{}) | {{ toBool "true" }} |
true |
Function | Description | Arguments | Example | Result |
---|---|---|---|---|
length |
Get collection length | (v interface{}) | {{ length .items }} |
3 |
len |
Get collection length | (v interface{}) | {{ len .items }} |
3 |
keys |
Get map keys (sorted) | (m map[string]interface{}) | {{ keys .tags }} |
["env", "name"] |
values |
Get map values | (m map[string]interface{}) | {{ values .tags }} |
["prod", "webserver"] |
list |
Create a list | (...interface{}) | {{ list "a" "b" "c" }} |
["a", "b", "c"] |
dict |
Create a dictionary | (interface{}) | {{ dict "key" "value" }} |
{"key": "value"} |
index |
Access map/slice element | (collection, key interface{}) | {{ index .tags "Environment" }} |
"prod" |
append |
Append to list | (slice []interface{}, v ...interface{}) | {{ append .list "new" }} |
[old, "new"] |
Function | Description | Arguments | Example | Result |
---|---|---|---|---|
regexMatch |
Pattern matching | (pattern, s string) | {{ regexMatch "^t.*t$" "test" }} |
true |
regexFind |
Find first match | (pattern, s string) | {{ regexFind "[0-9]+" "abc123def" }} |
"123" |
regexFindAll |
Find all matches | (pattern, s string, n int) | {{ regexFindAll "[0-9]+" "123 456" -1 }} |
["123", "456"] |
regexReplace |
Replace matches | (pattern, repl, s string) | {{ regexReplace "\\d+" "NUM" "abc123" }} |
"abcNUM" |
Function | Description | Arguments | Example | Result |
---|---|---|---|---|
eq |
Equal to | (a, b interface{}) | {{ eq .env "prod" }} |
true |
ne |
Not equal to | (a, b interface{}) | {{ ne .env "dev" }} |
true |
lt |
Less than | (a, b interface{}) | {{ lt 2 3 }} |
true |
le |
Less than or equal | (a, b interface{}) | {{ le 2 2 }} |
true |
gt |
Greater than | (a, b interface{}) | {{ gt 3 2 }} |
true |
ge |
Greater than or equal | (a, b interface{}) | {{ ge 2 2 }} |
true |
Function | Description | Arguments | Example | Result |
---|---|---|---|---|
debug |
Get detailed value representation | (v interface{}) | {{ debug .tags }} |
map[string]string{"env":"prod"} |
typeOf |
Get type information | (v interface{}) | {{ typeOf .count }} |
"int" |
pre_plan:
- name: "Resource Naming Policy"
description: "Enforce consistent resource naming"
condition: |
{{- $valid := true -}}
{{- $pattern := "^[a-z]+(-[a-z0-9]+)*$" -}}
{{- range $name, $resource := .resource -}}
{{- if not (regexMatch $pattern $name) -}}
{{- $valid = false -}}
{{- end -}}
{{- end -}}
{{- $valid -}}
deny_message: "Resource names must follow kebab-case pattern"
TerraForce includes a built-in linter to validate policy files before execution. Although the linter checks may return linting issues, and the issues are related to long conditions, terraforce will still execute using the policy if there are no errors in the policy definition.
# Lint a single policy file
terraforce lint policy.yml
# Lint all policies in a directory
terraforce lint --dir ./policies
Rule | Description | Example Error |
---|---|---|
Valid YAML | Checks for valid YAML syntax | "Invalid YAML at line 5: mapping values are not allowed here" |
Template Syntax | Validates Go template expressions | "Unclosed template action in condition" |
Required Fields | Ensures all required fields are present | "Missing required field 'deny_message' in rule 'aws_tags'" |
Function Usage | Verifies only valid functions are used | "Unknown function 'invalidFunc' at line 12" |
Common issues you might encounter when using TerraForce and how to resolve them.
Error | Cause | Solution |
---|---|---|
template: pattern matches multiple files | Malformed template syntax in policy condition | Check for matching {{ and }} brackets |
failed to load policy | Invalid YAML format or file permissions | Validate YAML syntax and file permissions |
failed to parse input | Invalid JSON format in Terraform files | Verify JSON format of plan/state files |
function "foo" not defined | Using undefined template function | Check function reference for available functions |
# Enable debug output
terraforce -d pre-plan policy.yml config.json
# Output includes:
# - Template evaluation details
# - Variable values
# - Resource processing steps
# - Error traces
pre_plan:
- name: "Debug Example"
condition: |
{{- /* Debug variable values */ -}}
{{- $debug := true -}}
{{- $value := .some_field -}}
{{- if $debug -}}
{{- printf "Processing value: %v\n" $value -}}
{{- end -}}
{{- /* Track execution path */ -}}
{{- if eq $value "expected" -}}
{{- if $debug -}}{{- printf "Path A taken\n" -}}{{- end -}}
true
{{- else -}}
{{- if $debug -}}{{- printf "Path B taken\n" -}}{{- end -}}
false
{{- end -}}
pre_plan:
- name: "Resource Tag Validator"
condition: |
{{- /* Initialize debugging */ -}}
{{- $valid := true -}}
{{- $required_tags := list "Environment" "Owner" -}}
{{- printf "Required tags: %v\n" $required_tags -}}
{{- /* Process resources */ -}}
{{- range $name, $resource := .resource -}}
{{- printf "Checking resource: %s\n" $name -}}
{{- $tags := $resource.tags | default dict -}}
{{- printf "Resource tags: %v\n" $tags -}}
{{- range $required_tags -}}
{{- $tag := . -}}
{{- if not (index $tags $tag | default "") -}}
{{- printf "Missing required tag: %s\n" $tag -}}
{{- $valid = false -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- printf "Final validation result: %v\n" $valid -}}
{{- $valid -}}
deny_message: "Resources missing required tags"
Real-world policy examples demonstrating common use cases and best practices.
# aws-security-policy.yml
pre_plan:
- name: "AWS Security Baseline"
description: "Enforce AWS security standards"
condition: |
{{- /* Initialize validation */ -}}
{{- $valid := true -}}
{{- $errors := list -}}
{{- /* Security standards */ -}}
{{- $allowed_instance_types := list "t3.micro" "t3.small" "t3.medium" -}}
{{- $required_tags := list "Environment" "Owner" "CostCenter" "SecurityLevel" -}}
{{- $allowed_regions := list "eu-west-1" "eu-central-1" -}}
{{- /* Check each resource */ -}}
{{- range $name, $resource := .resource -}}
{{- if eq $resource.type "aws_instance" -}}
{{- /* Instance type validation */ -}}
{{- if not (contains $allowed_instance_types $resource.instance_type) -}}
{{- $errors = append $errors (printf "Instance %s: unauthorized type %s" $name $resource.instance_type) -}}
{{- $valid = false -}}
{{- end -}}
{{- /* Encryption validation */ -}}
{{- if not $resource.ebs_block_device.encrypted -}}
{{- $errors = append $errors (printf "Instance %s: EBS volumes must be encrypted" $name) -}}
{{- $valid = false -}}
{{- end -}}
{{- /* Security group validation */ -}}
{{- range $resource.security_group_ids -}}
{{- if eq . "sg-0000000000" -}}
{{- $errors = append $errors (printf "Instance %s: default security group not allowed" $name) -}}
{{- $valid = false -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- /* Tag validation */ -}}
{{- $tags := $resource.tags | default dict -}}
{{- range $required_tags -}}
{{- if not (index $tags . | default "") -}}
{{- $errors = append $errors (printf "Resource %s: missing required tag %s" $name .) -}}
{{- $valid = false -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $valid -}}
deny_message: |
Security policy violations:
{{- range $error := $errors }}
- {{ $error }}
{{- end }}
# network-policy.yml
pre_plan:
- name: "Network Security Standards"
description: "Enforce network security across environments"
condition: |
{{- /* Initialize */ -}}
{{- $valid := true -}}
{{- $errors := list -}}
{{- /* Network rules by environment */ -}}
{{- $network_rules := dict -}}
{{- $_ := set $network_rules "production" (dict
"allowed_cidrs" (list "10.0.0.0/8" "172.16.0.0/12")
"required_ports" (list "80" "443")
"forbidden_ports" (list "22" "3389")
) -}}
{{- $_ := set $network_rules "staging" (dict
"allowed_cidrs" (list "10.0.0.0/8" "192.168.0.0/16")
"required_ports" (list "80" "443")
"forbidden_ports" (list "3389")
) -}}
{{- /* Process resources */ -}}
{{- range $name, $resource := .resource -}}
{{- if eq $resource.type "aws_security_group" -}}
{{- $env := index $resource.tags "Environment" | default "unknown" | toLower -}}
{{- $rules := index $network_rules $env | default dict -}}
{{- /* Check ingress rules */ -}}
{{- range $rule := $resource.ingress -}}
{{- /* Check CIDR blocks */ -}}
{{- range $cidr := $rule.cidr_blocks -}}
{{- $cidr_valid := false -}}
{{- range $allowed := $rules.allowed_cidrs -}}
{{- if hasPrefix $cidr $allowed -}}
{{- $cidr_valid = true -}}
{{- end -}}
{{- end -}}
{{- if not $cidr_valid -}}
{{- $errors = append $errors (printf "SG %s: unauthorized CIDR %s" $name $cidr) -}}
{{- $valid = false -}}
{{- end -}}
{{- end -}}
{{- /* Check ports */ -}}
{{- range $port := $rules.forbidden_ports -}}
{{- if and (le $rule.from_port (int $port)) (ge $rule.to_port (int $port)) -}}
{{- $errors = append $errors (printf "SG %s: forbidden port %s" $name $port) -}}
{{- $valid = false -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $valid -}}
deny_message: |
Network policy violations:
{{- range $error := $errors }}
- {{ $error }}
{{- end }}
# cost-control-policy.yml
pre_plan:
- name: "Cost Control Policy"
description: "Enforce cost controls across resources"
condition: |
{{- /* Initialize */ -}}
{{- $valid := true -}}
{{- $errors := list -}}
{{- /* Cost limits by environment */ -}}
{{- $limits := dict -}}
{{- $_ := set $limits "development" (dict
"max_instance_size" "t3.medium"
"max_volume_size" 100
"max_retention_days" 7
) -}}
{{- $_ := set $limits "staging" (dict
"max_instance_size" "t3.large"
"max_volume_size" 500
"max_retention_days" 14
) -}}
{{- $_ := set $limits "production" (dict
"max_instance_size" "t3.2xlarge"
"max_volume_size" 1000
"max_retention_days" 30
) -}}
{{- /* Resource size mappings */ -}}
{{- $instance_sizes := list "t3.micro" "t3.small" "t3.medium" "t3.large" "t3.xlarge" "t3.2xlarge" -}}
{{- /* Process resources */ -}}
{{- range $name, $resource := .resource -}}
{{- $env := index $resource.tags "Environment" | default "development" | toLower -}}
{{- $env_limits := index $limits $env -}}
{{- /* EC2 instance checks */ -}}
{{- if eq $resource.type "aws_instance" -}}
{{- $size_index := -1 -}}
{{- $max_size_index := -1 -}}
{{- range $i, $size := $instance_sizes -}}
{{- if eq $resource.instance_type $size -}}
{{- $size_index = $i -}}
{{- end -}}
{{- if eq $env_limits.max_instance_size $size -}}
{{- $max_size_index = $i -}}
{{- end -}}
{{- end -}}
{{- if gt $size_index $max_size_index -}}
{{- $errors = append $errors (printf "Instance %s: size %s exceeds limit %s for %s"
$name $resource.instance_type $env_limits.max_instance_size $env) -}}
{{- $valid = false -}}
{{- end -}}
{{- end -}}
{{- /* EBS volume checks */ -}}
{{- if eq $resource.type "aws_ebs_volume" -}}
{{- if gt $resource.size $env_limits.max_volume_size -}}
{{- $errors = append $errors (printf "Volume %s: size %dGB exceeds limit %dGB for %s"
$name $resource.size $env_limits.max_volume_size $env) -}}
{{- $valid = false -}}
{{- end -}}
{{- end -}}
{{- end -}}
{{- $valid -}}
deny_message: |
Cost policy violations:
{{- range $error := $errors }}
- {{ $error }}
{{- end }}