This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Rules Terraform

Bazel rules and pinned provider installation for Terraform

This standalone module owns reusable Terraform rules. Terraform and provider archives are declared Bazel inputs. Provider downloads belong to the Bzlmod extension; Terraform uses a filesystem mirror containing only the providers selected by its target.

The extension resolves one version per provider source. Consumers select a provider by Bazel label. Explicit version override policy can be added at this resolution boundary later; conflicting versions currently fail resolution.

Repository authentication and backend injection are supplied by the caller. The module does not depend on the enclosing monorepo’s AL or Vault packages.

Use terraform_providers from @rules_terraform//:extensions.bzl. Each archive tag requires a unique repository name, canonical lowercase source (hostname/namespace/type), exact version, Terraform platform (os_arch), immutable HTTPS urls, and SHA256 SRI integrity. Verify the integrity against the publisher’s release checksums before declaring it.

Import each generated repository with use_repo. Its public :provider target exposes TerraformProviderInfo. Different platforms can share one provider version; duplicate archives or conflicting versions fail before repositories are registered. Provider downloads use Bazel’s verified downloader and repository cache, without executing host tools or querying a registry.

This repository’s concrete declarations live in the Terraform dependency package. The module is currently developed with a local Bzlmod override; no registry release is implied.

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

terraform_binary(
    name = "tf_plan",
    srcs = glob(["*.tf"]),
    data = ["//modules/example:source"],
    providers = ["@my_provider_linux_amd64//:provider"],
    arguments = ["plan"],
)

terraform_test(
    name = "tf_fmt_test",
    srcs = glob(["*.tf"]),
    arguments = ["--direct", "fmt", "-check", "-recursive"],
)

arguments are fixed runner/Terraform arguments; command-line arguments are appended. srcs and data declare configuration, local child modules, and files read by the configuration. chdir defaults to the target’s package. The rules package ZIPs at HOST/NAMESPACE/TYPE/terraform-provider-TYPE_VERSION_OS_ARCH.zip beneath a target-specific runfiles mirror, following Terraform’s filesystem mirror contract.

The same terraform/defs.bzl exports terraform_binary_map, terraform_target_binary_map, and terraform_test_map. They preserve explicit operation names such as tf.plan, tf.apply, and tf_tests.fmt_test. Targeted maps produce plan/show/apply commands; their apply command requires one saved plan. No unnamed apply alias is generated.

Maps run the Terraform rules directly by default. Callers can supply a wrapper macro and its wrapper_kwargs to compose authentication or other command setup without adding that framework to rules_terraform. The wrapper receives the final target name, the inner executable in args, its runfiles in data, and common target attributes. wrapper_kwargs cannot replace those arguments or duplicate common attributes. Wrapped tests require a test wrapper.

Repository consumers load AL’s generic al_binary_run or al_binary_run_test and pass their configs and run_args in wrapper_kwargs. Their plugin binaries remain in data. Provider-free tests can use the Terraform test maps directly. Repository root command bindings live with the shared AL configuration.

The default Terraform executable is the pinned toolchain in terraform/binary_toolchain.json, acquired through rules_binary_toolchain. The initial supported executable platform is Linux amd64. The optional terraform executable attribute permits a caller-supplied Bazel tool. @rules_terraform//terraform:cli exposes the pinned CLI for formatting and other consumers that do not initialize providers.

Initialization and generated locks remain in runtime workspaces. No source .terraform.lock.hcl is required. The launcher contract describes working directories, direct commands, saved plans, and manifest-only execution. Provider and backend operations can still use the network when the operator invokes them; provider acquisition itself has no runtime registry fallback. Configuration modules must be declared local inputs.

1 -

terraform-execution Specification

Provide reusable Bazel rules that acquire pinned Terraform providers and run Terraform with declared configuration, executable, and provider inputs.

The provider module extension SHALL download immutable HTTPS archives with mandatory SHA256 integrity through Bazel repository fetching. It SHALL select one version for each canonical provider source across the extension graph.

  • WHEN declarations select different versions of the same provider source
  • THEN extension resolution fails before provider repositories are created.
  • WHEN a provider archive is fetched
  • THEN Bazel verifies the declared integrity and exposes its canonical source, version, platform, and packed filesystem-mirror path.

Terraform rules SHALL place selected provider archives in runfiles using the packed filesystem-mirror layout. The runner SHALL configure only that mirror for provider installation and SHALL preserve the declared Terraform executable.

  • WHEN all required providers are declared by a target
  • THEN initialization and provider schema validation succeed without registry access or a host provider cache.
  • WHEN configuration requires a provider absent from the target’s mirror
  • THEN initialization fails without falling back to a registry download.

The reusable module SHALL own Terraform execution and named command maps while remaining independent of parent repository labels. Maps SHALL support optional caller-supplied command wrappers. Repository consumers SHALL select generic AL wrappers explicitly to preserve configuration, plugin lifecycle, backend injection, named operations, and the saved-plan apply guard. Shared provider pins SHALL be owned by third_party/terraform.

  • WHEN a targeted apply wrapper receives a missing plan or extra arguments
  • THEN the runner rejects it before Terraform initialization.
  • WHEN repository Terraform commands and tests are analyzed
  • THEN they load maps from rules_terraform, select shared provider labels, and require neither tools/terraform nor a checked-in .terraform.lock.hcl file.
  • WHEN a command map supplies a wrapper
  • THEN the wrapper receives the declared Terraform invocation and runfiles without requiring a framework dependency in the reusable module.
  • WHEN a test map needs no external command setup
  • THEN it runs the Terraform test rule without an AL dependency.

2 - Terraform launcher

Execute Terraform with providers declared by Bazel

This implementation launcher reads the generated target sidecar and resolves Terraform, configuration inputs, and provider archives through Bazel runfiles. Consumer targets use the public Terraform rules instead of invoking this binary directly.

The launcher selects the target’s working directory inside the runfiles tree. Source files can remain symlinks, so formatting retains its existing source behavior. Initialization writes lockfiles and Terraform metadata in that runfiles directory. Saved plans and local state retain their normal paths relative to the working directory; the launcher never removes them.

Each invocation uses a private CLI configuration containing only the declared filesystem provider mirror. It overrides user CLI configuration, removes inherited provider-cache settings, disables checkpoint checks, and rejects injected CLI arguments, provider reattachment, and provider-download commands. Missing provider versions and platforms fail without a registry installation fallback. Temporary CLI configuration and any materialized provider mirror are removed on exit.

Before commands that can load providers, the launcher reads Terraform’s lock selections through version -json and checks every selected installed package against its declared archive. Source addresses, versions, file paths, and file contents must match; undeclared selections and extra, missing, modified, or linked package files fail before the requested command starts. The check honors TF_DATA_DIR, including paths relative to Terraform’s working directory. It also runs after explicit initialization. This adds local archive decompression and hashing work; it does not initialize the backend or contact a registry. The working directory must not be modified concurrently during execution.

Initialization runs before the requested command unless --direct precedes it. Its standard output goes to standard error, preserving structured command output. AL_TF_BACKEND_CONFIG* environment values remain literal initialization arguments; other provider and backend environment, including TF_HTTP_*, is preserved. Terraform process exit codes are returned unchanged.

Configuration supports declared local modules. Provider installation is isolated; the launcher does not inspect module source expressions or sandbox Terraform’s service connections.

--require-saved-plan accepts only apply <saved-plan-file>. The file must exist and be regular; relative paths resolve against the selected working directory. Argument and environment validation happens before initialization.

Manifest-only tests materialize declared configuration inputs beneath TEST_TMPDIR, retaining their workspace until Bazel removes that test directory. Outside tests, a directory runfiles tree is required to preserve persistent working-directory semantics. An explicit --chdir before the Terraform command selects a caller-owned directory when that behavior is intended.

Invocation scratch uses TEST_TMPDIR, then an explicitly configured TMPDIR, then BUILD_WORKSPACE_DIRECTORY/out/rules_terraform/runtime. It never falls back to the system temporary directory.