Installation and Project Structure

Authors

K19G

grok-bot

Updated

October 31, 2026

Installation and Project Structure

Pin the binary before you pin the providers. This chapter installs OpenTofu 1.13.2, checks that the binary is that release, and lays out a repository that a second engineer can clone.

Learning goals

  • Install 1.13.2 on Linux, and know the macOS, Windows, and container equivalents
  • Confirm the version before trusting a plan
  • Create a one-resource project that applies
  • Name files so a stranger can find variables, and ignore the files that must not be committed

Installing OpenTofu: Linux, macOS, Windows, and Containerized Options

The worked path in this book is Linux, with the version selected by a runtime manager so the repository and the laptop agree.

Linux, pinned with mise. mise ships an OpenTofu backend. From the project directory:

mise use opentofu@1.13.2
mise exec -- tofu version

A global install is mise use -g opentofu@1.13.2. The project file mise writes belongs in git. A laptop without mise can install the same release from the official installer, which also configures the project’s apt or rpm repository:

curl --proto '=https' --tlsv1.2 -fsSL \
  https://get.opentofu.org/install-opentofu.sh -o install-opentofu.sh
chmod +x install-opentofu.sh
./install-opentofu.sh --install-method deb

--install-method rpm is the dnf/yum equivalent. The package name is tofu. An apt repository that tracks “latest” will walk off 1.13 the day 1.14 ships. A pin beats a floating distro package for this book.

macOS. Homebrew’s brew install opentofu is the OpenTofu tap and tracks current. For 1.13.2 specifically, use mise or the release tarball. 1.13 requires macOS 13 or newer. Official 32-bit builds are gone.

Windows. Install the signed 1.13.2 package from the OpenTofu GitHub release, or the OpenTofu package in winget, and confirm the version in the next section. Windows on ARM64 is supported in 1.13. There is no official 32-bit build. WinRM provisioners are gone in this series. Use OpenSSH if a provisioner must reach Windows, and prefer not to use a provisioner at all (chapter 10).

Containers. The project publishes images at ghcr.io/opentofu/opentofu. Pin the tag:

docker run --rm ghcr.io/opentofu/opentofu:1.13.2 version

Mount the working directory when you init or plan. latest is not a pin. The minimal tag is the one to copy a binary out of when you build your own image.

Verifying Your Installation and Checking Versions

tofu version prints the release and the platform. You want 1.13.2 on the platform you will apply from. A plan created on 1.13.2 should be applied by 1.13.2. Providers have their own versions, printed by tofu version after init and locked in .terraform.lock.hcl.

Three versions are in play, and mixing them up wastes an afternoon:

Version Where it lives What breaks if it floats
OpenTofu tofu version, .terraform-version, mise Language and state behaviour
Provider required_providers and the lock file Arguments the resource accepts
Module source and version on a module block Inputs the child expects

required_version = "~> 1.13.0" refuses to run on 1.12 and on 1.14. That pessimism is correct for a book pin. Widen it only when you have read the changelog.

The Minimal Working Project

Create a directory with one file, main.tf, containing the random_pet configuration from chapter 2. Then:

tofu init
tofu plan -out=tfplan
tofu apply tfplan
tofu output name

Init creates .terraform/ and .terraform.lock.hcl. Plan writes a binary plan. Apply updates terraform.tfstate. Output prints the pet name. A second tofu plan should report no changes.

Delete the experiment with tofu destroy when you are done with it. The state file is not a secret worth keeping for a pet name, and it is a secret-shaped file the moment a real resource lands in it.

Directory Layout Conventions: Single Project vs. Modular Organization

A single project is one root module: the directory where you run tofu.

network/
├── main.tf            # resources, or a thin file that calls modules
├── variables.tf       # input variables
├── outputs.tf         # outputs
├── versions.tf        # required_version and required_providers
├── providers.tf       # provider configuration
├── terraform.tfvars   # values for this environment, if not secret
├── .terraform.lock.hcl
└── .terraform-version

Filenames are convention. OpenTofu loads every .tf and .tofu file in the directory and ignores the names. The split exists so a reviewer knows where to look.

A modular organisation pulls reusable blocks into child modules (chapter 8) and keeps each environment or each account as its own root module with its own state. That is the layout chapter 14 uses for production. Start in one directory until the pet example is boring. Splitting early hides the tool behind a forest of folders.

Configuration File Naming Rules: main.tf, variables.tf, and Beyond

File Put it here
versions.tf terraform block: version, providers, backend, encryption
variables.tf variable blocks only
outputs.tf output blocks only
providers.tf provider blocks
main.tf Resources, or module calls, when the file stays readable
*.tfvars Values. Not logic
*.tofu OpenTofu-only syntax you must not let Terraform parse

Do not use override files (override.tf) to smuggle a local tweak. They merge in a way reviewers miss. A variable does the same job in the open.

JSON (.tf.json) is valid and a poor authoring format. Generate HCL, or generate JSON from a tool and review the diff. Hand-written JSON configurations rot.

.terraform-version, .gitignore, and Essential Project Files

.terraform-version contains 1.13.2. tfenv, tofuenv, and tenv read it. mise users can rely on mise.toml instead. One pin, not both, unless you have checked they agree.

Commit:

  • *.tf, *.tofu, *.tfvars that hold no secrets
  • .terraform.lock.hcl
  • .terraform-version or mise.toml
  • A README with the init/plan/apply commands

Never commit:

  • .terraform/ (provider binaries)
  • terraform.tfstate and terraform.tfstate.backup
  • *.tfplan and tfplan (plans can contain sensitive values)
  • crash.log
  • *.auto.tfvars or secret.auto.tfvars if they hold credentials
  • .terraformrc when it contains tokens

A .gitignore that encodes that list belongs in the first commit, before the first apply. The expensive mistake is committing a state file once. History keeps it after you delete the working copy.

TF_VAR_name environment variables fill variable name. Prefer them for secrets. Chapter 12 is the full rule. Chapter 5 is how variables are declared so that path works.

Summary

  • Pin 1.13.2 with mise, the release image, or an installer, then run tofu version
  • ~> 1.13.0 keeps the series and refuses the next one
  • One root module is enough until the example applies cleanly twice
  • Names of .tf files are a human convention. The lock file and the version pin are not
  • State, plans, and .terraform/ stay out of git