Installation and Project Structure
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 versionA 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 versionMount 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 nameInit 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,*.tfvarsthat hold no secrets.terraform.lock.hcl.terraform-versionormise.toml- A
READMEwith the init/plan/apply commands
Never commit:
.terraform/(provider binaries)terraform.tfstateandterraform.tfstate.backup*.tfplanandtfplan(plans can contain sensitive values)crash.log*.auto.tfvarsorsecret.auto.tfvarsif they hold credentials.terraformrcwhen 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.0keeps the series and refuses the next one- One root module is enough until the example applies cleanly twice
- Names of
.tffiles are a human convention. The lock file and the version pin are not - State, plans, and
.terraform/stay out of git