Upgrading Terraform Safely: Compatibility Promises, No-op Verification, Rollback Prep

4 min read

Terraform CLI upgrades are easy to postpone, and the kind of work that gets scarier the longer it waits. The conclusion up front: minor upgrades within 1.x are light thanks to the compatibility promises, and the substance is three steps in order — read the changelog, verify a no-op plan on a non-production stack, and unify team and CI versions. At the time of writing the stable version is 1.15 with 1.16 in beta (as of 2026-08; check the latest at adoption time). The separate axis of provider major upgrades is the next post.

The premise: v1.x compatibility promises #

Since Terraform 1.0, the v1.x Compatibility Promises apply: within the 1.x range, existing configurations and core workflows are officially kept working. This is not the 0.x era of syntax changes and migration tools every minor release. As a consequence, you do not need to apply 1.x minor upgrades sequentially — skipping versions is fine. Going straight from 1.11 to 1.15 without breaking configuration is the intended behavior.

The promises do not cover everything: experimental features, undocumented behavior, and the final removal of things already deprecated sit outside them. That is why the first step is the changelog. Skimming just the removed and deprecated entries of the versions you are jumping over reveals most of the upgrade hazards. The substantive recent changes (native S3 locking with use_lockfile, ephemeral values, and so on) covered in Basics #9, remote state and collaboration are, if anything, the reasons to upgrade.

The procedure: no-op is the passing bar #

  1. Read the changelog: check removed and deprecated entries from your current version to the target. The question is whether anything you use is on the list.
  2. Upgrade a non-production stack first: run terraform plan with the new version on a development stack. The passing bar is “No changes.” If the version bump alone produces a diff with no code or infrastructure change, stop until you understand why.
  3. Read the warnings in the plan output: deprecation warnings from the new version are the homework list for the next upgrade. Clearing them now makes the next one lighter.
  4. Roll out stack by stack: from least critical to production, holding each stack to the same no-op bar.
  5. Update required_version: afterwards, pin the floor and range to the new version, e.g. required_version = "~> 1.15.0", so an old CLI cannot run by accident.

Keeping CLI and provider upgrades apart matters here. terraform init -upgrade raises providers to the newest versions within the lock file’s constraints, and running it out of habit while bumping the CLI changes two variables at once, making any diff impossible to attribute. Raise the CLI first, confirm the no-op, and upgrade providers in their own PR.

Rollback prep: state versions are one-way #

The reason to prepare a way back lives in state. When a newer CLI writes state, the version information inside is updated, and an older CLI may refuse to read it. Once half the team has applied with 1.15, there is no natural way to roll back to 1.13.

Two preparations cover it. First, confirm versioning on the S3 backend: if a rollback becomes necessary, restoring the state object version from before the new CLI wrote it is the recovery path — the same versioning dependence as in recovering a deleted tfstate. Second, make the upgrade a short, whole-team event. Weeks of old and new versions running side by side is the most dangerous state.

Team and CI: pin versions in files, not memory #

If only your laptop upgrades and CI stays behind, CI failing to read locally written state is exactly the scenario above. Versions belong in files.

  • required_version: the enforcement line at the configuration level; out-of-range CLIs are refused from init onwards.
  • tfenv or mise: local version managers. Commit a .terraform-version file and everyone who checks out the repo selects the same version. mise extends the same mechanism to other tools.
  • Explicit versions in CI: with GitHub Actions, set setup-terraform’s version input to the same value as .terraform-version. Leaving it on latest means CI upgrades itself some morning before you do.

The upgrade itself then becomes a single PR touching these files — .terraform-version, required_version, CI config — changed together in one commit that reviewers can take in at a glance.

Summary #

  • The v1.x compatibility promises make minor upgrades skippable rather than sequential. What needs reading is the changelog’s removed and deprecated entries.
  • The passing bar is a no-op plan. A version-only diff means stopping until it is understood.
  • Separate CLI and provider upgrades. Habitually mixing in init -upgrade makes diffs unattributable.
  • State written by a newer version may be unreadable to older ones. S3 versioning plus a short transition window is the rollback preparation.
  • Pin versions in three files — required_version, .terraform-version (tfenv/mise), CI config — and ship the upgrade as the one PR that changes them.
X