rules_terraform
Bazel rules for Terraform and
OpenTofu. Every terraform_* rule has a tofu_*
twin that takes the same inputs and runs the other engine; the text below
says terraform and means both.
Setup
bazel_dep(name = "rules_terraform", version = "{version}")
Toolchains for both engines are registered by rules_terraform's own
MODULE.bazel. A build uses one release per engine: the root module's
toolchain tag, else the newest shipped in
//terraform/private:versions.bzl /
//tofu/private:versions.bzl, which
carry the three newest releases with their checksums.
terraform = use_extension("@rules_terraform//terraform:extensions.bzl", "terraform")
terraform.toolchain(version = "1.9.8")
tofu = use_extension("@rules_terraform//tofu:extensions.bzl", "tofu")
tofu.toolchain(version = "1.9.1")
Any release works there. One outside the shipped table costs a single
fetch of its SHA256SUMS, recorded as a fact in MODULE.bazel.lock.
Changing the release is editing the tag; a tag in a dependency is ignored.
Nothing has to be installed on the host: the engine, provider archives
and modules are all fetched by Bazel, and every bazel run and
bazel test below uses the toolchain engine.
Getting started
A root module new to Bazel is a directory of .tf files. Three things get
added around it: BUILD targets, a provider lock, and a deps tag in
MODULE.bazel that fetches what the lock and the module blocks name.
Gazelle writes the BUILD targets.
-
Set up Gazelle with the rules' language and run it:
# BUILD.bazel at the repository root load("@gazelle//:def.bzl", "gazelle", "gazelle_binary") gazelle_binary( name = "gazelle_bin", languages = ["@rules_terraform//terraform/gazelle"], ) gazelle( name = "gazelle", gazelle = ":gazelle_bin", )bazel run //:gazellewrites aterraform_moduleper directory of configuration,depsbetween them following the localmoduleblocks, and for a root (a directory with abackendorcloudblock, or a# gazelle:tf_kind rootdirective) aterraform_binarynamedterraformand aterraform_providers_locknamedproviders_lock.update. -
Generate the lock and commit it. This runs the toolchain engine's
providers lockfor every platform in the default set and writes.terraform.lock.hclnext to the sources; it needs the network.bazel run //path/to/app:providers_lock.updateRerun it whenever a
required_providersblock changes. -
Declare the
depstag inMODULE.bazel. Gazelle names the hub a root depends on@tf_<path>with/and-replaced by_, or whatever a# gazelle:tf_deps_hubdirective says; the tag must use that name.terraform = use_extension("@rules_terraform//terraform:extensions.bzl", "terraform") terraform.deps( name = "tf_path_to_app", lock = "//path/to/app:.terraform.lock.hcl", roots = ["//path/to/app:main.tf"], # only if its `module` blocks name registry or git modules ) use_repo(terraform, "tf_path_to_app") -
Run Gazelle again. With the lock in place it adds the
terraform_providers_lock_test, which fails when the lock drifts from the sources. Then:bazel test //path/to/app/... # validate, fmt, lock drift bazel run //path/to/app:terraform -- plan # the engine, offline for providers and modules
Without Gazelle the same targets are written by hand; the
monorepo example
shows them for three environments sharing one lock. Add .terraform/ to
.gitignore: the rules read nothing from it, and
init writes it.
Running the engine
terraform_binary
bazel run //path/to:terraform -- plan
bazel run //path/to:terraform -- apply
The binary copies the module directory the build assembled, .tf files,
local child modules at their source paths, the lock and a populated
.terraform/, into a temp dir and runs the engine there with the
arguments given. The temp dir is removed when the command finishes.
Relative file arguments (-out=tfplan, -var-file=x.tfvars) resolve
against it, so pass absolute paths.
Because the build already installed the providers and the module
manifest, the only thing a run-time init still contributes is backend
setup, so the binary runs one only before subcommands that need a backend.
version, fmt, validate, test and metadata skip it and work
offline against a module with a remote backend.
State. A module with a backend or cloud block is unaffected: its
state belongs to the backend. A module with neither keeps state where the
native CLI keeps it, the working directory, which here is the temp dir,
so by default state goes with the run: disposable_state on the
terraform_module is True. A run that did write state is kept and its
path printed, so the record of an apply never vanishes silently, but
the next run starts fresh. Where state must outlive the run with no
backend (bootstrapping the bucket that will hold real state, a sandbox
kept for days), set disposable_state = False: the local backend is
pointed at a per-module directory under bazel-out/rules_terraform/,
which bazel clean deletes and which is named after every state-writing
command. Setting it on a module that declares a backend is an error.
Environment. bazel run keeps the operator's environment, which is
where credential helpers and TF_WORKSPACE live. The tests and the
validate aspect run the engine with the CLI configuration disabled
(TF_CLI_CONFIG_FILE pointed at an empty file, CHECKPOINT_DISABLE=1,
TF_IN_AUTOMATION=1), so a ~/.terraformrc on one machine cannot change
what a check reports.
Initializing in place
bazel run //path/to:terraform -- init is the one subcommand that does
not run in the temp dir, where its work would be deleted with it. It
installs what the build constructed into the module's source directory,
so the native CLI works there afterwards.
bazel run //envs/prod/app:terraform -- init
cd envs/prod/app
terraform plan # native CLI, providers and modules from the build
The directory is the root module's package under the checkout, wherever
the command was run from; BUILD_WORKSPACE_DIRECTORY and the target
decide it, not the shell's cwd (that is the @terraform hub's
job). The runner:
- copies the built
.terraform/providers/and.terraform/modules/over whatever is there, keeping.terraform/environmentand the saved backend configuration, as a native re-initkeeps them; - writes
.terraform.lock.hclnext to the sources only when the module has none of its own (it depends on a shared hub), and says so; - adds the host platform's
h1:hash to the lock for any installed provider the lock lacks it for, one line each, and says so. An unpacked package can only be verified by itsh1:, so a lock made on another platform, or natively with fewer-platforms than the updater records, would make the engine refuse the package it is standing next to. It is the lineproviders lock -platform=<host>would add; commit it, or regenerate the lock for every platform with the updater; - runs the engine's
init -get=false -plugin-dir=.terraform/providersin that directory with the remaining arguments appended, which is where the backend gets configured.
Each init flag is either the backend's business, which passes through,
or the build's, which is refused with the Bazel path named:
| Flag | Under the binary | Why |
|---|---|---|
-backend-config=…, -backend=false | passes through | Backend configuration is the module's, not the build's. |
-reconfigure | passes through | Re-reads the backend block, ignoring the saved configuration. The staged runs are implicitly always reconfigured, since a fresh copy has none saved; in place it matters. |
-migrate-state, -force-copy | pass through | Moves state from the previously saved backend configuration to the current one. It needs that saved configuration, which only a persisted directory has, so this is the only place under Bazel it can work. |
-input, -lock, -lock-timeout, -no-color, -json | pass through | Plain engine options. |
-lockfile=readonly | passes through | Forbids every lock write, step 3 included: a lock without the host's hash is then an error naming the updater rather than an edit. |
-upgrade | refused | Would re-resolve providers against the plugin dir and rewrite the lock. The versions are facts of the build: bazel run //path/to:providers_lock.update for providers, RULES_TERRAFORM_UPGRADE=1 bazel fetch for modules (see Upgrading). |
-get | refused | The build installs the modules; the manifest is theirs. |
-plugin-dir | refused | The build's providers are the plugin directory. |
-from-module | refused | A root module is a terraform_module target. |
-chdir (global) | refused | The target names the module directory. |
-help | passes through | Answered by the engine, with nothing installed. |
.terraform/ is the engine's cache and belongs in .gitignore; Gazelle
and the rules ignore it, and a Bazel run never reads it. This is the one
bazel run that writes into the source tree, and only because it was
asked to. The next bazel run //path/to:terraform -- plan still starts
from a fresh copy.
Workspaces
The rules add no workspace concept of their own; they use Terraform's.
TF_WORKSPACE in the environment selects one for every project in a run:
--action_env=TF_WORKSPACE=prod for the validate aspect,
--test_env=TF_WORKSPACE=prod for the test rules, the shell for
bazel run. A root whose configuration is keyed on terraform.workspace
gets each of its workspaces checked by one test target per workspace,
with the workspace attribute of terraform_validate_test,
terraform_test or terraform_plan_test forcing it, and no_validate
on the module where the default workspace is not a valid one. A
workspace select inside a bazel run changes only the temp copy, so
it does not carry to the next run and the runner says so; workspace new
still creates the workspace in the backend.
Tests and review artefacts
terraform_validate_test and
terraform_fmt_test are validate
and fmt -check as Bazel tests. The same checks exist as aspects, which
the examples' .bazelrc attaches so bazel build //... runs them over
every module without a target per module; no_validate and no_format
tags opt a module out.
terraform_test runs the engine's native
test framework. Each run block is reported as a JUnit test case through
XML_OUTPUT_FILE, so bazel test summaries show which run failed.
Terraform writes the report itself with -junit-xml; for OpenTofu the
runner builds it from the -json-into event stream and keeps that stream
too.
terraform_plan_test runs a real
plan and keeps plan.tfplan, plan.json and plan.txt as undeclared
outputs under bazel-testlogs/<pkg>/<name>/test.outputs/, so a plan can
be reviewed from CI artefacts. It passes when the plan succeeds, or with
expect_no_changes only when the plan is empty. A plan reaches the
backend and the provider APIs, so the target is tagged
requires-network and credentials go through --test_env; providers
that work offline (null, local, random) need neither. Plans hold
sensitive values, so treat the artefacts as you would the state.
terraform_providers_lock_test
fails when .terraform.lock.hcl does not cover what the module tree
requires: every provider a required_providers block declares or a
resource, data, ephemeral or provider block implies, in the root
and in every installed module. No network, no engine.
The @terraform and @tofu hubs
Two repositories let you invoke the toolchain engine in whatever directory you are standing in, like the native CLI:
cd path/to/my/tf/module
bazel run @terraform -- providers lock
bazel run @tofu -- state list
The wrapper chdirs to $BUILD_WORKING_DIRECTORY before exec-ing the
engine. It does no init aspect and no .terraform/ construction: a
terraform init there runs against the source tree and hits the network.
Reach for it for one-off CLI invocations against a directory with no
terraform_binary. Downstream, bind the names:
terraform = use_extension("@rules_terraform//terraform:extensions.bzl", "terraform")
use_repo(terraform, "terraform")
bazel run @rules_terraform//terraform:format (or //tofu:format)
rewrites the .tf files of every module in the workspace with the
toolchain engine's fmt; pass target patterns to narrow it.
Dependencies
Every network fetch happens when the extension evaluates or a repository
is fetched; build actions are offline, and what the engine sees at run
time is a complete module directory. A terraform_module is one
directory: srcs are the files the engine loads (.tf and .tf.json,
plus .tofu and .tofu.json for tofu_module), all directly in the
target's package, and anything else is an analysis error rather than a
file the engine would silently skip. A nested directory of .tf files is
a child module with its own target.
| Dependency | Fetched from | Pinned by | Refresh |
|---|---|---|---|
| Providers | Registry v1 download API | .terraform.lock.hcl | bazel run //path:providers_lock.update |
| Registry modules | Registry v1 API, archives from GitHub | the version each constraint resolved to, a fact in MODULE.bazel.lock | edit the block, or RULES_TERRAFORM_UPGRADE=1 bazel fetch //... |
| Git modules | GitHub archives over https; the host's git for any other remote | the ?ref=; what a tag or branch named is a fact in MODULE.bazel.lock | edit the ?ref=, or RULES_TERRAFORM_UPGRADE=1 bazel fetch //... for a branch |
| Local modules | the Bazel target graph | nothing | edit deps on the parent |
The deps tag
One tag declares both halves of a root's external dependencies, because
that is how the engine treats them: init installs the providers the lock
pins and the modules the module blocks name in one step.
terraform = use_extension("@rules_terraform//terraform:extensions.bzl", "terraform")
terraform.deps(
name = "my_deps",
lock = "//path/to:.terraform.lock.hcl", # optional: providers
roots = ["//path/to:main.tf"], # optional: registry and git modules named in that directory
# registry = "registry.opentofu.org", # default follows the extension
)
use_repo(terraform, "my_deps")
@my_deps carries every provider in the lock, the lock itself, and every
external module the roots name; a terraform_module that depends on it
needs no lock attribute, and a maintainer who adds a provider edits
required_providers and reruns the lock updater with no BUILD change.
@my_deps//:providers, @my_deps//:modules and
@my_deps//<namespace>_<name> are the parts. Archives are pooled across
hubs, so two locks that agree on a version fetch it once, and only the
platform a build resolves is fetched. The attributes are on the
extension page.
Providers
The lock is Terraform's own .terraform.lock.hcl. The
terraform_providers_lock
updater writes it by running the toolchain engine's
providers lock -platform=<all> over the sources, which is the one path
that records h1: hashes for every platform at once; the engine's own
init locks the platform it runs on only. The default set is
linux_amd64, linux_arm64, darwin_amd64, darwin_arm64 and
windows_amd64.
The extension asks the registry only where each archive lives; the
archive's checksum must be one of the lock's zh: hashes, so the
registry cannot change what is installed. A platform is left out of a hub
only when the registry says the version was never published for it;
every other failed lookup fails the evaluation, so an outage cannot leave
a platform permanently missing from the lockfile. The build installs each
provider under .terraform/providers/<registry>/<namespace>/<name>/<version>/<platform>/
and rewrites the h1: hashes in the tree's copy of the lock to the
installed files, which is what lets the engine accept them.
Providers built in the repo
A provider that is not on any registry is a
terraform_provider whose files is
the built binary, used like a hub-provided one:
go_binary(
name = "terraform-provider-custom", # the engine requires this prefix
srcs = ["main.go"],
)
terraform_provider(
name = "custom",
files = [":terraform-provider-custom"],
source = "example/custom", # matches `required_providers`
version = "1.0.0",
)
terraform_module(
name = "app",
srcs = glob(["*.tf"]),
lock = ".terraform.lock.hcl",
deps = [":custom"],
)
Leave platform unset: it is read from the configuration the provider is
built in. The lock still needs an entry for the provider, written by hand
with the version and a placeholder h1:; the build replaces the hash
with that of the binary it installed. A module that depends on providers
but has no lock is an analysis error.
Registry modules
A module block whose source is a registry address is resolved when the
extension evaluates: the highest version satisfying the block's constraint
is chosen once and recorded as a fact in MODULE.bazel.lock, and every
later evaluation declares it until an upgrade is asked for. List the
directory in a deps tag's roots; every .tf file in it is scanned,
and modules composed of other registry or git modules are followed.
terraform.deps(
name = "shared",
lock = "//:.terraform.lock.hcl",
roots = [
"//envs/prod/app:main.tf",
"//modules/naming:main.tf", # a library whose blocks name a registry module
],
)
Archives come straight from GitHub, which is where the public registries
host their modules; a registry module hosted elsewhere is an error with
the address, and is supplied by hand as below. A source written with its
own hostname (app.terraform.io/acme/vpc/aws) is resolved against that
host. OpenTofu users use the tofu extension's deps tag, which defaults
to registry.opentofu.org.
Git modules
A module block whose source is a git address goes through the same
roots scan with no registry involved, in the three spellings Terraform
accepts: git::https://… or git::ssh://…, the scp-like
git@host:org/repo.git, and the github.com/org/repo shorthand, each with
an optional //subdir and ?ref=.
module "vpc" {
source = "git::ssh://git@git.example.com/acme/terraform-modules.git//vpc?ref=v1.4.0"
}
What the ref pins. A commit id is exact. A tag is looked up once and
what it names is recorded, so a moved tag is not followed. A branch, or no
?ref= meaning the default branch, is resolved to a commit the first time
and recorded the same way: a choice, like a version constraint, that only
RULES_TERRAFORM_UPGRADE moves. The native CLI follows such a source live
on every init, so a tag or commit in the block is what keeps the two
workflows on one revision.
GitHub over https needs no git: the extension fetches the archive
GitHub serves for the ref, records its integrity, and declares an
http_archive pinned by it. Every other remote, ssh:// to GitHub
included, goes through the host's git: the ref is resolved with
ls-remote, checked out once to record the commit and read the module's
own references, and built as a new_git_repository of that commit,
fetched with the host's credentials. git has to be on PATH when the
extension evaluates and when the repository is fetched.
Not handled: ?sshkey=, submodules, and non-git sources such as plain
https:// archives or s3::.
Modules the extension cannot fetch
Fetch the archive with any repository rule, declare a
terraform_external_module
in its build file with source copied verbatim from the module block,
and put the target in deps; the build installs it under every block
whose source matches.
# MODULE.bazel
http_archive = use_repo_rule("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")
http_archive(
name = "acme_vpc",
urls = ["https://git.example.com/acme/terraform-vpc/-/archive/v1.4.0/terraform-vpc-v1.4.0.tar.gz"],
strip_prefix = "terraform-vpc-v1.4.0",
integrity = "sha256-…",
build_file_content = """
load("@rules_terraform//terraform:defs.bzl", "terraform_external_module")
terraform_external_module(
name = "module",
source = "https://git.example.com/acme/terraform-vpc/-/archive/v1.4.0/terraform-vpc-v1.4.0.tar.gz",
srcs = glob(["**/*.tf"]),
visibility = ["//visibility:public"],
)
""",
)
Local modules
A child module in the repository is its own terraform_module, listed in
the parent's deps; the path lives in the module block alone.
module "greeter" {
source = "./modules/greeter"
}
terraform_module(
name = "root",
srcs = glob(["*.tf"]),
deps = ["//root/modules/greeter"],
)
A child whose package sits under the parent's lands at the path its
package implies. Any other child is matched to the module block whose
source names it by the tail of its package, so //shared/greeter
satisfies source = "./greeter". A source no dep supplies, a dep no
block references, and two deps that could satisfy one source are build
errors.
Reproducibility
The extensions are reproducible = True: given the same tags, files and
facts they declare the same repositories. Every answer taken from the
network is a fact in MODULE.bazel.lock: a tagged engine release's
checksums, where a provider archive lives, the version a module
constraint resolved to, the commit a git ref named, an archive's
integrity. A .tf edit under a roots directory re-evaluates the
extension, but with warm facts that makes no network calls.
Facts persist only while the lockfile is enabled, so downstream:
common --lockfile_mode=update
# or, once confident:
common --lockfile_mode=strict
and commit MODULE.bazel.lock. That gives deterministic module versions
however loosely the constraints are written, and a PR-visible diff
whenever a resolution changes. --lockfile_mode=off resolves live on
every evaluation, exactly as terraform init does. An exact pin in a
module block is still worth writing: it is the only form the native
init also treats as fixed.
Upgrading
A recorded resolution is kept on purpose. To move a constraint or a branch to what it resolves to today:
RULES_TERRAFORM_UPGRADE=1 bazel fetch //... # or --repo_env=RULES_TERRAFORM_UPGRADE=1
Every constraint and git ref is resolved again and the facts rewritten;
facts that are properties of a published version, such as an archive's
integrity, are kept. Review the MODULE.bazel.lock diff and commit it.
Editing a block's source, version or ?ref= is itself a new key, so a
changed block resolves on the next bazel fetch without the switch.
Terraform Rules
Public rules
- terraform_binary —
bazel runwrapper for anyterraformsubcommand. - terraform_module — declares a
.tfmodule with providers and deps. - terraform_external_module — supplies a module fetched from outside the workspace (git, private hosts) to
deps. - terraform_provider — declares a provider built in the repo. Registry providers come from a
depstag's hub instead. - terraform_test — runs Terraform's HCL native test framework, reporting each
runblock as a JUnit test case. - terraform_plan_test — runs a real
planand keeps it as a reviewable test output; needs network. - terraform_validate_test / aspect —
terraform validateas a Bazel test/aspect. - terraform_fmt_test / aspect —
terraform fmt -checkas a Bazel test/aspect. - terraform_providers_lock —
bazel runupdater that writes.terraform.lock.hclwith the toolchain engine; nothing needs installing on the host. - terraform_providers_lock_test — fails when the lock file and the
required_providersblocks disagree.
Module extensions
- extensions —
terraform:toolchainanddepstags.
terraform_binary
Rules
terraform_binary
load("@rules_terraform//terraform:terraform_binary.bzl", "terraform_binary")
terraform_binary(name, env, root, workspace)
Runs terraform on a terraform_module root. Arguments pass through:
bazel run //x:terraform -- plan, -- apply, -- state list. The engine
runs in a temp copy of the built module directory, providers and modules
already installed, so nothing is fetched at run time.
init is the exception: it installs that directory into the module's
source directory and initializes the backend there, so the native CLI
works in that directory afterwards.
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| env | Environment variables for the engine, set over the host's for a run and over the hermetic one for a test: TF_VAR_<name> for a root keyed on an input variable, TF_LOG, TF_CLI_ARGS_plan. Values are literal. TF_WORKSPACE here and workspace must agree. | Dictionary: String -> String | optional | {} |
| root | The terraform_module target that serves as the root module. | Label | required | |
| workspace | Run in this Terraform workspace, over anything the environment or --test_env sets. Unset, the environment's TF_WORKSPACE applies, else default. One test per workspace is how a root keyed on terraform.workspace gets each one checked, and one binary per workspace runs it with no workspace select first. | String | optional | "" |
terraform_external_module
Rules
terraform_external_module
load("@rules_terraform//terraform:terraform_external_module.bzl", "terraform_external_module")
terraform_external_module(name, srcs, source, subdir, version)
A module whose files come from outside the workspace.
The deps tag declares one per registry or git module it resolves. Declare
one yourself, in the build_file_content of an http_archive or
git_repository, for a module the extension cannot fetch. source must
match the module block verbatim; list the target in the consuming
terraform_module's deps.
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| srcs | Every file of the module's repository or archive, not only the module's own: templates and other data the configuration reads, and the siblings a ../ source inside it names. | List of labels | optional | [] |
| source | The source string exactly as the module blocks write it. The module is installed under every block whose source matches. | String | required | |
| subdir | The module's directory inside srcs: the //subdir of the source, empty for the archive root. | String | optional | "" |
| version | The module's version, for a registry module. Leave it empty for a git or other source: the manifest only accepts a semantic version. | String | optional | "" |
terraform_fmt_aspect
Aspects
terraform_fmt_aspect
load("@rules_terraform//terraform:terraform_fmt_aspect.bzl", "terraform_fmt_aspect")
terraform_fmt_aspect()
An aspect for running terraform fmt on targets with Terraform sources.
ASPECT ATTRIBUTES
ATTRIBUTES
terraform_fmt_test
Rules
terraform_fmt_test
load("@rules_terraform//terraform:terraform_fmt_test.bzl", "terraform_fmt_test")
terraform_fmt_test(name, target)
Fails when terraform fmt would change a module's sources.
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| target | The module to check. | Label | required |
terraform_module
Rules
terraform_module
load("@rules_terraform//terraform:terraform_module.bzl", "terraform_module")
terraform_module(name, deps, srcs, data, disposable_state, lock)
One directory of terraform configuration: a root module, or a child that another module's module block names.
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| deps | What the configuration references: child terraform_modules for its local module blocks, a dependency hub (@<name> from a deps tag, or its //:providers / //:modules halves) or single @<name>//<namespace>_<name> providers, and terraform_provider or terraform_external_module targets. The module block names a child's path; deps says which target supplies it. A child in a package under this one lands at its relative path; any other is matched to the block whose source ends with its package name. | List of labels | optional | [] |
| srcs | The configuration files of this module: .tf, .tf.json, all directly in this target's package. A nested directory of configuration is its own module target, named by a module block. | List of labels | optional | [] |
| data | Additional files or targets that should be available at runtime. | List of labels | optional | [] |
| disposable_state | Whether local state may go with the run, for a module with no backend or cloud block. True, the default: the engine keeps state in the run's temp directory, which is deleted afterwards; a run that wrote state is kept and its path printed. False: state is kept under bazel-out/rules_terraform/ across runs, until bazel clean. An error when the module declares a backend. | Boolean | optional | True |
| lock | The module's .terraform.lock.hcl. Not needed when deps names a dependency hub, which carries its lock. | Label | optional | None |
terraform_plan_test
Functions
terraform_plan_test
load("@rules_terraform//terraform:terraform_plan_test.bzl", "terraform_plan_test")
terraform_plan_test(name, tags, **kwargs)
Runs terraform plan as a test and keeps the plan for review.
The rule of the same name, tagged requires-network: a plan reaches the
backend and the provider APIs, with credentials from --test_env.
PARAMETERS
terraform_providers_lock
Rules
terraform_providers_lock
load("@rules_terraform//terraform:terraform_providers_lock.bzl", "terraform_providers_lock")
terraform_providers_lock(name, output, platforms, target)
bazel run target that writes a module's .terraform.lock.hcl with the
toolchain terraform, recording hashes for every platform in platforms at
once, which the engine's own init does not do. Needs the network.
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| output | Where to write the lock, relative to this package: .terraform.lock.hcl. | String | required | |
| platforms | Platforms to record hashes for, as linux_amd64. Default: linux_amd64, linux_arm64, darwin_amd64, darwin_arm64, windows_amd64. | List of strings | optional | [] |
| target | The terraform_module the lock covers, its local child modules included. | Label | required |
terraform_providers_lock_test
Rules
terraform_providers_lock_test
load("@rules_terraform//terraform:terraform_providers_lock_test.bzl", "terraform_providers_lock_test")
terraform_providers_lock_test(name, lock, target)
Fails when .terraform.lock.hcl does not match what the module
tree requires: every module the init aspect placed, root, local children
and installed external ones, and in each the providers required_providers
declares plus the ones resource, data, ephemeral and provider
blocks imply. A provider the configuration needs but the lock lacks fails,
and so does a lock entry nothing needs. Network-free; no engine runs.
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| lock | The .terraform.lock.hcl file being verified. | Label | required | |
| target | The terraform_module whose module tree is being checked. | Label | required |
terraform_provider
Rules
terraform_provider
load("@rules_terraform//terraform:terraform_provider.bzl", "terraform_provider")
terraform_provider(name, files, platform, source, version)
A provider built in this repository, for a terraform_module's
deps. Registry providers come from a deps tag's hub instead.
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| files | The provider binary and any files shipped with it. | List of labels | required | |
| platform | The platform, as linux_amd64. Leave it empty for a provider built here: it is taken from the configuration the provider is built in. | String | optional | "" |
| source | The provider address as required_providers writes it, e.g. hashicorp/null. | String | required | |
| version | The provider version, e.g. 3.2.4. | String | required |
terraform_test
Rules
terraform_test
load("@rules_terraform//terraform:terraform_test.bzl", "terraform_test")
terraform_test(name, env, env_inherit, root, workspace)
Runs terraform test on the root module's .tftest.hcl files as a Bazel
test. Each run block is a JUnit test case in Bazel's XML report, and the
lock and module manifest the test ran against are kept as undeclared test
outputs. --test_arg is ignored.
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| env | Environment variables for the engine, set over the host's for a run and over the hermetic one for a test: TF_VAR_<name> for a root keyed on an input variable, TF_LOG, TF_CLI_ARGS_plan. Values are literal. TF_WORKSPACE here and workspace must agree. | Dictionary: String -> String | optional | {} |
| env_inherit | Variables the test takes from the environment bazel test ran in, AWS_PROFILE say. The variables tests scrub for hermeticity (the CLI configuration ones) cannot be inherited, and the runner says so. | List of strings | optional | [] |
| root | The terraform_module target that serves as the root module. | Label | required | |
| workspace | Run in this Terraform workspace, over anything the environment or --test_env sets. Unset, the environment's TF_WORKSPACE applies, else default. One test per workspace is how a root keyed on terraform.workspace gets each one checked, and one binary per workspace runs it with no workspace select first. | String | optional | "" |
terraform_validate_aspect
Aspects
terraform_validate_aspect
load("@rules_terraform//terraform:terraform_validate_aspect.bzl", "terraform_validate_aspect")
terraform_validate_aspect()
An aspect for running terraform validate on targets with Terraform sources.
ASPECT ATTRIBUTES
ATTRIBUTES
terraform_validate_test
Rules
terraform_validate_test
load("@rules_terraform//terraform:terraform_validate_test.bzl", "terraform_validate_test")
terraform_validate_test(name, env, env_inherit, target, workspace)
A rule for running terraform validate on a Terraform target.
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| env | Environment variables for the engine, set over the host's for a run and over the hermetic one for a test: TF_VAR_<name> for a root keyed on an input variable, TF_LOG, TF_CLI_ARGS_plan. Values are literal. TF_WORKSPACE here and workspace must agree. | Dictionary: String -> String | optional | {} |
| env_inherit | Variables the test takes from the environment bazel test ran in, AWS_PROFILE say. The variables tests scrub for hermeticity (the CLI configuration ones) cannot be inherited, and the runner says so. | List of strings | optional | [] |
| target | The target to validate. | Label | required | |
| workspace | Run in this Terraform workspace, over anything the environment or --test_env sets. Unset, the environment's TF_WORKSPACE applies, else default. One test per workspace is how a root keyed on terraform.workspace gets each one checked, and one binary per workspace runs it with no workspace select first. | String | optional | "" |
Terraform Extensions
One module extension lives in //terraform:extensions.bzl, terraform,
with two tag classes.
terraform.toolchain
The extension always materializes one hub repo (@terraform_toolchains)
that declares a toolchain per platform for one Terraform release, and
@terraform, the CLI hub. rules_terraform's own MODULE.bazel use_repos
the toolchain hub and register_toolchainss it, so downstream users get
toolchain resolution for free. Per-platform http_archives are lazy; only
the resolved platform is fetched.
The release is the root module's toolchain tag, else the newest shipped
in //terraform/private:versions.bzl (the three newest when the table was
generated carry checksums). For any other release the extension fetches
its SHA256SUMS once and records the checksums as facts in
MODULE.bazel.lock.
# Nothing to do beyond `bazel_dep` to use the newest shipped release.
bazel_dep(name = "rules_terraform", version = "...")
# To use another release:
terraform = use_extension("@rules_terraform//terraform:extensions.bzl", "terraform")
terraform.toolchain(version = "1.9.8")
terraform.toolchain
| Attribute | Type | Description |
|---|---|---|
version | string (required) | The release, e.g. 1.9.8. |
Root module only, at most one tag; tags in dependencies are ignored. A build has one engine release, and changing it is editing the tag.
terraform.deps
The deps tag class: the providers a lock file pins and the
registry and git modules some directories' module blocks name, as one
hub per tag.
terraform = use_extension("@rules_terraform//terraform:extensions.bzl", "terraform")
# One `deps` tag per lock file. `lock` brings the providers the lock pins;
# `roots` brings the registry and git modules named by the `module` blocks
# in those directories (root modules and shared libraries alike). Either may
# be omitted.
terraform.deps(
name = "app",
lock = "//path/to/module:.terraform.lock.hcl",
roots = [
"//path/to/module:main.tf",
"//path/to/library:main.tf",
],
# registry = "registry.opentofu.org", # optional; defaults to registry.terraform.io
)
use_repo(terraform, "app")
@app is a terraform_dependency_group: every provider in the lock, the
lock itself, and every external module the roots name. A terraform_module
that depends on it needs no lock attribute. @app//:providers and
@app//:modules are the two halves, and @app//<namespace>_<name> is one
provider. Name the hub after what it serves rather than after the tag.
Hubs are alias layers. The archives themselves go into pools shared by
every hub of the extension: one repository per distinct provider archive
(rt_prov_<registry>_<namespace>_<name>_<version>_<platform>) and one per
distinct module archive (rt_mod_<registry>_<address>_<version>), so two
hubs that agree on a version fetch and extract it once. Hub names share
one namespace across every Bazel module that uses the extension; a
duplicate is reported at evaluation time.
terraform.deps
| Attribute | Type | Description |
|---|---|---|
name | string (required) | Hub repository name. |
lock | label | A .terraform.lock.hcl. Every provider it pins is fetched for every platform the registry publishes. |
roots | label_list | Directories whose module { source = "…" } blocks name registry or git modules (git::…, git@…, github.com/…, ?ref= a tag, a full commit id or a branch; a branch, or no ref, resolves to a commit once and is kept as a fact), each as any .tf file inside it. Every *.tf in each directory is scanned; modules composed of other registry or git modules are followed. GitHub https remotes are fetched as integrity-pinned archives with no git involved; other remotes go through the host's git. |
root | label | Shorthand for a one-element roots. |
registry | string (default empty) | Registry host to query for lock entries and sources that name none. Empty means registry.terraform.io from this extension, registry.opentofu.org from the OpenTofu one. |
OpenTofu Rules
Everything under //tofu mirrors //terraform but binds to
//tofu:toolchain_type. Rule bodies are identical — only the
resolved toolchain differs — so behavior stays in lockstep between the
two engines. Data-only rules (tofu_module, tofu_external_module,
tofu_provider, tofu_provider_group) are literal aliases for their terraform_*
counterparts, provided so an OpenTofu-only project never has to import
a terraform_* symbol.
Public rules
- tofu_binary —
bazel runwrapper for anytofusubcommand. - tofu_module — alias for
terraform_module. - tofu_external_module — alias for
terraform_external_module. - tofu_provider — alias for
terraform_provider. - tofu_test — runs OpenTofu's HCL native test framework, reporting each
runblock as a JUnit test case. - tofu_plan_test — runs a real
planand keeps it as a reviewable test output; needs network. - tofu_validate_test —
tofu validateas a Bazel test. - tofu_fmt_test —
tofu fmt -checkas a Bazel test. - tofu_providers_lock —
bazel runupdater that writes.terraform.lock.hclwith the toolchaintofu. - tofu_providers_lock_test — alias for the terraform drift test.
Module extensions
- extensions —
tofu:toolchainanddepstags.
tofu_binary
Rules
tofu_binary
load("@rules_terraform//tofu:tofu_binary.bzl", "tofu_binary")
tofu_binary(name, env, root, workspace)
Runs tofu on a terraform_module root. Arguments pass through:
bazel run //x:tofu -- plan, -- apply, -- state list. The engine
runs in a temp copy of the built module directory, providers and modules
already installed, so nothing is fetched at run time.
init is the exception: it installs that directory into the module's
source directory and initializes the backend there, so the native CLI
works in that directory afterwards.
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| env | Environment variables for the engine, set over the host's for a run and over the hermetic one for a test: TF_VAR_<name> for a root keyed on an input variable, TF_LOG, TF_CLI_ARGS_plan. Values are literal. TF_WORKSPACE here and workspace must agree. | Dictionary: String -> String | optional | {} |
| root | The terraform_module target that serves as the root module. | Label | required | |
| workspace | Run in this Terraform workspace, over anything the environment or --test_env sets. Unset, the environment's TF_WORKSPACE applies, else default. One test per workspace is how a root keyed on terraform.workspace gets each one checked, and one binary per workspace runs it with no workspace select first. | String | optional | "" |
tofu_external_module — alias for terraform_external_module.
Same rule under both names: a module fetched from outside the workspace is
engine-neutral, so tofu_external_module is just a naming alias so
OpenTofu-only projects don't have to import a terraform_* symbol.
Rules
tofu_external_module
load("@rules_terraform//tofu:tofu_external_module.bzl", "tofu_external_module")
tofu_external_module(name, srcs, source, subdir, version)
A module whose files come from outside the workspace.
The deps tag declares one per registry or git module it resolves. Declare
one yourself, in the build_file_content of an http_archive or
git_repository, for a module the extension cannot fetch. source must
match the module block verbatim; list the target in the consuming
terraform_module's deps.
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| srcs | Every file of the module's repository or archive, not only the module's own: templates and other data the configuration reads, and the siblings a ../ source inside it names. | List of labels | optional | [] |
| source | The source string exactly as the module blocks write it. The module is installed under every block whose source matches. | String | required | |
| subdir | The module's directory inside srcs: the //subdir of the source, empty for the archive root. | String | optional | "" |
| version | The module's version, for a registry module. Leave it empty for a git or other source: the manifest only accepts a semantic version. | String | optional | "" |
tofu_fmt_test
Rules
Aspects
tofu_fmt_test
load("@rules_terraform//tofu:tofu_fmt_test.bzl", "tofu_fmt_test")
tofu_fmt_test(name, target)
Fails when tofu fmt would change a module's sources.
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| target | The module to check. | Label | required |
tofu_fmt_aspect
load("@rules_terraform//tofu:tofu_fmt_test.bzl", "tofu_fmt_aspect")
tofu_fmt_aspect()
An aspect for running tofu fmt on targets with Terraform sources.
ASPECT ATTRIBUTES
ATTRIBUTES
tofu_providers_lock
Rules
tofu_providers_lock
load("@rules_terraform//tofu:tofu_providers_lock.bzl", "tofu_providers_lock")
tofu_providers_lock(name, output, platforms, target)
bazel run target that writes a module's .terraform.lock.hcl with the
toolchain tofu, recording hashes for every platform in platforms at
once, which the engine's own init does not do. Needs the network.
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| output | Where to write the lock, relative to this package: .terraform.lock.hcl. | String | required | |
| platforms | Platforms to record hashes for, as linux_amd64. Default: linux_amd64, linux_arm64, darwin_amd64, darwin_arm64, windows_amd64. | List of strings | optional | [] |
| target | The terraform_module the lock covers, its local child modules included. | Label | required |
tofu_providers_lock_test
Rules
tofu_providers_lock_test
load("@rules_terraform//tofu:tofu_providers_lock_test.bzl", "tofu_providers_lock_test")
tofu_providers_lock_test(name, lock, target)
Fails when .terraform.lock.hcl does not match what the module
tree requires: every module the init aspect placed, root, local children
and installed external ones, and in each the providers required_providers
declares plus the ones resource, data, ephemeral and provider
blocks imply. A provider the configuration needs but the lock lacks fails,
and so does a lock entry nothing needs. Network-free; no engine runs.
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| lock | The .terraform.lock.hcl file being verified. | Label | required | |
| target | The terraform_module whose module tree is being checked. | Label | required |
tofu_module
The same rule body as terraform_module, bound to OpenTofu: srcs also
accepts .tofu and .tofu.json, which Terraform would ignore.
Rules
tofu_module
load("@rules_terraform//tofu:tofu_module.bzl", "tofu_module")
tofu_module(name, deps, srcs, data, disposable_state, lock)
One directory of tofu configuration: a root module, or a child that another module's module block names.
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| deps | What the configuration references: child terraform_modules for its local module blocks, a dependency hub (@<name> from a deps tag, or its //:providers / //:modules halves) or single @<name>//<namespace>_<name> providers, and terraform_provider or terraform_external_module targets. The module block names a child's path; deps says which target supplies it. A child in a package under this one lands at its relative path; any other is matched to the block whose source ends with its package name. | List of labels | optional | [] |
| srcs | The configuration files of this module: .tf, .tf.json, .tofu, .tofu.json, all directly in this target's package. A nested directory of configuration is its own module target, named by a module block. | List of labels | optional | [] |
| data | Additional files or targets that should be available at runtime. | List of labels | optional | [] |
| disposable_state | Whether local state may go with the run, for a module with no backend or cloud block. True, the default: the engine keeps state in the run's temp directory, which is deleted afterwards; a run that wrote state is kept and its path printed. False: state is kept under bazel-out/rules_terraform/ across runs, until bazel clean. An error when the module declares a backend. | Boolean | optional | True |
| lock | The module's .terraform.lock.hcl. Not needed when deps names a dependency hub, which carries its lock. | Label | optional | None |
tofu_plan_test
Functions
tofu_plan_test
load("@rules_terraform//tofu:tofu_plan_test.bzl", "tofu_plan_test")
tofu_plan_test(name, tags, **kwargs)
Runs tofu plan as a test and keeps the plan for review.
The rule of the same name, tagged requires-network: a plan reaches the
backend and the provider APIs, with credentials from --test_env.
PARAMETERS
tofu_provider — alias for terraform_provider.
Same rule under both names: a checked-in provider binary declaration is
engine-neutral, so tofu_provider is just a naming alias so
OpenTofu-only projects don't have to import a terraform_* symbol.
Rules
tofu_provider
load("@rules_terraform//tofu:tofu_provider.bzl", "tofu_provider")
tofu_provider(name, files, platform, source, version)
A provider built in this repository, for a terraform_module's
deps. Registry providers come from a deps tag's hub instead.
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| files | The provider binary and any files shipped with it. | List of labels | required | |
| platform | The platform, as linux_amd64. Leave it empty for a provider built here: it is taken from the configuration the provider is built in. | String | optional | "" |
| source | The provider address as required_providers writes it, e.g. hashicorp/null. | String | required | |
| version | The provider version, e.g. 3.2.4. | String | required |
tofu_test
Rules
tofu_test
load("@rules_terraform//tofu:tofu_test.bzl", "tofu_test")
tofu_test(name, env, env_inherit, root, workspace)
Runs tofu test on the root module's .tftest.hcl files as a Bazel
test. Each run block is a JUnit test case in Bazel's XML report, and the
lock and module manifest the test ran against are kept as undeclared test
outputs. --test_arg is ignored.
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| env | Environment variables for the engine, set over the host's for a run and over the hermetic one for a test: TF_VAR_<name> for a root keyed on an input variable, TF_LOG, TF_CLI_ARGS_plan. Values are literal. TF_WORKSPACE here and workspace must agree. | Dictionary: String -> String | optional | {} |
| env_inherit | Variables the test takes from the environment bazel test ran in, AWS_PROFILE say. The variables tests scrub for hermeticity (the CLI configuration ones) cannot be inherited, and the runner says so. | List of strings | optional | [] |
| root | The terraform_module target that serves as the root module. | Label | required | |
| workspace | Run in this Terraform workspace, over anything the environment or --test_env sets. Unset, the environment's TF_WORKSPACE applies, else default. One test per workspace is how a root keyed on terraform.workspace gets each one checked, and one binary per workspace runs it with no workspace select first. | String | optional | "" |
tofu_validate_test
Rules
Aspects
tofu_validate_test
load("@rules_terraform//tofu:tofu_validate_test.bzl", "tofu_validate_test")
tofu_validate_test(name, env, env_inherit, target, workspace)
A rule for running tofu validate on a Terraform target.
ATTRIBUTES
| Name | Description | Type | Mandatory | Default |
|---|---|---|---|---|
| name | A unique name for this target. | Name | required | |
| env | Environment variables for the engine, set over the host's for a run and over the hermetic one for a test: TF_VAR_<name> for a root keyed on an input variable, TF_LOG, TF_CLI_ARGS_plan. Values are literal. TF_WORKSPACE here and workspace must agree. | Dictionary: String -> String | optional | {} |
| env_inherit | Variables the test takes from the environment bazel test ran in, AWS_PROFILE say. The variables tests scrub for hermeticity (the CLI configuration ones) cannot be inherited, and the runner says so. | List of strings | optional | [] |
| target | The target to validate. | Label | required | |
| workspace | Run in this Terraform workspace, over anything the environment or --test_env sets. Unset, the environment's TF_WORKSPACE applies, else default. One test per workspace is how a root keyed on terraform.workspace gets each one checked, and one binary per workspace runs it with no workspace select first. | String | optional | "" |
tofu_validate_aspect
load("@rules_terraform//tofu:tofu_validate_test.bzl", "tofu_validate_aspect")
tofu_validate_aspect()
An aspect for running tofu validate on targets with Terraform sources.
ASPECT ATTRIBUTES
ATTRIBUTES
OpenTofu Extensions
One module extension lives in //tofu:extensions.bzl, tofu, with the
same two tag classes as the terraform extension.
tofu.toolchain
The extension always materializes @tofu_toolchains, a hub declaring a
toolchain per platform for one OpenTofu release, the root module's
toolchain tag or else the newest shipped in //tofu/private:versions.bzl,
and @tofu, the CLI hub. rules_terraform's own MODULE.bazel registers the
hub so downstream users get toolchain resolution for free.
# Nothing to do beyond `bazel_dep` to use the newest shipped release.
bazel_dep(name = "rules_terraform", version = "...")
# To use another release:
tofu = use_extension("@rules_terraform//tofu:extensions.bzl", "tofu")
tofu.toolchain(version = "1.9.1")
tofu.toolchain takes the same version attribute as the Terraform
extension's tag, with the same root-only rule; see
the Terraform page. For a tagged release the
extension fetches that release's SHA256SUMS once and records the
checksums as facts in MODULE.bazel.lock.
tofu.deps
The same deps tag class as the terraform extension, defaulting to
registry.opentofu.org. An OpenTofu-only
repository uses it for everything and never loads
//terraform:extensions.bzl:
tofu = use_extension("@rules_terraform//tofu:extensions.bzl", "tofu")
tofu.deps(
name = "app",
lock = "//path/to/module:.terraform.lock.hcl", # written by tofu
roots = ["//path/to/module:main.tf"],
)
use_repo(tofu, "app")
The attributes are those documented for the
terraform extension; only the default
registry differs. That default matters: registry.opentofu.org serves
OpenTofu's own builds of the hashicorp/* providers, whose archive hashes
differ from HashiCorp's, so a lock file written by tofu has to be
resolved against it. Write the lock with the module's
tofu_providers_lock updater and it will be.
Each extension pools its own archives. A repository using both engines against the same dependencies should declare them through one extension rather than both, or the same archive is fetched twice.
Gazelle
rules_terraform ships a Gazelle
language per engine. It writes the BUILD files the rules need from the
configuration itself: one module target per directory of .tf files, with
deps kept in step with the module blocks, and the binary and lock targets
for a root.
Setup
# BUILD.bazel at the repository root
load("@gazelle//:def.bzl", "gazelle", "gazelle_binary")
gazelle_binary(
name = "gazelle_bin",
languages = [
"@rules_terraform//terraform/gazelle",
# "@rules_terraform//tofu/gazelle", # for OpenTofu directories
],
)
gazelle(
name = "gazelle",
gazelle = ":gazelle_bin",
)
bazel run //:gazelle then writes or updates BUILD files; bazel run //:gazelle -- -mode diff shows what it would change, which is what a CI
check runs.
What a directory becomes
| The directory | Generated |
|---|---|
| has configuration files | terraform_module(name = <directory name>, srcs = [...], data = [...], deps = [...]) |
also declares a backend or cloud block | plus terraform_binary(name = "terraform") and terraform_providers_lock(name = "providers_lock.update") |
also has a .terraform.lock.hcl | plus terraform_providers_lock_test(name = "providers_lock"), and the updater if the directory is not a root |
holds .tofu files | the tofu_* kinds instead |
srcs are the files the engine loads, .tf and .tf.json, plus .tofu
and .tofu.json under OpenTofu. data is everything else a native run in
that directory could read: the remaining files, and the contents of
subdirectories that are not packages of their own. Build files, the lock,
state, and dotfiles are left out, as is anything under .terraform/.
deps has two parts. Every module block with a local source (./… or
../…) resolves to the module target in that directory; a directory nothing
generates a target for is reported. Then the dependency hubs: the ones a
tf_deps_hub directive names, or, for a root with a lock file and no
directive, @tf_<path> with / and - replaced by _.
Rules are merged, not overwritten: a hand-written tags, visibility, or
disposable_state stays, comments stay, and an attribute written as a glob()
is left alone with a warning, since a list cannot be merged into it. Delete
the glob to let Gazelle write the list.
Directives
| Directive | Meaning |
|---|---|
# gazelle:tf_engine terraform|tofu | Which engine's rules a directory tree gets. Unset, a directory with .tofu files is OpenTofu's, the rest Terraform's. |
# gazelle:tf_kind root|module|auto | Force a root (binary and lock targets) or a library. auto, the default, reads the configuration for a backend. |
# gazelle:tf_deps_hub NAME... | The hubs (@NAME) modules under the directory depend on. An empty value clears an inherited one. |
# gazelle:tf_default_visibility LABEL... | Visibility written on new library modules under the directory. An existing rule keeps its own. |
Directives inherit down the tree, so one tf_deps_hub on a module library's
parent directory covers every module in it, and one tf_engine tofu at the
repository root makes an OpenTofu-only repository.
What it does not do
It does not write .terraform.lock.hcl; that is providers_lock.update.
It does not declare the deps tags in MODULE.bazel; the hub names it
writes are the names those tags must have. And it does not read expressions:
a module block whose source is not a literal is not resolved.