| name | mise-cookbook-terraform |
| description | End-to-end recipe for a Terraform (or OpenTofu) IaC project managed by mise — terraform + tflint + terraform-docs + tfsec + aws-cli, with init/plan/apply/docs/lint tasks. Complete mise.toml template, first-run walkthrough, and common gotchas. Use when starting a new IaC project or adding mise to an existing one. |
Cookbook — Terraform / OpenTofu IaC project
A complete starting point for a Terraform (or OpenTofu) project using mise. All the common auxiliary tools — tflint, terraform-docs, tfsec, aws-cli — are pinned via aqua backends and wired into mise tasks.
Who this is for
- Starting a new IaC project.
- Standardizing tooling across a team so everyone's local
terraform is the same version.
- Running the same tools in dev, CI, and prod without custom installers.
Who this isn't for
- Pulumi / CDK users — the pattern partially transfers (pin language runtime + aws-cli) but the recipe here is Terraform-specific.
- Pure CloudFormation users — stick with aws-cli alone.
Terraform vs OpenTofu
This cookbook uses terraform. To switch to OpenTofu, replace terraform = "1.9.8" with opentofu = "1.8.4" and every task command from terraform to tofu. State files are compatible up through Terraform 1.5.x; newer features may diverge. See mise-migrate-from-tfenv for the full discussion.
The mise.toml
min_version = '2026.4.0'
[tools]
terraform = "1.9.8"
"aqua:terraform-linters/tflint" = "latest"
"aqua:terraform-docs/terraform-docs" = "latest"
"aqua:aquasecurity/tfsec" = "latest"
"aqua:gruntwork-io/terragrunt" = "latest"
"aqua:aws/aws-cli" = "latest"
[env]
TF_PLUGIN_CACHE_DIR = "{{env.HOME}}/.cache/terraform-plugin-cache"
TF_INPUT = "0"
TF_WORKSPACE = "dev"
AWS_PROFILE = { required = "Set AWS_PROFILE in your shell (or use aws-vault exec)" }
AWS_REGION = "us-east-1"
[redactions]
patterns = ["AWS_*", "*_SECRET", "*_TOKEN", "*_KEY"]
[tasks.init]
description = "Terraform init with plugin cache"
run = "terraform init -upgrade=false"
sources = ["*.tf", "versions.tf", ".terraform.lock.hcl"]
outputs = [".terraform/"]
[tasks.fmt]
description = "Format all .tf files"
run = "terraform fmt -recursive"
[tasks."fmt:check"]
description = "Check formatting without modifying (for CI)"
run = "terraform fmt -recursive -check"
[tasks.validate]
description = "Validate configuration"
depends = ["init"]
run = "terraform validate"
[tasks.plan]
description = "Plan changes (saves to tfplan)"
depends = ["init", "validate"]
run = "terraform plan -out=tfplan"
[tasks.apply]
description = "Apply the saved plan"
run = "terraform apply tfplan"
[tasks.destroy]
description = "Destroy all managed resources (interactive confirmation)"
depends = ["init"]
run = "terraform destroy"
[tasks.lint]
description = "Run tflint"
depends = ["init"]
run = "tflint --recursive"
[tasks.sec]
description = "Run tfsec security scan"
run = "tfsec ."
[tasks.docs]
description = "Regenerate README.md from terraform-docs"
run = "terraform-docs markdown table . > README.md"
[tasks.check]
description = "Full pre-commit check (fmt + validate + lint + sec)"
depends = ["fmt:check", "validate", "lint", "sec"]
run = "echo 'all checks passed'"
What this gives you
- Pinned
terraform — everyone runs the same version; no surprise state-format changes.
- Shared plugin cache —
TF_PLUGIN_CACHE_DIR dedupes provider downloads across projects.
- Non-interactive by default —
TF_INPUT=0 fails fast instead of prompting.
- One-stop
mise run check — formats, validates, lints, security-scans — the same set of checks local and in CI.
terraform-docs auto-README — mise run docs regenerates the README's variables/outputs tables.
- Redacted AWS vars — keeps credentials out of
mise env output.
First-run walkthrough
git clone <repo> infra && cd infra
mise trust
mise install
export AWS_PROFILE=myprofile
mise run init
mise run plan
mise run apply
Suggested project layout
infra/
├── mise.toml
├── README.md # auto-generated by terraform-docs
├── .gitignore # includes .terraform, *.tfstate*, tfplan, .terraform.lock.hcl? (debated)
├── .tflint.hcl
├── versions.tf # required_providers + required_version
├── providers.tf
├── main.tf
├── variables.tf
├── outputs.tf
├── modules/
│ ├── vpc/
│ ├── ecs/
│ └── rds/
└── envs/
├── dev/
│ └── terragrunt.hcl # if using terragrunt
└── prod/
└── terragrunt.hcl
.gitignore for a typical IaC project:
.terraform/
*.tfstate
*.tfstate.*
*.tfstate.backup
crash.log
tfplan
*.auto.tfvars
.terraform.tfstate.lock.info
Whether to commit .terraform.lock.hcl is a team decision — most teams should commit it to pin provider versions. Only skip if you're a library/module shop.
CI snippet (GitHub Actions)
name: CI
on: [push, pull_request]
permissions:
id-token: write
contents: read
jobs:
check:
runs-on: ubuntu-latest
env:
AWS_REGION: us-east-1
steps:
- uses: actions/checkout@v4
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions
aws-region: us-east-1
- uses: jdx/mise-action@v3
with: { install: true, cache: true }
- run: mise run "fmt:check"
- run: mise run init
- run: mise run validate
- run: mise run lint
- run: mise run sec
- run: mise run plan
Common gotchas
terraform init is slow on every fresh checkout → TF_PLUGIN_CACHE_DIR fixes this. The cache is ~GB across projects but reuses provider binaries.
AWS_PROFILE unset → SessionStart will warn. Use aws-vault exec <profile> to wrap commands with temporary creds instead of exporting permanent ones.
- tflint doesn't know about AWS providers → Add a
.tflint.hcl with plugin "aws" { enabled = true; version = "…" }. tflint --init installs the plugin.
- tfsec reports false positives → Add
.tfsec/exclude.yml to suppress specific rules with justification comments. Never blanket-disable.
terraform-docs overwrites hand-written README → Put the auto-generated section between <!-- BEGIN_TF_DOCS --> / <!-- END_TF_DOCS --> markers; terraform-docs respects them.
- State file in git → NEVER commit. Use S3/GCS backend. Add
*.tfstate* to .gitignore before first commit.
- Provider version drift between team members → Commit
.terraform.lock.hcl. It's the provider equivalent of package-lock.json.
- Terraform vs OpenTofu confusion in a team → Pick one. Document in the README. Don't mix.
See also
mise-migrate-from-tfenv — moving off tfenv.
mise-backends-overview — aqua backends for tflint/tfsec/etc.
mise-ci-github-actions — mise-action + AWS OIDC.
mise-trust-and-security — why AWS_* should never be plaintext in config files.
- Terraform docs:
developer.hashicorp.com/terraform.
- OpenTofu docs:
opentofu.org.