Terraform configuration
The Terraform configuration in the checkout: every root directory holding .tf files, the backend it stores state in, the providers and modules it takes on and how tightly they are pinned, the resources it declares, the variables it takes, the lock file that resolves it, and any state file left in the tree. Read out of the files themselves, so it works on a runner with no Terraform installed, and enriched with the versions an initialised root has already resolved when the terraform CLI is there.
| Facts key | terraform |
| Version | v1 |
| Script | terraform.py |
| Timeout | 90 seconds |
Tools
This collector reads what these tools report. It never installs one: a tool that is not on the runner is missing evidence, and the guardrails asking for these facts skip rather than fail.
| Tool | Needed | Description |
|---|---|---|
terraform | optional | Terraform CLI, run only to read back the provider versions an already initialised root resolved. Never init, plan or apply, so nothing it does reaches real infrastructure. |
Installing terraform
Install with brew install terraform, or add the hashicorp/setup-terraform step to the workflow. The configuration is read out of the files either way, so this only adds the resolved versions.
Inputs
| Input | Description | Default | Environment |
|---|---|---|---|
directory | Directory the configuration is looked for under, relative to the directory the CLI runs in. Discovery walks the whole working directory by default and this only narrows it, so a repository holding its Terraform in one place collects that place alone. | . | GUARDRAIL_INPUT_DIRECTORY |
A guardrail that declares an input of the same name passes it through, so the guardrail's configuration in buildnote.json is what decides these values.
A guardrail asks for these facts by name, and reads them back the same way:
{
"collect": ["terraform"]
}terraform = guardrail.facts("terraform")Facts
The document terraform collects. A path carrying [] is an entry of the list before it, and one carrying [path] is a key of the object before it.
| Fact | Meaning |
|---|---|
directory | The directory input, as the guardrail configured it. |
source | report when the configuration was read out of the checkout, tool when terraform was also run against an initialised root, none when there is no Terraform here at all. |
reason | Why nothing was found, present only when source is none. |
reports | Every configuration file that was read and understood, in path order. A file the reader could not handle is in unparsed instead. |
reports[].path | Path of the file, relative to the directory the CLI runs in. |
reports[].modified | When the file was last written, ISO 8601. |
reports[].format | What it was read as: tf, tfvars or lock. |
tool.name | terraform, present only when it was run. |
tool.version | Version of the terraform on the runner. |
tool.path | Where that terraform was found on PATH. |
tool.args | Arguments of the last invocation. providers -json is tried first and version -json is the fallback, because only the second is supported by every release. |
tool.exitCode | What that invocation exited with. |
tool.durationMs | How long it took. |
roots | Every directory holding at least one .tf file, in path order. A reusable module directory is one of these too, so read backend.type on a root that is not a live one with that in mind. |
roots[].path | Directory of the root, relative to the directory the CLI runs in, . for the working directory itself. |
roots[].backend.type | Backend the root stores state in, as the backend label names it. local when the root declares no backend block at all, which is the finding rather than a default worth ignoring. |
roots[].backend.attributes | Every attribute the backend block declares, by name. A value is carried only when it is a plain scalar the reader could resolve; anything interpolated, structured or named like a credential is null, so the key is evidence the attribute is set without the secret becoming evidence too. |
roots[].backend.encrypted | Whether the backend declares its state encrypted, from encrypt or a key attribute for the backends that have one. null when the backend says nothing either way, which is not the same as false. |
roots[].backend.locking | Whether the backend declares state locking, from use_lockfile, dynamodb_table, lock or lock_address depending on the backend. null when the backend says nothing either way. |
roots[].providers | Every provider the root requires, from required_providers first and then any provider block naming one that was not required. |
roots[].providers[].name | Local name of the provider, as the configuration refers to it. |
roots[].providers[].source | Source address as written, such as hashicorp/aws, or null when only a provider block names it. |
roots[].providers[].version | Version constraint exactly as written, such as ~> 6.0, or null when none is declared. |
roots[].providers[].pinned | Whether that constraint pins one exact version. A ~>, a range, or no constraint at all is not pinned. |
roots[].modules | Every module block the root declares, in the order the files declare them. |
roots[].modules[].name | Label of the module block. |
roots[].modules[].source | Its source exactly as written, or null when the source is interpolated rather than literal. |
roots[].modules[].version | Its version constraint, which only a registry source can carry, or null. |
roots[].modules[].local | Whether the source is a relative path inside the repository, so nothing is fetched. |
roots[].modules[].registry | Whether the source is a module registry address, meaning version is what pins it. |
roots[].modules[].git | Whether the source is a git or HTTP URL, meaning the ref in the URL is what pins it and version never applies. |
roots[].resources | How many resource blocks the root declares, by resource type. data blocks are not counted. |
roots[].resourceTypes | Those resource types, sorted. |
roots[].variables | Every variable block the root declares. Names only: a default can be a credential and these facts are attached to the build as evidence. |
roots[].variables[].name | Label of the variable block. |
roots[].variables[].sensitive | Whether it declares sensitive = true. |
roots[].variables[].hasDefault | Whether it declares a default at all. The default itself is never collected. |
roots[].varFiles | Every .tfvars file committed beside the root. |
roots[].varFiles[].path | Path of the file, relative to the directory the CLI runs in. |
roots[].varFiles[].variables | The variable names it assigns, sorted. The values are never collected. |
roots[].lockfile.present | Whether the root commits a .terraform.lock.hcl. |
roots[].lockfile.path | Path of that lock file, or null when there is none. |
roots[].lockfile.providers | Every provider the lock file resolves. |
roots[].lockfile.providers[].name | Last segment of the provider address, matching the local name in the usual case. |
roots[].lockfile.providers[].source | Full address the lock file names, such as registry.terraform.io/hashicorp/aws, so a mirror is recognisable. |
roots[].lockfile.providers[].version | Version it resolves to. |
roots[].lockfile.providers[].hashes | How many hashes it records. The hashes themselves are a count rather than a list, because the count is what says whether the lock covers more than one platform. |
roots[].unparsed | How many files in this root the reader could not handle. Anything above zero means this root is described from part of its configuration, so treat an absence here as unknown rather than as a finding. |
stateFiles | Every .tfstate or .tfstate.backup file left in the tree, which is a finding in itself. The walk skips .terraform, so a working directory Terraform manages is not one of these. |
stateFiles[].path | Path of the state file, relative to the directory the CLI runs in. |
stateFiles[].bytes | Its size in bytes. The contents are never read. |
unparsed | Every file the reader could not handle, so a guardrail can tell a configuration that says nothing from one nobody could read. |
unparsed[].path | Path of that file. |
unparsed[].reason | What stopped the reader. |
resolved | Provider versions an initialised root has already resolved, read back with terraform. Present only when the tool is on the runner and the root carries a .terraform/providers directory, which is what terraform init leaves behind and what tells an initialised root from a stale one. |
resolved[].root | Root the versions were read in. |
resolved[].name | Last segment of the provider address. |
resolved[].source | Full provider address as Terraform names it. |
resolved[].version | Version Terraform selected. |
A collector that cannot finish prints what it had along with an incomplete key saying why, so the facts below the point it stopped are absent. A check that cares reads incomplete before the rest.
Example facts
The example facts collected by terraform collector:
{
"directory": ".",
"source": "report",
"reports": [
{
"path": ".terraform.lock.hcl",
"modified": "2026-03-04T10:15:00Z",
"format": "lock"
},
{
"path": "main.tf",
"modified": "2026-03-04T10:15:00Z",
"format": "tf"
}
],
"roots": [
{
"path": ".",
"backend": {
"type": "s3",
"attributes": {
"bucket": "widget-terraform-state",
"key": "live/widget.tfstate",
"region": "eu-west-1",
"encrypt": true,
"use_lockfile": true
},
"encrypted": true,
"locking": true
},
"providers": [
{
"name": "aws",
"source": "hashicorp/aws",
"version": "6.4.0",
"pinned": true
},
{
"name": "random",
"source": "hashicorp/random",
"version": "~> 3.6",
"pinned": false
}
],
"modules": [
{
"name": "events",
"source": "terraform-aws-modules/sqs/aws",
"version": "4.2.1",
"local": false,
"registry": true,
"git": false
}
],
"resources": {
"aws_s3_bucket": 1,
"aws_s3_bucket_versioning": 1
},
"resourceTypes": [
"aws_s3_bucket",
"aws_s3_bucket_versioning"
],
"variables": [
{
"name": "region",
"sensitive": false,
"hasDefault": true
},
{
"name": "api_token",
"sensitive": true,
"hasDefault": false
}
],
"varFiles": [],
"lockfile": {
"present": true,
"path": ".terraform.lock.hcl",
"providers": [
{
"name": "aws",
"source": "registry.terraform.io/hashicorp/aws",
"version": "6.4.0",
"hashes": 3
},
{
"name": "random",
"source": "registry.terraform.io/hashicorp/random",
"version": "3.6.3",
"hashes": 2
}
]
},
"unparsed": 0
}
],
"stateFiles": [],
"unparsed": []
}