Skip to content
This repository was archived by the owner on Jul 23, 2026. It is now read-only.

Repository files navigation

Warning

This repository is deprecated and archived.

rules_tf_apply has been merged into rillanetwork/rules_tf and is no longer maintained here. All of its rules (tf_root_module, tf_init, tf_plan, tf_apply, tf_destroy, tf_cmd) and tools (list_modules, run) now ship in rules_tf from the v1.0.0 release onward.

To migrate:

  1. Drop the rules_tf_apply bazel_dep (and any archive_override) from MODULE.bazel, and pin rules_tf to v1.0.0 or later.
  2. Replace load("@rules_tf_apply//tf_apply:defs.bzl", ...) with load("@rules_tf//tf_apply:defs.bzl", ...).
  3. Move tool references from @rules_tf_apply//tools:... to @rules_tf//tools:....

See the rules_tf README for installation and rules_tf#11 for the full migration notes.

Rules for applying TF (Terraform / Tofu) modules using Bazel

This module provides rules to initialize, plan, and apply Terraform modules using Bazel. This effectively enables using tools like bazel-diff to selectively apply changes to Terraform modules only when necessary.

Usage

This depends on the rules_tf module, which provides the necessary toolchain and providers for Terraform.

# MODULE.bazel

bazel_dep(name = "rules_tf_apply", version = "0.1.0")
bazel_dep(name = "rules_tf", version = "0.0.10")

tf = use_extension("@rules_tf//tf:extensions.bzl", "tf_repositories", dev_dependency = True)
tf.download(
    mirror = {
        "random": "hashicorp/random:3.3.2",
        "null": "hashicorp/null:3.1.1",
        "aws": "hashicorp/aws:>=5.0.0",
    },
    tfdoc_version = "0.19.0",
    tflint_version = "0.53.0",
    use_tofu = False,
    version = "1.12.2",
)
use_repo(tf, "tf_toolchains")

register_toolchains(
    "@tf_toolchains//:all",
    dev_dependency = True,
)

and on the BUILD.bazel file:

# BUILD.bazel

load("@rules_tf_apply//tf_apply:defs.bzl", "tf_module")

tf_module(
    name = "my_tf_module",
    providers = [
        "aws",
    ],
    providers_versions = ":providers",
    tags = [
        "terraform",
    ],
    deps = [
        "//:my_other_module",
    ],
)

Running bazel run //:my_tf_module.init, plan, destroy, or apply would generate bazel-tf directory at the root of the workspace that must be gitignored. This directory contains the Terraform state and plan files generated by each of the phases.

Destroying a Module

<name>.destroy mirrors .plan — it runs terraform plan -destroy and writes the destroy plan to bazel-tf/<module>/plan.tfplan. The subsequent <name>.apply then consumes that plan exactly like a forward apply, preserving the same review-then-execute workflow:

bazel run //:my_tf_module.init
bazel run //:my_tf_module.destroy   # writes a destroy plan to bazel-tf/<mod>/plan.tfplan
# review the plan output, then:
bazel run //:my_tf_module.apply     # consumes the destroy plan and tears down

.destroy is purely a planning step — nothing is actually destroyed until .apply runs against the destroy plan. Note that .destroy overwrites plan.tfplan (same as .plan does), so the last plan written is the one .apply will execute.

Passing Additional Arguments

You can pass additional Terraform arguments using Bazel's -- syntax:

# Target specific resources
bazel run //:my_tf_module.plan -- --target module.web --target module.api

# Apply with additional flags  
bazel run //:my_tf_module.apply -- --target module.database -auto-approve

# Plan with custom options
bazel run //:my_tf_module.plan -- -refresh=false -parallelism=10

Any arguments passed after -- are forwarded directly to the underlying Terraform command.

Running Other Terraform Subcommands

For terraform subcommands without a dedicated target (destroy, state, import, taint, output, refresh, show, validate, ...), tf_root_module exposes a generic <name>.tf target that forwards all arguments to terraform -chdir=<module>:

# Read-only commands
bazel run //:my_tf_module.tf -- validate
bazel run //:my_tf_module.tf -- state list
bazel run //:my_tf_module.tf -- output

# Show a plan that was previously generated
bazel run //:my_tf_module.plan
bazel run //:my_tf_module.tf -- show plan.tfplan

# Import existing infrastructure
bazel run //:my_tf_module.tf -- import aws_iam_role.example example-role

# Destroy (auto-approve is deliberately not implicit — pass it explicitly)
bazel run //:my_tf_module.tf -- destroy -auto-approve -target=null_resource.x

Unlike .apply, the .tf target adds no implicit flags (no -auto-approve, no -input=false, no plan file). The caller is responsible for whatever terraform needs. This keeps destructive operations opt-in rather than baked into the target.

Shipped tools

Two versioned py_binary tools ship with this ruleset so consuming repos share one implementation of terraform module enumeration and fan-out orchestration. A change to affected-detection or the artifact schema lands here once and every consumer inherits it on the next pin bump.

@rules_tf_apply//tools:list_modules

Enumerates tf_root_module targets (via kind(tf_plan, <query_path>)) and emits a JSON matrix describing each one:

BASE_REF=origin/main bazel run @rules_tf_apply//tools:list_modules -- //terraform/...

Each row: {"package", "module_package", "name", "skip", "affected"}.

  • skip is true when the module's tf_plan target is tagged deploy:manual.
  • affected is true when the module's transitive bazel deps include a file changed between BASE_REF (env) and HEAD. With BASE_REF unset, every module is affected (the full list).
  • module_package is where the rendered terraform working dir lives — equal to package unless the root points module = at a shared module elsewhere.

This output is cloud-neutral by design: it carries module identity plus the skip/affected classification and nothing tenant-specific. A consumer that keys CI off deployment topology (an account per module, say) decorates these rows with its own fields from its own path convention — the ruleset does not own any tenant's cloud/account layout.

@rules_tf_apply//tools:run

Fans out init/plan/apply over the modules under a query, in-process:

bazel run @rules_tf_apply//tools:run -- //terraform/... init plan \
  [--extra_var_file vars.tfvars] [--plan_artifacts_dir out/]

With --plan_artifacts_dir (and plan among the actions) it copies each module's plan.tfplan.json to <package>--<name>.json, writes an error envelope for failed plans, and emits a modules.json matrix — best-effort reporting that never fails the run.

About

Bazel module with rules to initialize, plan, and apply Terraform modules

Topics

Resources

Stars

2 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages