Skip to content

Getting Started

Most chant lexicons ask you to declare infrastructure in TypeScript. This one starts from the .tf files you already have. You name the root modules, chant parses them, and the blocks join the build as entities that the post-synth checks can read. Nothing is written back over the HCL.

Terminal window
npm install --save-dev @intentius/chant-lexicon-terraform

Add the terraform namespace to chant.config.ts. Importing the package is what brings the key into ChantConfig.

import type { ChantConfig } from "@intentius/chant/config";
import "@intentius/chant-lexicon-terraform";
export default {
lexicons: ["terraform"],
terraform: {
binary: "terraform",
roots: {
app: { dir: "./terraform" },
},
},
} satisfies ChantConfig;

dir resolves against the project root, the directory holding chant.config.ts, because a .tf tree usually sits beside the typed source rather than inside it. Each root also takes an optional workspace, a varFiles list and a backendConfig map.

binary is "terraform" or "tofu". The two are wire-compatible for everything this lexicon does, so the choice is recorded rather than guessed.

Terminal window
chant build

Every block of every configured root becomes one entity, keyed <root>/<address>:

HCLEntity typeKey
terraform { ... }Terraform::Terraformapp/terraform
provider "null" {}Terraform::Providerapp/provider.null
resource "null_resource" "web" {}Terraform::Resourceapp/null_resource.web
data "aws_ami" "base" {}Terraform::Dataapp/data.aws_ami.base
module "cdn" {}Terraform::Moduleapp/module.cdn
variable "region" {}Terraform::Variableapp/var.region
output "url" {}Terraform::Outputapp/output.url
locals { ... }Terraform::Localsapp/locals

Each entity carries props.address, props.body (the block, verbatim from the parser), props.file and props.root.

The build emits no Terraform of its own. Your .tf files stay the only source of truth, and terraform apply keeps working exactly as before.

TF001 fires once per root whose terraform block declares neither a backend "<type>" nor a cloud {}. Such a root keeps its state in a local terraform.tfstate: unshared, unlocked, and holding every resource attribute in plaintext. The first apply from a second machine starts from an empty state and proposes to create the estate again.

Add a backend to clear it:

terraform {
backend "s3" {
bucket = "acme-tfstate"
key = "app/terraform.tfstate"
region = "us-east-1"
}
}

A root whose dir does not exist, or one holding a .tf the parser refuses, produces a warning naming the root and contributes no entities. The rest of the build carries on, and the other roots are unaffected. Reading someone else’s estate is the job here, so half of it parsing is more useful than none of it.

Driving the root: TerraformApplyOp on the local executor

Section titled “Driving the root: TerraformApplyOp on the local executor”

Reading the root’s blocks doesn’t run anything. To actually init/plan/apply it, declare a TerraformApplyOp and run it with chant run:

import { TerraformApplyOp } from "@intentius/chant-lexicon-terraform";
export const { op } = TerraformApplyOp({
name: "app-apply",
root: "app",
gate: "never", // no approval step, so a run goes straight through, see below
});

gate: "never" matters here specifically: every other gate mode emits a Gate phase, and a run that reaches a gate nobody has approved records a pending fact on the gate ledger, ends with status gated and exits 3, so Apply never runs until someone records the resolution with chant approve. With it, chant run app-apply executes Init, Plan and Apply straight through in your terminal. See Ops for every TerraformApplyOp option, the four activities and builders underneath it, and the compensation rule for a failed apply.

The worked example lives in examples/getting-started/: a config, a terraform/ root with a local backend, and the entities it produces. examples/apply-gated/ pairs a gated TerraformApplyOp with an explicit rollback command.