Skip to content

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.


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.

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

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 provider configurations 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.

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.

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.


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.

The wrapper constructs CLI arguments and invokes them via subprocess. Two environment variables control Terraform’s behaviour:

VariableEffect
TF_IN_AUTOMATIONTells Terraform not to expect interactive input
TF_LOGSets the logging level for the operation

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:

FormatUsed byHow to consume
Standard JSONterraform show, terraform validate, terraform outputSingle JSON payload - capture stdout when the command finishes
JSONL (line-delimited JSON)terraform plan, terraform applyEach line is an independent JSON object - read and process incrementally

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.

Long-running commands (plan, apply) generate a continuous stream of events. Each line shares five standard fields:

FieldPurpose
@levelSeverity: info, warn, or error
@messageHuman-readable event description
@moduleIdentifies the engine (terraform.ui or tofu.ui)
@timestampISO 8601 time the event occurred
typeThe most important field - defines the event kind and determines which optional fields are present

Common type values:

TypeWhat it carries
versionEngine version
diagnosticErrors or warnings encountered during the run
change_summaryFinal counts of resources added, changed, removed, or imported
outputsEvaluated output variables

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:

  1. Generator yields a new event → wrapper inspects the type field
  2. If a handler is registered for that exact type → call it with the event data
  3. 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.

No single command returns the complete state structure:

CommandReturnsMissing
terraform state pullFull internal representationNot part of the documented machine-readable UI; format may change
terraform show -jsonWell-documented, structured JSONOmits serial and lineage; requires a physical .tfstate file

Workaround: run both commands and merge the results:

  1. terraform state pull > /tmp/state.tfstate - capture full state including metadata
  2. terraform show -json /tmp/state.tfstate - get structured, documented JSON
  3. Merge serial and lineage from step 1 into the JSON from step 2

State object hierarchy:

  • Outputs - name, value, and a sensitive boolean
  • Modules - recursive (a module can contain child modules)
  • Resources - inside modules; hold attributes but no further nesting

Parsing plans is the most complex wrapper task, for three reasons:

  1. Streaming - terraform plan -json produces JSONL, requiring incremental processing
  2. Multi-command - you must save the plan to a file, then run terraform show -json <plan.tfplan> to get the full structure
  3. 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 omitted
  • before_sensitive / after_sensitive - markers indicating which attributes should be redacted from output

Once your wrapper can parse state and plans, you can build:

Tool typeExample
Security scannerIterate planned changes; halt deployment if any aws_vpc_security_group_ingress_rule sets cidr_ipv4 = "0.0.0.0/0"
Cost estimatorLook up resource types being added/modified against a pricing API before anything is provisioned
Infrastructure replicatorRead live infrastructure → output valid .tf.json files that reproduce the environment
Dynamic provisionerAnalyse 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.

  • 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
ConceptHCL equivalentDescription
AppRoot moduleThe top-level entry point; contains one or more stacks
StackA self-contained root module instanceEach stack produces its own Terraform state and can be independently planned/applied
Resourceresource blockA provider resource instantiated from generated type bindings
// TypeScript example
import { 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.json
Terminal window
cdktf synth # Generates .tf.json files from your code
cdktf diff # Runs terraform plan against synthesised output
cdktf deploy # Runs terraform apply
cdktf destroy # Runs terraform destroy

Each command delegates to the standard Terraform CLI internally. The synthesised JSON files are fully inspectable - you can even run terraform plan directly against them.

  • 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

DimensionHCL (.tf)JSON (.tf.json)CDKTF
AudienceHumans writing infrastructureMachines generating infrastructureDevelopers who prefer general-purpose languages
Readability✅ Excellent❌ Verbose, hard to scan⚠️ Language-dependent
ExpressivenessBuilt-in: for_each, count, dynamic, functionsLimited - all expressions must use ${} interpolationFull language features: loops, classes, type systems
Comments#, //, /* */"//" key workaround onlyNative language comments
ToolingBroad: terraform fmt, linters, IDEsMinimal - standard JSON toolsIDE autocomplete, type checking, unit test frameworks
Mixing✅ Can coexist in same directory✅ Can coexist with HCLSynthesises to JSON; runs alongside HCL if needed
Learning curveLow (domain-specific)Low (universal format)Higher (CDKTF runtime + language + Terraform concepts)
Best forDay-to-day infrastructure managementMachine-generated configs, infrastructure replicationComplex platforms, dynamic generation, language-centric teams