Static Validation Patterns#

Static validation catches infrastructure errors before anything is deployed. No cluster needed, no cloud credentials needed, no cost incurred. These tools analyze configuration files – Helm charts, Kubernetes manifests, Terraform modules, Kustomize overlays – and report problems that would cause failures at deploy time.

Static validation does not replace integration testing. It cannot verify that a service starts successfully, that a pod can pull its image, or that a database accepts connections. What it catches are structural errors: malformed YAML, invalid API versions, missing required fields, policy violations, deprecated resources, and misconfigured values. In practice, this covers roughly 40% of infrastructure issues – the ones that are cheapest to find and cheapest to fix.

The tools in this article are ordered from simplest to most powerful. Each section covers what the tool catches, what it misses, how to install it, and practical command examples.

Helm Lint and Template#

What It Catches#

helm lint validates chart structure: Chart.yaml is present and valid, templates parse correctly, values.yaml defaults produce valid templates. helm template renders templates to raw Kubernetes manifests without contacting a cluster, exposing rendering errors, missing values, and incorrect template logic.

What It Misses#

Helm lint does not validate that the rendered manifests are valid Kubernetes resources. A template that produces apiVersion: v1 with kind: Nonexistent passes helm lint. It also does not evaluate whether resource requests are reasonable, whether image tags exist, or whether RBAC permissions are sufficient.

Installation#

# Helm is typically already installed. If not:
curl https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 | bash

Commands#

# Lint a chart (catches structural issues)
helm lint ./my-chart/

# Lint with specific values (catches value-dependent issues)
helm lint ./my-chart/ -f values-production.yaml

# Lint strict mode (treats warnings as errors)
helm lint ./my-chart/ --strict

# Render templates without deploying (catches template rendering errors)
helm template my-release ./my-chart/ > /dev/null

# Render with specific values and inspect output
helm template my-release ./my-chart/ -f values-production.yaml

# Render a single template for focused debugging
helm template my-release ./my-chart/ --show-only templates/deployment.yaml

# Render and pipe to kubeconform for manifest validation
helm template my-release ./my-chart/ | kubeconform -strict -

Common Issues Found#

  • Template function errors ({{ .Values.missing.key }} when missing is not defined)
  • YAML indentation errors in template output
  • Chart.yaml missing required fields (name, version, apiVersion)
  • Subchart dependency issues (missing charts/ directory or Chart.lock)

Kubeconform (Manifest Schema Validation)#

What It Catches#

Kubeconform validates Kubernetes manifests against the OpenAPI schemas published for each Kubernetes version. It catches invalid field names, wrong field types, missing required fields, and resources using removed API versions. It replaces the unmaintained kubeval.

What It Misses#

Schema validation verifies structure, not semantics. A Deployment with replicas: -5 passes because -5 is a valid integer. A PodSpec referencing a nonexistent ConfigMap passes because cross-resource references are not checked.

Installation#

# Binary download
curl -fsSL https://github.com/yannh/kubeconform/releases/latest/download/kubeconform-linux-amd64.tar.gz \
  | tar xz -C /usr/local/bin

# Homebrew (macOS)
brew install kubeconform

# Go install
go install github.com/yannh/kubeconform/cmd/kubeconform@latest

Commands#

# Validate a single file
kubeconform deployment.yaml

# Validate all YAML files in a directory
kubeconform -strict manifests/

# Validate against a specific Kubernetes version
kubeconform -kubernetes-version 1.29.0 -strict manifests/

# Validate with summary output
kubeconform -summary manifests/

# Validate Helm template output
helm template my-release ./my-chart/ | kubeconform -strict -

# Validate with custom resource definitions (CRDs)
# Download CRD schemas first, then:
kubeconform -schema-location default \
  -schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' \
  -strict manifests/

# Ignore specific resources that lack schemas (custom CRDs without published schemas)
kubeconform -strict -skip Certificate,IngressRoute manifests/

Common Issues Found#

  • apiVersion: extensions/v1beta1 (removed in Kubernetes 1.22)
  • spec.template.spec.containers[0].port instead of ports (wrong field name)
  • Missing metadata.name on resources
  • spec.replicas as a string instead of integer

Conftest and OPA (Policy-as-Code)#

What It Catches#

Conftest evaluates configuration files against policies written in Rego (OPA’s policy language). Unlike schema validation which checks structure, policy validation checks intent: do all containers have resource limits? Do images come from approved registries? Are labels present for cost tracking?

What It Misses#

Conftest only evaluates what the policies check. If you do not write a policy for something, it is not checked. It also cannot validate runtime behavior – a policy can require that a health check is defined, but cannot verify the health check works.

Installation#

# Binary download
curl -fsSL https://github.com/open-policy-agent/conftest/releases/latest/download/conftest_Linux_x86_64.tar.gz \
  | tar xz -C /usr/local/bin conftest

# Homebrew
brew install conftest

# Go install
go install github.com/open-policy-agent/conftest@latest

Policy Examples#

Create a policy/ directory with Rego files:

# policy/kubernetes.rego
package main

# Deny containers without resource limits
deny[msg] {
  input.kind == "Deployment"
  container := input.spec.template.spec.containers[_]
  not container.resources.limits
  msg := sprintf("Container '%s' in Deployment '%s' has no resource limits", [container.name, input.metadata.name])
}

# Deny images from untrusted registries
deny[msg] {
  input.kind == "Deployment"
  container := input.spec.template.spec.containers[_]
  not startswith(container.image, "gcr.io/")
  not startswith(container.image, "ghcr.io/")
  msg := sprintf("Container '%s' uses untrusted image '%s'", [container.name, container.image])
}

# Warn if no liveness probe
warn[msg] {
  input.kind == "Deployment"
  container := input.spec.template.spec.containers[_]
  not container.livenessProbe
  msg := sprintf("Container '%s' has no liveness probe", [container.name])
}

Commands#

# Test Kubernetes manifests against policies
conftest test deployment.yaml --policy policy/

# Test all manifests in a directory
conftest test manifests/ --policy policy/

# Test Helm template output
helm template my-release ./my-chart/ | conftest test - --policy policy/

# Test Terraform plan (JSON format)
terraform plan -out=plan.tfplan
terraform show -json plan.tfplan > plan.json
conftest test plan.json --policy policy/

# Test with specific output format
conftest test manifests/ --policy policy/ --output json

# Combine multiple policy directories
conftest test manifests/ --policy policy/ --policy shared-policies/

Terraform Validate, TFLint, and Checkov#

terraform validate#

What it catches: Syntax errors, invalid references, type mismatches, missing required arguments, invalid provider configurations. It requires terraform init to have been run (to download providers) but does not contact any remote APIs.

What it misses: Everything that requires API calls – whether an AMI exists, whether an instance type is valid in a region, whether IAM permissions are sufficient.

terraform init -backend=false    # Skip backend config, just download providers
terraform validate               # Check configuration validity

tflint#

What it catches: Everything terraform validate catches, plus provider-specific rules. For AWS: invalid instance types, deprecated AMI references, invalid security group rules. For GCP: invalid machine types, deprecated API usage. For Azure: invalid VM sizes.

What it misses: Cross-module validation (tflint checks each module independently), runtime API errors, IAM permission issues.

# Install
curl -fsSL https://github.com/terraform-linters/tflint/releases/latest/download/tflint_linux_amd64.zip \
  -o /tmp/tflint.zip && unzip /tmp/tflint.zip -d /usr/local/bin && rm /tmp/tflint.zip

# Initialize plugins, lint current directory, lint all modules
tflint --init
tflint
tflint --recursive

# Output as JSON for pipeline processing
tflint --format json

Minimal .tflint.hcl configuration:

plugin "aws" {
  enabled = true
  version = "0.30.0"
  source  = "github.com/terraform-linters/tflint-ruleset-aws"
}

rule "terraform_naming_convention" {
  enabled = true
}

rule "terraform_documented_variables" {
  enabled = true
}

rule "terraform_documented_outputs" {
  enabled = true
}

checkov#

What it catches: Security misconfigurations and compliance violations. Over 1000 built-in rules covering AWS, GCP, Azure, Kubernetes, and Docker. Checks for: unencrypted storage, public access, missing logging, overly permissive IAM, hardcoded secrets.

What it misses: Business logic, application-level security, runtime behavior.

# Install
pip install checkov

# Scan Terraform directory
checkov -d ./terraform/

# Scan Kubernetes manifests
checkov -d ./manifests/ --framework kubernetes

# Scan with specific checks, or skip specific checks
checkov -d . --check CKV_AWS_18,CKV_AWS_19
checkov -d . --skip-check CKV_AWS_18

# Scan Helm chart (renders templates first)
checkov -d ./my-chart/ --framework helm

Kustomize Build Validation#

kustomize build renders final manifests from base and overlay configurations, catching missing base files, invalid patches, and reference errors. It does not catch semantically incorrect patches – a strategic merge patch targeting a nonexistent field silently does nothing.

# Build and validate rendered output against K8s schemas
kustomize build overlays/production/ | kubeconform -strict -

# Build and test against policies
kustomize build overlays/production/ | conftest test - --policy policy/

# Validate all overlays
for overlay in overlays/*/; do
  kustomize build "$overlay" | kubeconform -strict -
done

YAML and JSON Schema Validation#

For configuration files that are not Kubernetes manifests or Terraform, validate YAML syntax and schemas directly.

# Validate YAML syntax (catches malformed YAML before any tool sees it)
yq eval '.' config.yaml > /dev/null

# Validate against a JSON schema
pip install check-jsonschema
check-jsonschema --schemafile schema.json config.yaml

# Validate GitHub Actions workflows against their schema
check-jsonschema --builtin-schema vendor.github-workflows .github/workflows/*.yml

# Validate docker-compose files
docker compose config -q

The Pre-Flight Validation Script#

This script chains all static validation tools into a single pipeline. Run it before any deployment to catch the cheapest-to-fix errors first.

#!/bin/bash
set -euo pipefail

# pre-flight-validate.sh
# Runs all static validation checks and reports results.
# Exit code 0 = all checks passed, non-zero = failures found.

ERRORS=0; WARNINGS=0

check() {
  local name="$1"; shift
  echo -n "  $name... "
  if output=$("$@" 2>&1); then echo "PASS"
  else echo "FAIL"; echo "$output" | sed 's/^/    /'; ERRORS=$((ERRORS + 1)); fi
}

warn_check() {
  local name="$1"; shift
  echo -n "  $name... "
  if output=$("$@" 2>&1); then echo "PASS"
  else echo "WARN"; echo "$output" | sed 's/^/    /'; WARNINGS=$((WARNINGS + 1)); fi
}

echo "=== Pre-Flight Validation ==="
echo ""

# [1/6] YAML Syntax
echo "[1/6] YAML Syntax"
command -v yq &>/dev/null && \
  for f in $(find . -name '*.yaml' -o -name '*.yml' | head -100); do
    check "YAML: $f" yq eval '.' "$f"; done

# [2/6] Helm Charts
echo "[2/6] Helm Charts"
for chart in $(find . -name Chart.yaml -exec dirname {} \;); do
  check "helm lint: $chart" helm lint "$chart" --strict
  check "helm template: $chart" helm template test-release "$chart"
  command -v kubeconform &>/dev/null && \
    check "kubeconform: $chart" bash -c "helm template test-release $chart | kubeconform -strict -summary -"
done

# [3/6] Kubernetes Manifests
echo "[3/6] Kubernetes Manifests"
[ -d "manifests/" ] && command -v kubeconform &>/dev/null && \
  check "kubeconform: manifests/" kubeconform -strict -summary manifests/
[ -d "manifests/" ] && command -v conftest &>/dev/null && [ -d "policy/" ] && \
  warn_check "conftest: manifests/" conftest test manifests/ --policy policy/

# [4/6] Kustomize Overlays
echo "[4/6] Kustomize"
for overlay in $(find . -name kustomization.yaml -exec dirname {} \;); do
  check "kustomize: $overlay" bash -c "kustomize build $overlay | kubeconform -strict -"
done

# [5/6] Terraform
echo "[5/6] Terraform"
for tfdir in $(find . -name '*.tf' -exec dirname {} \; | sort -u); do
  check "tf validate: $tfdir" bash -c "cd $tfdir && terraform init -backend=false -input=false >/dev/null 2>&1 && terraform validate"
  command -v tflint &>/dev/null && warn_check "tflint: $tfdir" bash -c "cd $tfdir && tflint"
  command -v checkov &>/dev/null && warn_check "checkov: $tfdir" checkov -d "$tfdir" --quiet --compact
done

# [6/6] Docker / Compose
echo "[6/6] Docker / Compose"
[ -f "docker-compose.yaml" ] || [ -f "docker-compose.yml" ] && \
  check "docker compose config" docker compose config -q
for df in $(find . -name Dockerfile | head -20); do
  command -v hadolint &>/dev/null && warn_check "hadolint: $df" hadolint "$df"
done

echo "=== Results: $ERRORS errors, $WARNINGS warnings ==="
[ "$ERRORS" -gt 0 ] && { echo "Pre-flight validation FAILED"; exit 1; }
echo "Pre-flight validation PASSED"

Make it executable and run it from the repository root:

chmod +x scripts/pre-flight-validate.sh
./scripts/pre-flight-validate.sh

What Static Validation Catches vs. What It Misses#

Category Static Validation Catches Requires Runtime to Catch
YAML Syntax errors, indentation, type mismatches Nothing – YAML errors are fully static
Kubernetes schemas Invalid fields, removed API versions, missing required fields Valid fields with wrong values (image does not exist, secret not found)
Helm Template rendering errors, chart structure, value type mismatches Helm hooks behavior, post-install job success, resource ordering
Policy Security misconfigurations, missing labels, missing resource limits Runtime policy enforcement (admission controllers, network policies in action)
Terraform Syntax, type errors, invalid references, security patterns API errors, quota limits, permission errors, state conflicts
Kustomize Patch application errors, missing bases, rendering Resource ordering, namespace creation, cross-reference validity

The 40% figure is a rough industry estimate. Static validation catches most structural and configuration errors but none of the runtime, networking, or stateful errors. For an agent workflow, static validation is the first gate – fast, free, and sufficient to catch the most common mistakes before spending any compute or cloud resources on deeper validation.