Validating Synthesized Terraform from CDKTF
This guide shows how to run terraform validate and terraform fmt -check against the HCL JSON that CDK for Terraform emits into cdktf.out, then turn those checks into a CI gate that catches provider-schema errors before any plan or apply. It is the validation half of CDKTF Testing and CI/CD, within the broader CDKTF Workflows & Terraform Synthesis practice.
Context
CDKTF unit tests prove your Python builds the resource graph you intended, but they cannot prove that graph is legal for the providers you pinned — a snapshot test will happily freeze an attribute the provider rejects. terraform validate against the synthesized output closes that gap by checking the emitted cdk.tf.json against the actual provider schemas. Running it after CDKTF architecture and synthesis produces the artifact, and before cdktf diff, is the cheapest place to catch a misconfigured resource.
Prerequisites
cdktf-cliinstalled, with providers pinned incdktf.jsonso synthesis is reproducible.- Terraform CLI on
PATH(terraform version≥ 1.5 recommended). - The generated bindings present (
cdktf get) so synthesis succeeds. - No backend credentials required — validation runs with
init -backend=false.
Confirm the toolchain:
# CLI: validation needs only the Terraform CLI and a synthesized output dir
cdktf --version && terraform version
Implementation
Step 1 — Synthesize, Then Initialize Providers Without a Backend
terraform validate needs the provider plugins installed but does not need state. Run init with -backend=false so the step stays read-only and needs no credentials.
# CLI: produce cdk.tf.json, then install providers without touching state
cdktf get
cdktf synth --output cdktf.out
terraform -chdir=cdktf.out/stacks/NetworkStack init -backend=false
State implication:
-backend=falsepreventsinitfrom contacting the remote backend, so this step can run on any runner without backend access or locking.
Step 2 — Run validate and fmt -check
validate checks the graph against provider schemas; fmt -check confirms the emitted JSON is canonically formatted (useful for catching a CDKTF version mismatch that changes output shape).
# CLI: schema validation plus a non-mutating format check
terraform -chdir=cdktf.out/stacks/NetworkStack validate
terraform fmt -check -recursive cdktf.out
Step 3 — Wrap It in a Typed Validator for CI
Driving the checks from Python lets you iterate every synthesized stack and surface a clean pass/fail, which is convenient for the GitHub Actions pipeline.
# CLI: python scripts/validate_synth.py
# Provider note: validate needs providers installed (init), but no cloud creds.
import subprocess
from dataclasses import dataclass
from pathlib import Path
@dataclass(frozen=True)
class StackValidation:
stack: str
ok: bool
detail: str
def validate_stack(stack_dir: Path) -> StackValidation:
name = stack_dir.name
init = subprocess.run(
["terraform", f"-chdir={stack_dir}", "init", "-backend=false", "-input=false"],
capture_output=True, text=True,
)
if init.returncode != 0:
return StackValidation(name, False, init.stderr.strip())
result = subprocess.run(
["terraform", f"-chdir={stack_dir}", "validate", "-no-color"],
capture_output=True, text=True,
)
return StackValidation(name, result.returncode == 0, (result.stdout + result.stderr).strip())
def main() -> int:
stacks = sorted(Path("cdktf.out/stacks").iterdir())
failures = [v for v in (validate_stack(s) for s in stacks) if not v.ok]
for f in failures:
print(f"FAIL {f.stack}: {f.detail}")
return 1 if failures else 0
if __name__ == "__main__":
raise SystemExit(main())
Verification
A clean run prints the success line per stack and exits zero:
# CLI: validate every synthesized stack; non-zero exit fails the CI job
cdktf synth && python scripts/validate_synth.py && echo "all stacks valid"
Terraform reports Success! The configuration is valid. for a well-formed graph; the wrapper's exit code is what your pipeline keys on.
Gotchas & Edge Cases
Error: Backend initialization required, please run "terraform init". You ranvalidateafter a plaininitthat tried to configure the backend and failed without credentials. Add-backend=falseto theinitused for validation.
fmt -checkfails on freshly synthesized output. CDKTF emits JSON, and a CLI version skew between developers and CI can change formatting. Pincdktf-cliand Terraform versions in CI so the canonical format is stable; treat a failure as a version-mismatch signal, not a code error.
validatepasses but the resource is still wrong.validateonly checks schema legality, not intent — it cannot tell that you attached the wrong subnet. Pair it with snapshot tests (see CDKTF Testing and CI/CD) and a reviewedcdktf diffplan gate.
Related
- CDKTF Testing and CI/CD — where this validation step sits in the full pipeline alongside unit and snapshot tests.
- Running CDKTF Pipelines in GitHub Actions — wiring this validation into a pull-request gate with OIDC.
- CDKTF Architecture & Synthesis — how the
cdk.tf.jsonyou validate is produced from Python constructs.
Frequently Asked Questions
Do I need cloud credentials to run terraform validate on CDKTF output?
No. With init -backend=false, Terraform installs the provider plugins to read their schemas but never contacts the backend or cloud APIs. Credentials are only needed later, at cdktf diff and cdktf deploy.
What is the difference between a CDKTF snapshot test and terraform validate?
A snapshot test asserts your Python emitted the exact JSON you expect — it checks intent. terraform validate checks that JSON against the provider schema — it checks legality. A snapshot can freeze an invalid attribute, and validate can pass a graph that wires the wrong resources, so you need both.
Why does terraform fmt -check matter for generated JSON?
A formatting difference usually means a CLI version drift between a developer's machine and CI, which can also shift synthesized output. Failing fmt -check early surfaces that drift before it produces a confusing plan diff.
Should validation run per stack or once for the whole output directory?
Per stack. Each stack under cdktf.out/stacks/ is its own Terraform working directory with its own providers, so init and validate must be run with -chdir pointed at each one — which the typed wrapper above iterates automatically.