Alternative Interfaces
HCL is designed for humans to read and write, but it is not the only way to interact with Terraform. When automation, programmatic analysis, or general-purpose language features are the priority, Terraform provides two additional interfaces: JSON configuration files and a machine-readable CLI that can be wrapped by any programming language.
JSON Instead of HCL
Section titled “JSON Instead of HCL”Terraform fully supports JSON as an alternative configuration language. Any file ending in .tf.json (or .tofu.json for OpenTofu) is processed identically to a .tf file. You can freely mix HCL and JSON files in the same project directory.
When to Use JSON
Section titled “When to Use JSON”JSON is most useful when machines are writing Terraform configurations:
- Converting visual architecture diagrams directly into deployable infrastructure
- Replicating a live environment into code (infrastructure scanning → JSON generation)
- Generating Terraform from higher-level domain-specific tools or languages
Mapping HCL Blocks to JSON
Section titled “Mapping HCL Blocks to JSON”The entire file is a single JSON object. Top-level keys correspond to HCL block types: resource, data, variable, output, locals, module, provider, and terraform.
{ "resource": { "aws_instance": { "web": { "ami": "ami-0abcdef1234567890", "instance_type": "t3.micro", "tags": { "Name": "web-server" } } } }, "output": { "instance_ip": { "value": "${aws_instance.web.public_ip}" } }}Key structural rules:
- All instances of a block type must be nested under their single top-level key - JSON does not allow duplicate keys at the same level.
- Repeated blocks (e.g., multiple
providerconfigurations with different aliases) must be expressed as a JSON array of objects under the provider key, not as repeated keys. - Meta-arguments that take unquoted references in HCL (
depends_on,provider) must be written as quoted JSON strings.
Expressions and Functions
Section titled “Expressions and Functions”JSON has no native concept of expressions. All dynamic values must be wrapped in Terraform’s string template syntax:
{ "locals": { "upper_name": "${upper(var.config)}" }}Conditionals and ternary statements work inside interpolation but are limited to producing string outputs. Complex logic is generally easier in HCL.
Comments in JSON
Section titled “Comments in JSON”Standard JSON has no comment syntax. Terraform supports a workaround - use "//" as a key:
{ "//": "WARNING: This file is auto-generated. Do not edit manually.", "resource": { "aws_s3_bucket": { "logs": { "bucket": "my-app-logs" } } }}Terraform recognises the "//" key and treats it as a comment rather than a configuration argument.
Wrapping Terraform Programmatically
Section titled “Wrapping Terraform Programmatically”Because Terraform does not provide a traditional SDK, the only programmatic interface is the CLI itself. A wrapper is any application that executes CLI commands, parses the resulting output, and exposes the data as native programming objects (Python data classes, Go structs, etc.).
Many tools you already use are wrappers: Terragrunt, Checkov, Trivy, HCP Terraform, Spacelift, and Atlantis are all built on top of the Terraform CLI.
Core Mechanisms
Section titled “Core Mechanisms”1 · Command Execution
Section titled “1 · Command Execution”The wrapper constructs CLI arguments and invokes them via subprocess. Two environment variables control Terraform’s behaviour:
| Variable | Effect |
|---|---|
TF_IN_AUTOMATION | Tells Terraform not to expect interactive input |
TF_LOG | Sets the logging level for the operation |
2 · Machine-Readable UI (-json flag)
Section titled “2 · Machine-Readable UI (-json flag)”Appending -json to most commands forces Terraform to output structured data instead of human-readable text. The output arrives in one of two formats depending on the command:
| Format | Used by | How to consume |
|---|---|---|
| Standard JSON | terraform show, terraform validate, terraform output | Single JSON payload - capture stdout when the command finishes |
| JSONL (line-delimited JSON) | terraform plan, terraform apply | Each line is an independent JSON object - read and process incrementally |
Standard JSON Commands
Section titled “Standard JSON Commands”validate -json returns a single object containing a valid boolean, error/warning counts, and a diagnostics array with detailed misconfiguration explanations.
show -json returns a structured representation of a state file or saved plan - essential for security scanning, cost estimation, and compliance validation.
JSONL Streaming Commands (Plan & Apply)
Section titled “JSONL Streaming Commands (Plan & Apply)”Long-running commands (plan, apply) generate a continuous stream of events. Each line shares five standard fields:
| Field | Purpose |
|---|---|
@level | Severity: info, warn, or error |
@message | Human-readable event description |
@module | Identifies the engine (terraform.ui or tofu.ui) |
@timestamp | ISO 8601 time the event occurred |
type | The most important field - defines the event kind and determines which optional fields are present |
Common type values:
| Type | What it carries |
|---|---|
version | Engine version |
diagnostic | Errors or warnings encountered during the run |
change_summary | Final counts of resources added, changed, removed, or imported |
outputs | Evaluated output variables |
Streaming Event Hooks
Section titled “Streaming Event Hooks”A wrapper reads the JSONL stream incrementally - typically via a generator that reads stdout character-by-character, yielding a parsed JSON object every time it encounters a newline. This lets the wrapper process events while the command is still running.
Developers attach event handlers (hooks) when calling long-running methods:
def on_change_summary(event): slack.post(f"Deploy complete: {event['changes']}")
wrapper.apply( event_handlers={ "change_summary": on_change_summary, "all": lambda e: logger.info(e), # catch-all })Hook dispatch logic:
- Generator yields a new event → wrapper inspects the
typefield - If a handler is registered for that exact type → call it with the event data
- If a catch-all (
"all") handler exists → also call it for every event
Common uses: streaming live progress to a deployment console, forwarding events to an external logging server in real time.
Parsing State Programmatically
Section titled “Parsing State Programmatically”No single command returns the complete state structure:
| Command | Returns | Missing |
|---|---|---|
terraform state pull | Full internal representation | Not part of the documented machine-readable UI; format may change |
terraform show -json | Well-documented, structured JSON | Omits serial and lineage; requires a physical .tfstate file |
Workaround: run both commands and merge the results:
terraform state pull > /tmp/state.tfstate- capture full state including metadataterraform show -json /tmp/state.tfstate- get structured, documented JSON- Merge
serialandlineagefrom step 1 into the JSON from step 2
State object hierarchy:
- Outputs - name, value, and a
sensitiveboolean - Modules - recursive (a module can contain child modules)
- Resources - inside modules; hold attributes but no further nesting
Parsing Plans Programmatically
Section titled “Parsing Plans Programmatically”Parsing plans is the most complex wrapper task, for three reasons:
- Streaming -
terraform plan -jsonproduces JSONL, requiring incremental processing - Multi-command - you must save the plan to a file, then run
terraform show -json <plan.tfplan>to get the full structure - Large structures - the plan object contains metadata, prior state, the planned root module, and lists of resource changes
Analysing resource changes:
Each change sits inside an undocumented ChangeContainer object that carries the resource address and embeds the actual change data:
actions- a list (a replacement produces both"delete"and"create")before/after- simple key-value maps of attribute values; unchanged attributes are omittedbefore_sensitive/after_sensitive- markers indicating which attributes should be redacted from output
Building Custom Tools
Section titled “Building Custom Tools”Once your wrapper can parse state and plans, you can build:
| Tool type | Example |
|---|---|
| Security scanner | Iterate planned changes; halt deployment if any aws_vpc_security_group_ingress_rule sets cidr_ipv4 = "0.0.0.0/0" |
| Cost estimator | Look up resource types being added/modified against a pricing API before anything is provisioned |
| Infrastructure replicator | Read live infrastructure → output valid .tf.json files that reproduce the environment |
| Dynamic provisioner | Analyse application source code → auto-generate the exact infrastructure configuration needed to host it |
Cloud Development Kit for Terraform (CDKTF)
Section titled “Cloud Development Kit for Terraform (CDKTF)”CDKTF lets you define infrastructure using a general-purpose programming language (TypeScript, Python, Go, Java, C#) instead of HCL. Under the hood, CDKTF synthesises your code into standard Terraform JSON (.tf.json) and then runs the normal Terraform CLI to plan and apply.
Why Use CDKTF
Section titled “Why Use CDKTF”- Full access to language features: loops, conditionals, type systems, unit testing frameworks
- IDE autocomplete and type checking for every resource attribute
- Share logic across teams via standard package managers (npm, PyPI, Maven)
- Build higher-level abstractions (your own “L2 constructs”) on top of raw provider resources
Core Concepts
Section titled “Core Concepts”| Concept | HCL equivalent | Description |
|---|---|---|
| App | Root module | The top-level entry point; contains one or more stacks |
| Stack | A self-contained root module instance | Each stack produces its own Terraform state and can be independently planned/applied |
| Resource | resource block | A provider resource instantiated from generated type bindings |
// TypeScript exampleimport { App, TerraformStack } from "cdktf";import { AwsProvider } from "@cdktf/provider-aws/lib/provider";import { Instance } from "@cdktf/provider-aws/lib/instance";
class MyStack extends TerraformStack { constructor(scope: Construct, name: string) { super(scope, name);
new AwsProvider(this, "aws", { region: "us-east-1" });
new Instance(this, "web", { ami: "ami-0abcdef1234567890", instanceType: "t3.micro", tags: { Name: "web-server" }, }); }}
const app = new App();new MyStack(app, "production");app.synth(); // → outputs cdktf.out/stacks/production/*.tf.jsonWorkflow
Section titled “Workflow”cdktf synth # Generates .tf.json files from your codecdktf diff # Runs terraform plan against synthesised outputcdktf deploy # Runs terraform applycdktf destroy # Runs terraform destroyEach command delegates to the standard Terraform CLI internally. The synthesised JSON files are fully inspectable - you can even run terraform plan directly against them.
When CDKTF Makes Sense
Section titled “When CDKTF Makes Sense”- Teams with deep general-purpose language expertise but limited HCL experience
- Complex internal platforms where infrastructure is generated dynamically from application metadata
- Projects that benefit from shared libraries, inheritance, and composition patterns unavailable in HCL
Comparison: HCL vs. JSON vs. CDKTF
Section titled “Comparison: HCL vs. JSON vs. CDKTF”| Dimension | HCL (.tf) | JSON (.tf.json) | CDKTF |
|---|---|---|---|
| Audience | Humans writing infrastructure | Machines generating infrastructure | Developers who prefer general-purpose languages |
| Readability | ✅ Excellent | ❌ Verbose, hard to scan | ⚠️ Language-dependent |
| Expressiveness | Built-in: for_each, count, dynamic, functions | Limited - all expressions must use ${} interpolation | Full language features: loops, classes, type systems |
| Comments | #, //, /* */ | "//" key workaround only | Native language comments |
| Tooling | Broad: terraform fmt, linters, IDEs | Minimal - standard JSON tools | IDE autocomplete, type checking, unit test frameworks |
| Mixing | ✅ Can coexist in same directory | ✅ Can coexist with HCL | Synthesises to JSON; runs alongside HCL if needed |
| Learning curve | Low (domain-specific) | Low (universal format) | Higher (CDKTF runtime + language + Terraform concepts) |
| Best for | Day-to-day infrastructure management | Machine-generated configs, infrastructure replication | Complex platforms, dynamic generation, language-centric teams |