rules_terraform

Overview

This repository implements Bazel rules for Terraform and OpenTofu.

Setup

Add rules_terraform to your MODULE.bazel:

bazel_dep(name = "rules_terraform", version = "{version}")

Toolchains for both engines are auto-registered — no use_extension or register_toolchains call is required. Pick a specific version by flipping the corresponding string flag:

--@rules_terraform//terraform/settings:version=1.14.1
--@rules_terraform//opentofu/settings:version=1.10.6

Both default to the latest release shipped in //terraform/private:versions.bzl / //opentofu/private:versions.bzl.

To fetch external providers or registry modules, opt into the terraform extension explicitly:

terraform = use_extension("@rules_terraform//terraform:extensions.bzl", "terraform")
terraform.providers(
    name = "my_providers",
    lock = "//path/to:.terraform.lock.hcl",
)
use_repo(terraform, "my_providers")

Running the engine CLI directly (@terraform / @opentofu)

rules_terraform ships two hub repositories — @terraform and @opentofu — that let a user invoke the toolchain-resolved engine binary from any subdirectory of their workspace:

cd path/to/my/tf/module
bazel run @terraform -- plan             # ≈ terraform plan
bazel run @terraform -- init             # ≈ terraform init
bazel run @opentofu -- state list        # ≈ tofu state list

The wrapper chdirs to $BUILD_WORKING_DIRECTORY (the shell cwd where bazel run was invoked) before exec-ing the engine, so relative paths and any terraform.tfstate land in the directory you're standing in — just like the native CLI. Version resolution follows the same //terraform/settings:version / //opentofu/settings:version flags as the rest of the ruleset, so you get one hermetic Terraform release across all invocations.

This is NOT the Bazel-managed workflow. @terraform does no init aspect, no lock-file rewriting, no .terraform construction — terraform init still runs against your source tree, hits the network, and writes state. Reach for it when you want a one-off CLI invocation (state manipulation, ad-hoc plan against an unrelated module) without wiring up a terraform_binary target for the module.

To use @terraform / @opentofu from downstream, add them to your MODULE.bazel:

terraform_toolchains = use_extension("@rules_terraform//terraform:extensions.bzl", "terraform_toolchains")
use_repo(terraform_toolchains, "terraform")

opentofu_toolchains = use_extension("@rules_terraform//opentofu:extensions.bzl", "opentofu_toolchains")
use_repo(opentofu_toolchains, "opentofu")

The extensions themselves are already invoked by rules_terraform's own MODULE.bazel (they register toolchains); the use_repo line only binds the repo name in your namespace so @terraform / @opentofu resolve.

Bazel-managed Terraform

When a terraform_module is built through Bazel, the .terraform directory is assembled hermetically by an init aspect rather than by terraform init. Every network fetch happens at repository-rule / bzlmod-extension time; build actions are offline. As a consequence:

  • Provider binaries are fetched at repository-rule time and copied into .terraform/providers/… during the aspect's action. The lock file's h1: hashes are rewritten to match the actually-installed binary layout.
  • External modules (from a Terraform registry) are resolved via a Bazel-owned lock file, downloaded at repository-rule time, and copied into .terraform/modules/… with a generated modules.json manifest.
  • Cross-package modules (other terraform_module targets in the monorepo) are wired via the module_sources attribute on the parent and materialized into .terraform/modules/… alongside the registry modules.

The full mechanics — how the extensions resolve each dependency type, what each lock file looks like, and how to keep them fresh — are in External dependencies and lock files below.

Direct terraform CLI incompatibility

Once a terraform_module has Bazel-managed dependencies, running terraform (or tofu) directly outside Bazel is not expected to work: the .terraform directory is Bazel's construction, h1: hashes are rewritten for the installed platform binary, and modules.json reflects the Bazel target graph — not the source tree.

Use bazel run to invoke the engine:

# Instead of: terraform plan
bazel run //path/to:terraform -- plan

# Instead of: terraform apply
bazel run //path/to:terraform -- apply

The terraform_binary rule creates an executable that sets up a hermetic working directory with all dependencies wired into place before delegating to the real terraform binary.

For validation and formatting, use the corresponding test rules:

terraform_validate_test(
    name = "validate_test",
    target = ":my_module",
)

terraform_fmt_test(
    name = "fmt_test",
    target = ":my_module",
)

External dependencies and lock files

rules_terraform partitions external Terraform state into three buckets, each with its own resolution path and (where applicable) lock file. Every network fetch happens at repository-rule / bzlmod-extension time — build actions never touch the network. What the aspect and Terraform actually see at run time is a fully-populated .terraform/ tree.

At a glance

Dependency typeFetched fromLock fileRefresh via
ProvidersTerraform Registry v1 API.terraform.lock.hclbazel run //path:providers_lock (a terraform_providers_lock target) — see below
Registry modulesTerraform Registry v1 APInone — live-resolved by the terraform.modules(...) / opentofu.modules(...) extensionAutomatic on bazel fetch. See "External registry modules" below for the tradeoff.
Local / in-repo modulesBazel target graphnoneEdit deps / module_sources on the parent terraform_module

Providers

terraform = use_extension("@rules_terraform//terraform:extensions.bzl", "terraform")
terraform.providers(
    name = "my_providers",
    lock = "//path/to:.terraform.lock.hcl",
    # Optional; default is `registry.terraform.io`. Use
    # `registry.opentofu.org` if you're fetching from the OpenTofu registry.
    # registry = "registry.opentofu.org",
)
use_repo(terraform, "my_providers")

The lock file is Terraform's native .terraform.lock.hcl — rules_terraform consumes it, it doesn't author it.

At repo-rule time the extension:

  1. Reads the lock file via module_ctx.read.
  2. For every (provider, platform) pair, calls https://<registry>/v1/providers/<namespace>/<name>/<version>/download/<os>/<arch> via module_ctx.download(...) and reads the returned JSON to obtain the archive download_url + shasum.
  3. Declares an http_archive per pair. These are lazy — Bazel only fetches the archive for the platform your build actually resolves.
  4. Emits a hub repo @my_providers whose per-provider aliases pick the right platform archive via select(). Depend on @my_providers//<namespace>_<name> (e.g. @my_providers//hashicorp_null) from your terraform_module.deps.

At build time terraform_init_aspect extracts each provider's files into .terraform/providers/<registry>/<namespace>/<name>/<version>/<platform>/ and recomputes the h1: hash in a copy of the lock file it writes inside .terraform/. That rewrite is what lets terraform init -get=false accept the Bazel-installed binaries.

Regenerating. Wire up a terraform_providers_lock target (or opentofu_providers_lock for the OpenTofu variant):

load(
    "@rules_terraform//terraform:terraform_modules_lock.bzl",
    "terraform_providers_lock",
)

terraform_providers_lock(
    name = "providers_lock",
    output = ".terraform.lock.hcl",
    target = ":my_module",
    # Optional; defaults to a 5-platform set (linux/darwin ×
    # amd64/arm64 + windows_amd64).
    # platforms = ["linux_amd64", "darwin_arm64"],
)

bazel run //:providers_lock seeds a temp directory with your .tf sources, runs real terraform providers lock -platform=<all> under the toolchain-fetched engine binary, and writes the multi-platform result back into $BUILD_WORKSPACE_DIRECTORY. This is the only path that produces hashes for platforms Bazel didn't resolve for the current build — plain terraform init on your laptop locks the current platform only.

Drift check. terraform_providers_lock_test fails if any provider declared in a required_providers { … } block isn't in the lock file (or vice-versa). Pure text comparison — no network. terraform_lock_diff_test goes further: it structurally compares the init-aspect-generated .terraform.lock.hcl against a checked-in golden produced by real terraform init, catching cases where the aspect's rewrite doesn't match what the engine would produce.

External registry modules

There is no lock file for registry modules — the extension resolves them live against the Registry API on every fresh evaluation. Terraform users load the extension from the terraform side:

terraform = use_extension("@rules_terraform//terraform:extensions.bzl", "terraform")
terraform.modules(
    name = "my_modules",
    root = "//path/to:main.tf",   # any .tf file in the root module directory
)
use_repo(terraform, "my_modules")

OpenTofu users load the parallel extension — same tag class shape, but the registry defaults to registry.opentofu.org:

opentofu = use_extension("@rules_terraform//opentofu:extensions.bzl", "opentofu")
opentofu.modules(
    name = "my_modules",
    root = "//path/to:main.tf",
)
use_repo(opentofu, "my_modules")

At extension eval time the impl reads every *.tf file in the same directory as root (matching Terraform's own "root module is a directory" model), parses module { source = "…" } blocks, and for every registry-shaped source:

  1. Hits /v1/modules/<ns>/<name>/<provider>/versions and picks the highest version satisfying the block's version constraint via a Starlark port of Terraform's semver logic.
  2. Hits /v1/modules/<ns>/<name>/<provider>/<version> for metadata — reads source (git URL) and tag, constructs a direct GitHub tarball URL (<source>/archive/refs/tags/<tag>.tar.gz) with the matching strip_prefix.
  3. Downloads the archive to compute the sha256.
  4. Registers an http_archive per module.

Every downstream fetch goes direct to GitHub — no registry hop at build time. Only GitHub-backed modules are supported; non-GitHub git hosts fail with a clear error at eval time.

At build time the init aspect copies each module's files into .terraform/modules/<key>/ and adds a corresponding entry to Terraform's modules.json manifest.

Reproducibility and caching. The extension is reproducible = False and every .tf edit in the root module directory invalidates it (bzlmod tracks each module_ctx.read()). Re-eval cost is cached across runs via module_ctx.facts — but that cache only persists when MODULE.bazel.lock is enabled. See Reproducibility for the full recipe and the Implementation detail: network calls per eval note below.

Non-default registries. Pass registry = "…" on the tag class if you're pointing at a private mirror or an alternate registry (e.g. a Terraform user consuming the OpenTofu registry, or vice-versa).

Implementation detail: network calls per eval

For each module { } block, the extension issues up to three Registry API requests, cached across evals via module_ctx.facts:

CallPurposeCostSkipped when
GET /v1/modules/<source>/versionsResolve version constraint to a concrete SemVerSmall JSONversion is already an exact-pinned SemVer
GET /v1/modules/<source>/<version>Read source (git URL) + tag to build the archive URLSmall JSONFacts cache has an entry for <source>@<version>
GET <archive URL>Stream the archive body to compute an sha256 integrityMB per moduleFacts cache has an entry for <source>@<version>

Facts is keyed by <source>@<concrete_version> and stores {url, strip_prefix, integrity} — historical facts that don't change once a module version is published. Only entries for currently-declared modules carry across evals; entries for removed modules prune naturally.

Local / in-repo modules

Modules living inside your monorepo don't need a lock file — Bazel's own target graph is the source of truth.

terraform_module(
    name = "root",
    srcs = glob(["*.tf"]),
    deps = ["//modules/greeter"],
    module_sources = {
        # Terraform source path (as written in `module "…" { source = "…" }`)
        # → Bazel target the init aspect should materialize into
        # `.terraform/modules/<key>/`.
        "./modules/greeter": "//modules/greeter",
    },
)

The module_sources map translates each module "greeter" { source = "./modules/greeter" } block into a Bazel-managed file copy. The sub-module can live in an entirely different package — no filesystem-sibling relationship is required, because the aspect copies files into .terraform/modules/… based on the label, not on the original layout.

For modules that genuinely sit alongside the parent's .tf files (i.e. source = "./foo" where ./foo really is a subdirectory of the parent's own srcs), no module_sources entry is needed — the aspect discovers the block and includes those files automatically.

Update workflow — one-page summary

ChangeWhat to do
Bump / add a providerUpdate required_providers { }; regenerate .terraform.lock.hcl (real terraform providers lock or bazel run //:providers_lock)
Bump / add a registry moduleEdit the module { source = "…" version = "…" } block. Live-resolved on the next bazel fetch; regenerate MODULE.bazel.lock if you have it enabled.
Add / rename a local moduleUpdate deps and module_sources on the parent; no lock file involved
Verify everythingbazel test //... — runs validate, fmt, tftest, and every lock drift check in one shot

Reproducibility

The state of each bzlmod extension in rules_terraform:

ExtensionReproducible?Why
terraform_toolchains / opentofu_toolchainsEvery URL + integrity is vendored in //<engine>/private:versions.bzl. No network at eval time; identical inputs → identical repo declarations.
terraform.providers(...)Reads a checked-in .terraform.lock.hcl; never resolves version constraints. Registry API is called to look up download URLs, but the version and hash come from the lock.
terraform.modules(...) / opentofu.modules(...)Live version-constraint resolution against the Registry API. MODULE.bazel.lock is the capture layer that closes the gap — see below.

The role of MODULE.bazel.lock

The modules extension is the only piece of rules_terraform that depends on MODULE.bazel.lock for both reproducibility AND performance. terraform_toolchains, opentofu_toolchains, and terraform.providers(...) all read fully vendored state and produce deterministic output without any lockfile involvement. The modules extension is different in two related ways:

  1. Reproducibility. Without the lockfile, ~> 5.0 can resolve to 5.7.1 today and 5.7.2 tomorrow. With the lockfile enabled, bzlmod captures the extension's resolved output and reuses it on every subsequent eval — the extension is reproducible = False under Bazel's contract, but the lockfile makes builds byte-identical across time in practice.

  2. Performance. Every .tf edit invalidates the extension (bzlmod tracks each module_ctx.read()). Without any cache, every re-eval hits the Registry API three times per module block, including downloading each archive to compute an sha256 — MB per module. The extension caches per-(source, version) archive URL + integrity in module_ctx.facts; those facts persist through MODULE.bazel.lock. Without the lockfile, facts is always empty, and every re-eval pays the full cost.

Recommended downstream setup — in your consuming repo's .bazelrc:

common --lockfile_mode=update
# or, once you're confident in the state:
common --lockfile_mode=strict

Commit MODULE.bazel.lock. This gives you:

  • Deterministic builds regardless of version constraints in .tf.
  • Cheap re-evals — cached facts short-circuit metadata calls and archive downloads on every re-run.
  • A PR-visible diff (MODULE.bazel.lock) whenever a module version or archive hash actually changes.

Bonus: pin exact versions in module { } blocks. Combined with MODULE.bazel.lock, exact pins let the extension skip even the /versions API call — bringing warm-cache re-evals to zero network calls per module:

module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "5.7.1"   # not "~> 5.0"
}

Rules_terraform's own .bazelrc does NOT enable the lockfile

The ruleset ships with common --lockfile_mode=off. This is deliberate for a rules repo: bumping any dev-only bazel_dep version would produce lockfile churn in unrelated PRs. Downstream users are recommended to enable it in their own repos.

Update workflow

To bump a module version with the lockfile enabled:

  1. Edit the exact version (or constraint) in the module { } block.
  2. Run bazel mod deps --lockfile_mode=update (or just bazel fetch //...) to regenerate MODULE.bazel.lock.
  3. Commit both the .tf change AND the lockfile diff — reviewers see exactly which archive changed.

Why not just mark the extension reproducible = True?

The extension makes live Registry API calls at eval time. Network responses CAN vary across invocations (transient 5xx, mirror rotation, registry outages, schema changes over years). Claiming reproducible = True would be dishonest — same inputs, potentially different outputs. MODULE.bazel.lock is Bazel's designed capture layer for exactly this shape of extension, and setting reproducible = True would tell bzlmod NOT to record the extension in the lockfile — the opposite of what we want.

When could reproducible = True come back?

  • If HashiCorp publishes a Terraform-level module lockfile schema that captures resolved versions + hashes ahead of extension eval.
  • If rules_terraform introduces a mode where the extension consumes a user-supplied pinned-version list without hitting the network at all (essentially reintroducing a lockfile — the one we deliberately removed in favor of live resolution + facts caching).

Neither is planned. Until then, MODULE.bazel.lock + exact pins covers the same ground with Bazel's native machinery.

Terraform Rules

Public rules

Module extensions

terraform_binary

Rules

terraform_binary

load("@rules_terraform//terraform:terraform_binary.bzl", "terraform_binary")

terraform_binary(name, lock, root)

Wraps the terraform binary for a terraform_module root. Invoke via bazel run — any subcommand and args are passed straight through, so bazel run //x:terraform -- plan, ... -- apply, ... -- state list, etc. mirror the nominal CLI.

The .terraform directory is assembled hermetically by terraform_init_aspect; providers and modules come from Bazel-managed dependencies rather than the network.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
lockAn optional .terraform.lock.hcl file.LabeloptionalNone
rootThe terraform_module target that serves as the root module.Labelrequired

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)

A rule for running terraform fmt on a Terraform target.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
target-Labelrequired

terraform_module

Rules

terraform_module

load("@rules_terraform//terraform:terraform_module.bzl", "terraform_module")

terraform_module(name, deps, srcs, data, lock, main, module_sources)

Defines a Terraform module that can be used as a dependency in other Terraform targets.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
depsOther terraform_module, terraform_provider, terraform_provider_group, terraform_external_module, or terraform_module_group targets that this module depends on.List of labelsoptional[]
srcsTerraform source files (.tf) that make up this module.List of labelsoptional[]
dataAdditional files or targets that should be available at runtime.List of labelsoptional[]
lockAn optional .terraform.lock.hcl file.LabeloptionalNone
mainAn explicit file to use for the entrypoint of the module. If unspecified, main.tf or the first .tf file will be used.LabeloptionalNone
module_sourcesMapping of Terraform module source paths to Bazel target labels. Use this to map local module "foo" { source = "./modules/vpc" } references to Bazel targets from other packages or external bzlmod dependencies. Keys are the Terraform source paths, values are Bazel labels providing TerraformInfo. The init tool will symlink these at the expected paths.Dictionary: String -> Stringoptional{}

Lock-file rules for .terraform.lock.hcl.

terraform_providers_lock regenerates the multi-platform lock file; terraform_providers_lock_test catches drift against the module's required_providers blocks; terraform_lock_diff_test structurally diffs the init-aspect output against a checked-in golden.

External Terraform registry modules are resolved live by the terraform.modules(...) bzlmod extension — no separate lock file, no updater rule. See docs/src/index.md.

Rules

terraform_lock_diff_test

load("@rules_terraform//terraform:terraform_modules_lock.bzl", "terraform_lock_diff_test")

terraform_lock_diff_test(name, golden, regenerate_hint, target)

Diffs the init-aspect-generated .terraform.lock.hcl for target against a checked-in golden produced by real terraform init / tofu init. Verifies same provider set, same version+constraints per provider, same zh: set, and at least one h1: overlap.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
goldenA .terraform.lock.hcl produced by real terraform/tofu init.Labelrequired
regenerate_hintCommand shown in the failure message telling users how to refresh the golden.Stringoptional""
targetA terraform_module target. The init aspect runs against it to produce the lock under test.Labelrequired

terraform_providers_lock

load("@rules_terraform//terraform:terraform_modules_lock.bzl", "terraform_providers_lock")

terraform_providers_lock(name, output, platforms, target)

Regenerates .terraform.lock.hcl for a terraform_module by running real terraform providers lock -platform=<all> under the toolchain-fetched terraform binary. bazel run writes the multi-platform result back to source — the one path that produces hashes for platforms Bazel didn't resolve for the current build.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
outputWhere to write the multi-platform .terraform.lock.hcl, relative to this BUILD file's package. Diff-test companions should point golden = "<same-path>".Stringrequired
platformsos_arch platforms to record hashes for. Defaults to the common set.List of stringsoptional[]
targetThe terraform_module whose .tf sources drive the lock resolution.Labelrequired

terraform_providers_lock_test

load("@rules_terraform//terraform:terraform_modules_lock.bzl", "terraform_providers_lock_test")

terraform_providers_lock_test(name, lock, target)

Fails the test if .terraform.lock.hcl doesn't cover every provider declared in terraform { required_providers { ... } } blocks reachable from target (and vice versa). Network-free presence check.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
lockThe .terraform.lock.hcl file being verified.Labelrequired
targetThe terraform_module whose transitive sources are being checked.Labelrequired

terraform_provider

Rules

terraform_provider

load("@rules_terraform//terraform:terraform_provider.bzl", "terraform_provider")

terraform_provider(name, files, platform, source, version)

Defines a Terraform provider that can be used as a dependency in terraform_module targets.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
filesThe provider binary files.List of labelsrequired
platformPlatform string (e.g., 'linux_amd64', 'darwin_arm64'). If omitted, auto-detected at build time.Stringoptional""
sourceProvider source (e.g., 'hashicorp/null').Stringrequired
versionProvider version (e.g., '3.2.4').Stringrequired

terraform_provider_group

Rules

terraform_provider_group

load("@rules_terraform//terraform:terraform_provider_group.bzl", "terraform_provider_group")

terraform_provider_group(name, deps, lock)

Defines a group of Terraform providers with their lock file. This ensures all providers are from the same lock file.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
depsList of terraform_provider targets that belong to this group.List of labelsrequired
lockThe .terraform.lock.hcl file for this provider group.Labelrequired

terraform_test

Rules

terraform_test

load("@rules_terraform//terraform:terraform_test.bzl", "terraform_test")

terraform_test(name, lock, root)

Runs terraform's native HCL test framework (terraform test) on the root module as a Bazel test. Test cases live in .tftest.hcl files. Fully hermetic — the .terraform directory is assembled by the init aspect from Bazel-managed deps.

Bazel --test_arg values are intentionally ignored so the surface stays predictable — every run executes the module's .tftest.hcl files.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
lockAn optional .terraform.lock.hcl file.LabeloptionalNone
rootThe terraform_module target that serves as the root module.Labelrequired

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, target)

A rule for running terraform validate on a Terraform target.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
targetThe target to validate.Labelrequired

Terraform Extensions

Two module extensions live in //terraform:extensions.bzl:

terraform_toolchains

No user-facing configuration. On evaluation it materializes one hub repo (@terraform_toolchains) that declares a toolchain per (version, platform), each guarded by a target_settings matching //terraform/settings:version_<v>. rules_terraform's own MODULE.bazel use_repos the hub and register_toolchainss it, so downstream users get toolchain resolution for free — no ceremony beyond flipping //terraform/settings:version to pick which Terraform release to actually download. Per-(version, platform) http_archives are lazy; only the selected combination is fetched.

# In your MODULE.bazel — nothing to do beyond `bazel_dep`:
bazel_dep(name = "rules_terraform", version = "...")

terraform

Carries providers and modules tag classes for users who want to pin providers or registry-modules via Bazel-managed fetching.

terraform = use_extension("@rules_terraform//terraform:extensions.bzl", "terraform")

# One providers hub per `.terraform.lock.hcl` you want to fetch.
terraform.providers(
    name = "my_providers",
    lock = "//path/to/module:.terraform.lock.hcl",
    # registry = "registry.opentofu.org",  # optional; defaults to registry.terraform.io
)

# One modules hub per `terraform_modules.lock.json` (generated by lockgen).
terraform.modules(
    name = "my_modules",
    lock = "//path/to/module:terraform_modules.lock.json",
)

use_repo(terraform, "my_providers", "my_modules")

terraform.providers

AttributeTypeDescription
namestring (required)Hub repository name. Each declared provider lives at @<name>//<namespace>_<name>.
locklabel (required).terraform.lock.hcl file to fetch providers from.
registrystring (default registry.terraform.io)Registry host to query.

terraform.modules

AttributeTypeDescription
namestring (required)Hub repository name. Each fetched module is aggregated under @<name>//:<name>.
locklabel (required)terraform_modules.lock.json file (see terraform_modules_lock.md).

Public build settings for //terraform.

Each exported function is named after a build setting in this package and its docstring describes what the setting does. The function body returns the setting's label so callers can reference it programmatically.

version

Picks which Terraform release the auto-registered toolchains resolve to.

rules_terraform ships one toolchain per (version, platform) combination in TERRAFORM_VERSIONS, each guarded by a config_setting that matches this flag; Bazel's toolchain resolver picks the matching one. The corresponding http_archive is fetched lazily on first use, so declaring dozens of versions does not slow down builds that only use one.

Flip the flag globally in .bazelrc, per-invocation via --@rules_terraform//terraform/settings:version=1.10.5, or per-target with a Starlark configuration transition.

Defaults to the latest stable release listed in //terraform/private:versions.bzl (TERRAFORM_DEFAULT_VERSION), computed by tools/update_versions from the shipped versions table.

OpenTofu Rules

Everything under //opentofu mirrors //terraform but binds to //opentofu:toolchain_type. Rule bodies are identical — only the resolved toolchain differs — so behavior stays in lockstep between the two engines. Data-only rules (opentofu_module, opentofu_provider, opentofu_provider_group) are literal aliases for their terraform_* counterparts, provided so an OpenTofu-only project never has to import a terraform_* symbol.

Public rules

Module extensions

opentofu_binary

Rules

opentofu_binary

load("@rules_terraform//opentofu:opentofu_binary.bzl", "opentofu_binary")

opentofu_binary(name, lock, root)

Wraps the opentofu binary for a terraform_module root. Invoke via bazel run — any subcommand and args are passed straight through, so bazel run //x:opentofu -- plan, ... -- apply, ... -- state list, etc. mirror the nominal CLI.

The .terraform directory is assembled hermetically by terraform_init_aspect; providers and modules come from Bazel-managed dependencies rather than the network.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
lockAn optional .terraform.lock.hcl file.LabeloptionalNone
rootThe terraform_module target that serves as the root module.Labelrequired

opentofu_fmt_test

Rules

Aspects

opentofu_fmt_test

load("@rules_terraform//opentofu:opentofu_fmt_test.bzl", "opentofu_fmt_test")

opentofu_fmt_test(name, target)

A rule for running opentofu fmt on a Terraform target.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
target-Labelrequired

opentofu_fmt_aspect

load("@rules_terraform//opentofu:opentofu_fmt_test.bzl", "opentofu_fmt_aspect")

opentofu_fmt_aspect()

An aspect for running opentofu fmt on targets with Terraform sources.

ASPECT ATTRIBUTES

ATTRIBUTES

opentofu_module — alias for terraform_module.

Same rule under both names: a bundle of .tf files + deps + optional lock is engine-neutral, so opentofu_module is just a naming alias so OpenTofu-only projects don't have to import a terraform_* symbol.

Rules

opentofu_module

load("@rules_terraform//opentofu:opentofu_module.bzl", "opentofu_module")

opentofu_module(name, deps, srcs, data, lock, main, module_sources)

Defines a Terraform module that can be used as a dependency in other Terraform targets.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
depsOther terraform_module, terraform_provider, terraform_provider_group, terraform_external_module, or terraform_module_group targets that this module depends on.List of labelsoptional[]
srcsTerraform source files (.tf) that make up this module.List of labelsoptional[]
dataAdditional files or targets that should be available at runtime.List of labelsoptional[]
lockAn optional .terraform.lock.hcl file.LabeloptionalNone
mainAn explicit file to use for the entrypoint of the module. If unspecified, main.tf or the first .tf file will be used.LabeloptionalNone
module_sourcesMapping of Terraform module source paths to Bazel target labels. Use this to map local module "foo" { source = "./modules/vpc" } references to Bazel targets from other packages or external bzlmod dependencies. Keys are the Terraform source paths, values are Bazel labels providing TerraformInfo. The init tool will symlink these at the expected paths.Dictionary: String -> Stringoptional{}

opentofu_provider — alias for terraform_provider.

Same rule under both names: a checked-in provider binary declaration is engine-neutral, so opentofu_provider is just a naming alias so OpenTofu-only projects don't have to import a terraform_* symbol.

Rules

opentofu_provider

load("@rules_terraform//opentofu:opentofu_provider.bzl", "opentofu_provider")

opentofu_provider(name, files, platform, source, version)

Defines a Terraform provider that can be used as a dependency in terraform_module targets.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
filesThe provider binary files.List of labelsrequired
platformPlatform string (e.g., 'linux_amd64', 'darwin_arm64'). If omitted, auto-detected at build time.Stringoptional""
sourceProvider source (e.g., 'hashicorp/null').Stringrequired
versionProvider version (e.g., '3.2.4').Stringrequired

opentofu_provider_group — alias for terraform_provider_group.

Same rule under both names.

Rules

opentofu_provider_group

load("@rules_terraform//opentofu:opentofu_provider_group.bzl", "opentofu_provider_group")

opentofu_provider_group(name, deps, lock)

Defines a group of Terraform providers with their lock file. This ensures all providers are from the same lock file.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
depsList of terraform_provider targets that belong to this group.List of labelsrequired
lockThe .terraform.lock.hcl file for this provider group.Labelrequired

opentofu_test

Rules

opentofu_test

load("@rules_terraform//opentofu:opentofu_test.bzl", "opentofu_test")

opentofu_test(name, lock, root)

Runs opentofu's native HCL test framework (opentofu test) on the root module as a Bazel test. Test cases live in .tftest.hcl files. Fully hermetic — the .terraform directory is assembled by the init aspect from Bazel-managed deps.

Bazel --test_arg values are intentionally ignored so the surface stays predictable — every run executes the module's .tftest.hcl files.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
lockAn optional .terraform.lock.hcl file.LabeloptionalNone
rootThe terraform_module target that serves as the root module.Labelrequired

opentofu_validate_test

Rules

Aspects

opentofu_validate_test

load("@rules_terraform//opentofu:opentofu_validate_test.bzl", "opentofu_validate_test")

opentofu_validate_test(name, target)

A rule for running opentofu validate on a Terraform target.

ATTRIBUTES

NameDescriptionTypeMandatoryDefault
nameA unique name for this target.Namerequired
targetThe target to validate.Labelrequired

opentofu_validate_aspect

load("@rules_terraform//opentofu:opentofu_validate_test.bzl", "opentofu_validate_aspect")

opentofu_validate_aspect()

An aspect for running opentofu validate on targets with Terraform sources.

ASPECT ATTRIBUTES

ATTRIBUTES

OpenTofu Extensions

One module extension lives in //opentofu:extensions.bzl:

opentofu_toolchains

No user-facing configuration. Mirrors //terraform:extensions.bzl%terraform_toolchains but resolves against //opentofu:toolchain_type. Materializes @tofu_toolchains covering every supported OpenTofu version, each toolchain guarded by a target_settings on //opentofu/settings:version_<v>. rules_terraform's own MODULE.bazel registers the hub so downstream users get toolchain resolution for free.

# In your MODULE.bazel — nothing to do beyond `bazel_dep`:
bazel_dep(name = "rules_terraform", version = "...")

Providers and modules

Provider and external-module fetching is engine-neutral — use //terraform:extensions.bzl%terraform for both engines. If you're targeting OpenTofu, pass registry = "registry.opentofu.org" to terraform.providers (or leave it defaulted to registry.terraform.io, which OpenTofu also serves).

Public build settings for //opentofu.

Each exported function is named after a build setting in this package and its docstring describes what the setting does. The function body returns the setting's label so callers can reference it programmatically.

version

Picks which OpenTofu release the auto-registered toolchains resolve to.

rules_terraform ships one toolchain per (version, platform) combination in TOFU_VERSIONS, each guarded by a config_setting that matches this flag; Bazel's toolchain resolver picks the matching one. The corresponding http_archive is fetched lazily on first use.

Flip the flag globally in .bazelrc, per-invocation via --@rules_terraform//opentofu/settings:version=1.10.6, or per-target with a Starlark configuration transition.

Defaults to the latest stable release listed in //opentofu/private:versions.bzl (TOFU_DEFAULT_VERSION), computed by tools/update_versions from the shipped versions table.