set-up-opentofu
Use when infrastructure should be code — OpenTofu repo layout (one terraform/ root, per-env tfvars and backend files), encrypted remote state with locking, pinned providers, checks in CI, saved plans
SKILL.mdskills/infra/set-up-opentofu/SKILL.md
GitHubSet Up OpenTofu
Infrastructure as reviewed code: one module set, one root with a state and a credential set per environment, state that is encrypted and locked, and an apply that only runs a plan a human approved. Contract: environments.md. Layout and CI follow the fleet standard (VelimirMueller/fleet-standard, rules TF-* and CI-*).
1. Audit current state (change nothing)
cat .claude/stack-profile.md 2>/dev/null # iac, hosting, ci, runtime_manager, task_runner
ls terraform/ terraform/envs terraform/modules infra/ 2>/dev/null; ls *.tf 2>/dev/null
git ls-files | grep -E '\.tfstate|\.tfplan|tfvars' # committed state or plans: stop and fix first
grep -rn --include=*.tf -E '^(terraform|provider) ' . | head -20
ls terraform/.terraform.lock.hcl terraform/.opentofu-version .tflint.hcl 2>/dev/null
tofu version 2>&1 | head -1
From the profile: iac (opentofu or none), hosting (picks the provider), ci (github-actions), runtime_manager (mise, asdf, none). If iac: none and the user did not ask for IaC, stop. If the repo already has Terraform (required_providers with hashicorp source, .terraform.lock.hcl from Terraform), keep its layout and migrate in place (step 2).
2. Decide
- No tofu files → full setup (steps 3 to 7).
- Existing tofu, partial → add only the missing pieces: lock file, encryption, CI, drift, tests.
- Existing per-environment roots (
infra/envs/<env>/) → move them into oneterraform/root withenvs/<env>.tfvarsandenvs/<env>.backend.hcl; keep each state key, so no resource moves. Until the move is done, record aTF-LAYOUT-02exception withexpiresinfleet.yml. - Terraform repo →
tofu initreads it as is; add.terraform.lock.hclfor the registry.opentofu.org addresses withtofu providers lock, then add encryption. Do not rename resources. - Layout, backend, encryption, CI and drift all present → "already in place", run step 7, stop.
- Committed
*.tfstate: remove from history first and rotate every secret it held.
3. Detect the provider
hosting |
Provider | Skill that uses this |
|---|---|---|
hetzner |
hetznercloud/hcloud ~> 1.70 |
deploy-to-hetzner |
ionos |
ionos-cloud/ionoscloud ~> 6.7 |
deploy-to-ionos |
vercel, supabase only |
none required | Skip; configure through deploy-to-vercel. A provider exists for both; add it only when the team wants projects and env vars as code |
4. Install only what is missing
Pin the tool with the profile's runtime manager (mise use opentofu@1.13.1) or the package manager of the machine (brew install opentofu). CI uses opentofu/setup-opentofu. Line and support window: stack-versions.md. Use a supported line (1.12 or 1.13); 1.11 and 1.10 are end of life.
5. Generate
Layout (fleet standard TF-LAYOUT-01, TF-LAYOUT-02, TF-FILE-01):
terraform/
.opentofu-version .terraform.lock.hcl
versions.tf backend.tf providers.tf variables.tf locals.tf main.tf outputs.tf
envs/{dev,stg,prd}.tfvars envs/{dev,stg,prd}.backend.hcl
modules/<name>/{main,variables,outputs,versions}.tf + tests/*.tftest.hcl
.tflint.hcl
Details and every file in opentofu-patterns.md. In short:
versions.tf:required_version = "= 1.13.1", the exact version thatterraform/.opentofu-versionholds (a supported line from step 4), and arequired_providersblock with the fullsource. Without it OpenTofu looks forhashicorp/<name>and fails or, worse, picks a different provider with the same name.backend.tfnames the S3 backend;envs/<env>.backend.hclholds the bucket, one state key per environment,use_lockfile = true. Versioning on the bucket. Create the bucket once by hand (console or CLI); it is the one resource that cannot manage itself.- Encryption:
TF_ENCRYPTIONenvironment variable withpbkdf2key provider andaes_gcm,enforced = truefor state and plan. Passphrase from the secret store (manage-secrets). - Lock file:
tofu providers lock -platform=linux_amd64 -platform=linux_arm64 -platform=darwin_arm64, committed. CI runsinit -lockfile=readonly. .gitignore:.terraform/,*.tfstate*,*.tfplan,tfplan,crash.log,*.auto.tfvars. Noterraform.tfvarsand no*.auto.tfvarsunderterraform/(TF-FILE-04).- Module tests with
mock_provider(tofu test).
6. Wire
- Workflows: the set-up-terraform-pipeline files with
tool: tofuandcloud: none, plus the changes in opentofu-ci-patterns.md. PRs run only the credential-free checks; every plan and apply starts by hand (infra.yml,deploy.yml,promote.yml); drift istf-drift.yml, started by hand. - GitHub environments (
scripts/setup-github.sh):dev,stg,prdhold the secrets for apply, without reviewers; the gatespromote-stg,promote-prdandtf-apply-prdhold the reviewer.dev-plan,stg-plan,prd-planhold plan secrets without reviewers, so a plan does not wait for a person. - Environment secrets:
TF_ENCRYPTION,TFSTATE_ACCESS_KEY_IDandTFSTATE_SECRET_ACCESS_KEY(state bucket credentials, mapped toAWS_*in the job), andHCLOUD_TOKENorIONOS_TOKEN. No OIDC exists for Hetzner or IONOS (none found 2026-10-09), so these are long-lived tokens: scope them per environment, rotate on a schedule, list them infleet.ymlsecrets:(SEC-07), and record theSEC-01exception there. - Delivery of the application itself stays in set-up-delivery-pipeline.
7. Verify
cd terraform
tofu fmt -check -recursive # no output
tofu init -input=false -lockfile=readonly -backend-config=envs/dev.backend.hcl # "successfully initialized"
tofu validate # "The configuration is valid."
tofu -chdir=modules/<name> test # "Success! N passed, 0 failed."
tofu plan -input=false -var-file=envs/dev.tfvars -detailed-exitcode; echo $? # 0 = no changes, 2 = changes, 1 = error
pnpm --dir <fleet-standard> fleet check .. # "Result: conforms"
Encryption check: with TF_ENCRYPTION unset, tofu plan must fail to read the state. A plan file must start with {"meta":{"key_provider.pbkdf2…, not with the plan JSON.
Locking check: run two applies at once; the second must print Error acquiring the state lock. If the bucket cannot lock, use_lockfile is not safe there; see the S3 rule in the patterns file.
References
- opentofu-patterns.md: layout, backend, encryption, providers, secrets, tests.
- opentofu-ci-patterns.md: the pipeline changes for OpenTofu, plan artifact, drift.
- ../set-up-terraform-pipeline/workflow-templates.md: the workflow files.
- ../_shared/environments.md, ../_shared/hosting-decision.md.
- ../../core/_shared/security-baseline.md, ../../core/_shared/engineering-principles.md.