Getting Started
Show the whole prompt
Read https://intentius.io/chant/lexicons/terraform/getting-started/.
Install `@intentius/chant-lexicon-terraform`, and name this repository's Terraform root modules under `terraform.roots` in `chant.config.ts`, with `binary` set to the tool the repository uses.
Run `npx chant build` and `npx chant graph --format ir`, and report what the post-synth checks find in each root.
Do not run `terraform apply` or `tofu apply`, and do not declare a `TerraformApplyOp`. Open a pull request with the config.
Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.Optional: hand this page to your coding agent
Read https://intentius.io/chant/lexicons/terraform/getting-started/.Install `@intentius/chant-lexicon-terraform`, and name this repository's Terraform root modules under `terraform.roots` in `chant.config.ts`, with `binary` set to the tool the repository uses.Run `npx chant build` and `npx chant graph --format ir`, and report what the post-synth checks find in each root.Do not run `terraform apply` or `tofu apply`, and do not declare a `TerraformApplyOp`. Open a pull request with the config.Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.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.
Install
Section titled “Install”npm install --save-dev @intentius/chant-lexicon-terraformName your roots
Section titled “Name your roots”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.
chant buildEvery block of every configured root becomes one entity, keyed
<root>/<address>:
| HCL | Entity type | Key |
|---|---|---|
terraform { ... } | Terraform::Terraform | app/terraform |
provider "null" {} | Terraform::Provider | app/provider.null |
resource "null_resource" "web" {} | Terraform::Resource | app/null_resource.web |
data "aws_ami" "base" {} | Terraform::Data | app/data.aws_ami.base |
module "cdn" {} | Terraform::Module | app/module.cdn |
variable "region" {} | Terraform::Variable | app/var.region |
output "url" {} | Terraform::Output | app/output.url |
locals { ... } | Terraform::Locals | app/locals |
Each entity carries props.address, props.body (the block, verbatim from the
parser), props.file and props.root.
The build writes no Terraform of its own. terraform apply keeps working on
your .tf files as before.
Graph the estate
Section titled “Graph the estate”chant graph --format irEvery parsed block is a node, and every reference between two blocks is an
edge. Five reference forms become edges: resource, data, module, var
and local. depends_on is one too, with no attribute on it.
Each edge names the attribute it flows through on both sides, the way
chant graph labels any other reference. A
subnet_id = aws_subnet.private.id gives viaAttr: "subnet_id" and
toAttr: "id"; an edge whose reference named two producer attributes carries
no toAttr rather than an arbitrary one.
A count or for_each block stays one node. chant’s entity is the block, not
the instance, so count = 3 draws one card and one edge per target, and the
block records which meta-argument expanded it on props.expansion. The
instance count is known only at plan time, and this reads the declaration.
References resolve inside their own module scope, which is Terraform’s own
namespace: a var.region inside module.cdn is the child module’s variable
and never the root’s. Nothing resolves across roots.
Separate roots read each other by name through a data source, not by
reference.
groups.byStack is keyed by root name, one entry per root you named in
terraform.roots. That is the grouping a boundary-box renderer draws, and a
root is what one terraform apply runs against. groups.byLexicon keeps
saying terraform.
A provider = aws.replica meta-argument is not an edge. It names a provider
block by type and alias together, and the entity key for one is
provider.<type> with the alias inside the body, so two aliased providers of
the same type cannot be told apart from the reference.
What the checks tell you
Section titled “What the checks tell you”TF001 fires once per root whose terraform block declares neither a
backend "<type>" nor a cloud {}, and once per root whose only backend is
backend "local". 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 remote backend to clear it:
terraform { backend "s3" { bucket = "acme-tfstate" key = "app/terraform.tfstate" region = "us-east-1" }}Failure modes
Section titled “Failure modes”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”The build only reads the root’s blocks. To run init, plan and apply on 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.
A root as a workspace member
Section titled “A root as a workspace member”A root can also be a member of a chant workspace
without a chant project of its own. This lexicon supplies the terraform member
kind as data, in workspace-kinds.json at its ./workspace-kinds subpath. Pin
the lexicon in chant.workspace.json and declare the root with that kind.
{ "name": "estate", "schema": 1, "pins": [{ "package": "@intentius/chant-lexicon-terraform", "version": "0.80.0" }], "members": [{ "name": "network", "dir": "network", "kind": "terraform" }]}The kind’s probe passes when a .tf file sits directly in the member’s
directory, and its precedence is 400. A chant project (500) that keeps .tf
files beside its config is still a chant member. A choudoufu root passes this
probe too, and the choudoufu kind this lexicon also ships (450, an
estate.chdf.hcl sidecar or a live block) outranks it. chant workspace ls reads the kind from the file and never
imports the lexicon. behold’s own terraform member kind moved here, so an estate
behold reads lists the same way in a chant workspace. See
Workspace Kinds for the
format.
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.