Terraform/OpenTofu lifecycle CLI with retry, run-directory orchestration, and CI scaffold
Project description
tfdo
The outer layer Terraform and OpenTofu are missing.
tfdo replaces the shell scripts, retry hacks, and copy-pasted CI workflows that every Terraform repo accumulates with a single CLI that handles init reliability, plan readability, run-directory orchestration, and CI scaffolding out of the box.
Project status
tfdo is in early development. Interfaces and behavior can change without notice. Plan rendering, apply, and rendering across multiple run directories (multi run-dir) are work in progress (WIP), along with other gaps the docs may not call out yet. Verify behavior on your stacks before you rely on it in production pipelines.
Full reference docs: espenalbert.github.io/tfdo.
Why tfdo
Pick any Terraform repo that's grown past one run directory. You'll find the same patterns being rewritten by hand:
- Flaky
init: Registry timeouts and plugin download races fail CI. "Just re-run" becomes the team policy. - 200-line plan dumps: Three resources change, buried in 247 lines of refresh output and
(known after apply). Nobody reads it. - Shell-script CI: A 180-line
run-terraform.shper repo, copy-pasted between teams, nobody wants to touch. - Blind provider upgrades: Bump
v5.xtov6.x, runplan, discover 12 breakages. - The 50-module PR: One-line change to a shared variable triggers
planin every run directory. 22-minute CI feedback.
tfdo ships solutions for the entire list as commands. None of them require restructuring your repo. cd into any run directory and terraform plan still works.
What tfdo owns
- Invocation reliability: Retry on transient
initerrors, sharedTF_PLUGIN_CACHE_DIRacross repos,--init-mode auto|always|never. - Run-directory orchestration: Multi-directory plan/apply with selectors (
--env,--team,--app,--tags), parallel waves, dependency DAG, and git-diff-based--changed. - Quality checks:
tfdo check --fixrunsfmt + validate + tflintruff-style and rewrites in place. - Schema intelligence:
tfdo schema diffandtfdo inspect resource-usagewalk cached provider schemas to flag breaking changes and unused attributes beforeplan. - CI scaffolding:
tfdo sync github --oidcprovisions the workflows, env secrets, AWS OIDC provider, and per-env IAM roles end to end. - Repo bootstrap:
tfdo boottakes an empty folder to a workingtfdo.yaml, backend, and module cache.
Terraform and OpenTofu still own everything they always did: graph execution, apply semantics, state, locking, and the provider protocol. tfdo wraps the binary; it never forks it.
Install
tfdo requires Python 3.13+ and a terraform (or tofu) binary on PATH.
uv tool install git+https://github.com/EspenAlbert/tfdo.git
# or, with pipx
pipx install git+https://github.com/EspenAlbert/tfdo.git
Use a specific binary or version (via mise):
tfdo --binary tofu plan
tfdo --tf-version 1.9.0 plan # rewrites to `mise x terraform@1.9.0 -- terraform`
Quickstart
From an empty directory to a CI-wired repo:
tfdo boot # backend + providers + tfdo.yaml + module cache
tfdo new run-dir # questionnaire-driven stack (e.g. envs/dev/project)
tfdo check --fix # fmt + validate + tflint across all run-dirs
tfdo sync justfile # repo-level just targets per env and run-dir
tfdo sync github --oidc # workflows, env secrets, IAM roles
After this you have:
tfdo.yamlat the repo root, env layer, and run-dir layer.envs/{env}/{run_dir}/with backend, providers, and module calls.- A
justfilewhose targets match the discovered tree. - GitHub Actions workflows per env, with OIDC trust roles so Actions reaches state without long-lived AWS keys.
Concepts
tfdo uses vocabulary that applies to any Terraform orchestrator, not just this one. The short version:
- Run directory: Any directory containing a
backend {}block. One backend equals one plan/apply scope. The atomic unittfdooperates on. - Module source: Reusable
.tfcode with no backend. Consumed by run directories viamodule {}blocks, never planned directly. - Lifecycle:
init,plan,apply,destroy.tfdoruns them as a sequence with auto-init and retry. - Selectors: Named dimensions (
env,team,app) plus free-form--tags key=value. Multiple--tagsflags AND, comma-separated values OR within a key. - Dependencies: A run directory lists its parents in
tfdo.yaml.tfdobuilds a DAG and runs in order, passing outputs through var-files. - Change detection:
tfdo run plan --changedusesgit diffto plan only the run directories affected by the current branch. - Config resolution: CLI flag, then env var (
TFDO_*), then nearesttfdo.yaml, then ancestortfdo.yamls, then user config, then default.tfdo infoprints what won.
Daily commands
Single run directory (cd into it, or pass --work-dir):
tfdo init: Retries on transient registry and network errors. InjectsTF_PLUGIN_CACHE_DIR.tfdo plan [-f vars.tfvars] [--json -o plan.json]: Wrapsterraform planwith var-file and JSON helpers.tfdo apply [--auto-approve]: Standard apply. With--init-mode auto, runsinitfirst when terraform reports an init-required error.tfdo destroy [--auto-approve]: Standard destroy.tfdo check [--fix] [--tflint]: ruff-stylefmt + validate(plus optional tflint).--fixrewrites files.tfdo info: Prints resolved settings, paths, and user config.
Across many run directories (tfdo run group):
tfdo run plan --env dev: Plan every run directory underenvs/dev/.tfdo run apply --tags team=infra --parallel 5: Tag-filtered apply, up to 5 concurrent.tfdo run plan --changed: Only run directories touched bygit diffvsHEAD.tfdo run plan --dry-run: Print the wave plan without running terraform.tfdo run apply --on-failure continue: Keep going past a failed run directory.
Other groups:
tfdo config init|show: Generate or print resolvedtfdo.yamllayers.tfdo new run-dir: Scaffold new stacks against modules selected duringboot.tfdo copy env: Copy a known-good env (e.g.dev) into a new env (e.g.prod).tfdo schema show|diff: Fetch provider schemas; diff between two versions or the localdevplugin.tfdo inspect resource-usage|hcl-paths|api-coverage: Walk HCL against provider schemas for coverage and gap reports.tfdo sync justfile|github: Regenerate repo glue when run directories change.
Configuration
tfdo.yaml stacks from the git root down to each run directory. Layers higher in the tree provide defaults; lower layers override.
# repo-root tfdo.yaml
backend:
type: s3
bucket: my-tf-state
key: "envs/{env}/{run_dir}/terraform.tfstate"
region: eu-west-1
dynamodb_table: my-tf-lock
run_dir_discovery: "envs/{env}/{run_dir}"
tags:
managed_by: tfdo
tags_inject: aws # rewrite aws_* resources to carry these tags
providers:
- name: mongodbatlas
constraint: ">= 1.20.0"
ci:
repo_org: EspenAlbert
repo_name: my-atlas-infra
oidc: true
Key concepts:
- Discovery pattern:
run_dir_discoveryis a path with named selectors. The first selector must be{env}. Selectors auto-populate CLI filters (--env,--app,--team). - Backend:
s3orlocal, defined once at the root and rendered into each run directory'sbackend "s3" {}block. - Layered overrides: An env-level
tfdo.yamlcan overridebinary,tf_version,tags, or pin different provider versions per env. - Var-file resolution:
var_filesandenv_var_filesresolve relative to each layer, so dev and prod can sharecommon.tfvarswhile overriding specific knobs. - Hooks:
hook_configsruns a shell command or Python entry-point on lifecycle events (pre_init,pre_plan,post_apply, ...). Input and output flow through env vars and JSON files. - Dependencies:
dependencies: [{ref: ../project}]pulls outputs from another run directory as.dep.tfvars.jsonso ordering is explicit.
tfdo config show prints the resolved layers for the current run directory.
How it differs from Terragrunt, Terramate, and Atmos
- vs Terragrunt: Same wrapper idea, flat YAML instead of HCL include chains. No
.terragrunt-cachetemp dirs; tfdo runs in place. Auto-init is explicit through--init-mode, not magic. - vs Terramate: Borrows the change-detection idea via
git diff, stays closer to the terraform CLI.tfdo run --changed planinstead ofterramate run -- terraform plan. No.tm.hclfiles, no stack UUIDs. - vs Atmos: YAML config too, no component/stack abstraction. Run directories map directly to root module directories. No mandatory Spacelift integration.
- Unique territory: Schema diffing, resource-usage analysis, and provider breaking-change detection. None of the orchestrators above inspect provider schemas.
Design principles
- Wrap, never fork:
tfdocallsterraformortofu(--binary,TFDO_BINARY). Any run directory still works with plainterraform plan. - One effective environment per invocation: A single run uses one coherent set of env vars. Multi-step orchestration spawns separate invocations when credentials differ.
- Committed Terraform preferred: Generated
.tflives in git, reviewable in PRs. The--debugflag (andtfdo config show) surfaces what was generated. - Config simplicity over power:
tfdo.yamlis flat YAML with pydantic validation. No custom functions, no include chains, no expression evaluation at config-load time.
Environment variables
All env vars use the TFDO_ prefix and override CLI flags only when the flag is not passed.
TFDO_BINARY: Terraform binary name or path. Defaultterraform.TFDO_TF_VERSION: When set, binary becomesmise x terraform@{version} -- {binary}.TFDO_WORK_DIR: Working directory for terraform commands. Defaultcwd.TFDO_INTERACTIVE:auto(detect TTY),always, ornever. Withnever, prompting commands require--auto-approve.TFDO_INIT_MODE:auto(init on init-error),always, ornever. Defaultauto.TFDO_TFLINT: Run tflint alongsidecheck.TFDO_VERBOSE_SHELL: Log every successful shell completion (default is errors only).TFDO_BACKENDS_DIRS,TFDO_ENV_VARS_DIRS,TFDO_PROVIDER_HINTS_PATH: Override the static directories shipped withtfdo.CACHE_DIR: Override the per-user cache base; bypasses platformdirs. See caching.
tfdo info prints the resolved values for the current shell.
Development
From the repo root:
just pre-commit # fmt + fix + lint
just pre-push # lint + fmt-check + test + vulture
just test # tests only
just docs-serve # local mkdocs preview
Internal architecture lives in CLAUDE.md. Public API and CLI reference live under docs/ and ship via mkdocs-material.
License
MIT.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tfdo-0.9.2.tar.gz.
File metadata
- Download URL: tfdo-0.9.2.tar.gz
- Upload date:
- Size: 157.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
073acefc8799f40aececc8e30835a6c4a0a6424f83c3dd03e584de6b14773cf7
|
|
| MD5 |
0d5282e67efe009c58b33223c25454d1
|
|
| BLAKE2b-256 |
8109466091523bfc1de182e9aee3ca74aacbbb127de32fe6fc318fc0c9b9bdb8
|
Provenance
The following attestation bundles were made for tfdo-0.9.2.tar.gz:
Publisher:
release.yaml on EspenAlbert/tfdo
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tfdo-0.9.2.tar.gz -
Subject digest:
073acefc8799f40aececc8e30835a6c4a0a6424f83c3dd03e584de6b14773cf7 - Sigstore transparency entry: 1694023320
- Sigstore integration time:
-
Permalink:
EspenAlbert/tfdo@58adbe2c14294083c2f0f11d6b532cc6a9df6b32 -
Branch / Tag:
refs/tags/v0.9.2 - Owner: https://github.com/EspenAlbert
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@58adbe2c14294083c2f0f11d6b532cc6a9df6b32 -
Trigger Event:
push
-
Statement type:
File details
Details for the file tfdo-0.9.2-py3-none-any.whl.
File metadata
- Download URL: tfdo-0.9.2-py3-none-any.whl
- Upload date:
- Size: 225.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
77d68561e477c57bb02d2ef04f9662a67daa8460e232495fb9b6c44ee97314fc
|
|
| MD5 |
2e89014cb2f10fea7c65762f4d3a5e4e
|
|
| BLAKE2b-256 |
a4912a344b1b3910483a61baf12e2e017778faaec56f38125201cd59cd7ebe57
|
Provenance
The following attestation bundles were made for tfdo-0.9.2-py3-none-any.whl:
Publisher:
release.yaml on EspenAlbert/tfdo
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tfdo-0.9.2-py3-none-any.whl -
Subject digest:
77d68561e477c57bb02d2ef04f9662a67daa8460e232495fb9b6c44ee97314fc - Sigstore transparency entry: 1694023331
- Sigstore integration time:
-
Permalink:
EspenAlbert/tfdo@58adbe2c14294083c2f0f11d6b532cc6a9df6b32 -
Branch / Tag:
refs/tags/v0.9.2 - Owner: https://github.com/EspenAlbert
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yaml@58adbe2c14294083c2f0f11d6b532cc6a9df6b32 -
Trigger Event:
push
-
Statement type: