Upgrading Providers and Terraform Versions
The google provider ships a major version roughly once a year — 5 to 6 to 7 — and each major renames arguments, removes deprecated ones, and changes defaults that quietly alter your plan. Upgrading is not optional; you cannot pin to a dead major forever, because new GCP features only land in current provider versions and old ones stop getting fixes.
The way the upgrade goes wrong is doing it everywhere at once. Bump the provider across all the Hatch state files in a single PR and a renamed argument breaks bootstrap, networking, and every app stack simultaneously, with no untouched stack left to compare against. The work is staged, guide-driven, and done one stack at a time.
The Two Versions You Pin
There are two independent versions in play, and conflating them is the first confusion. required_version in the terraform block pins the Terraform core — the CLI binary itself. The required_providers constraint pins the google provider — the plugin that talks to GCP. They upgrade on separate cadences, and a major provider bump is by far the riskier of the two, because the provider is where the breaking changes to resource behavior live.
terraform { required_version = ">= 1.9" # pins the CLI (core) — changes rarely required_providers { google = { source = "hashicorp/google" version = "~> 7.0" # pins the provider — where the risk lives } } }
Reading the Upgrade Guide
Every google major ships a published "Upgrade Guide" that lists the renamed and removed arguments and the changed defaults — fields that moved into blocks, a default that flipped from false to true, arguments that no longer exist. This guide is the work order. Read it before you touch the constraint, not after the plan breaks and you are reverse-engineering which of forty diffs is the one the upgrade introduced.
Convert the guide into a checklist of exactly which arguments your configurations use that the major changed. Most majors touch a handful of resources you actually run; the guide tells you which, so the upgrade becomes editing a known list rather than discovering breakage one failed plan at a time.
The Mechanics
The mechanical steps are short. Bump the constraint to ~> 7.0, run terraform init -upgrade to resolve the new provider and rewrite .terraform.lock.hcl, then run terraform plan and read every diff. A major bump often surfaces drift the old provider was hiding, so an empty plan is not guaranteed even when you changed no HCL — the new provider may simply report a field the old one ignored.
# After bumping the constraint to ~> 7.0 in required_providers terraform init -upgrade terraform plan # read every diff; an empty plan is not guaranteed
Staging Across Many State Files
Hatch has a state file per stack — bootstrap, networking, each app, data — and they upgrade independently. Upgrade the least-critical stack first, a dev project, and validate it end to end. Then roll forward to staging, then to prod, one PR per stage. Never bump all stacks in one PR: if a regression hits, you want it to hit one low-stakes stack you can compare against the rest, not every stack in the org at the same moment.
This staging is the whole reason a per-stack state layout pays off here. The blast radius of a bad provider bump is one stack, and the dev-first order means the riskiest discovery happens where an outage costs nothing.
Deprecation Warnings as the Early-Warning System
Minor releases warn about arguments the next major will remove. Treat those warnings as a backlog, not noise. Clear them as they appear and the major upgrade is editing a handful of already-known fields; ignore them for months and the major becomes a scavenger hunt for every removed argument under time pressure. Letting required_version drift far behind has the mirror problem: a state file written by a newer CLI cannot be read by an older one on a teammate's machine or in CI.
required_version — gates which Terraform CLI may run the config. It changes rarely and breaks little; its main failure mode is drifting so far behind that an older CLI cannot read state a newer one wrote.
The provider constraint (~> 7.0) — gates which google provider resolves, and is where the breaking-change risk lives. Pin both, but read the provider upgrade guide for every major and expect the provider, not core, to break your plan.
- Bumping the
googleprovider major across every Hatch stack in one PR, so a renamed argument breaks bootstrap, networking, and all app stacks at once with no safe stack left to compare against. - Running
init -upgradeand applying without reading the upgrade guide, then having a changed default silently modify a live resource hidden among the real changes in the plan. - Ignoring deprecation warnings for months, so the major upgrade becomes a scavenger hunt for every removed argument instead of a known list.
- Letting
required_versiondrift far behind, so a state file written by a newer CLI cannot be read by an older one on a teammate's machine or in CI. - Upgrading the provider but forgetting to commit the rewritten
.terraform.lock.hcl, so CI resolves a different build than the laptop that validated the upgrade.
- Read the published
googleprovider upgrade guide before every major bump and convert it into a checklist of arguments to change. - Stage the upgrade least-critical-stack-first — dev, then staging, then prod — one PR per stage, never all stacks at once.
- Pin both
required_versionand a pessimistic provider constraint, and commit the lock file the upgrade rewrites. - Clear deprecation warnings as they appear in minor releases so each major upgrade is small and predictable.
- Run
terraform planafterinit -upgradeand read every diff, since a major often surfaces drift the old provider hid.
Knowledge Check
What is the difference between required_version and the provider version constraint?
required_versionpins the Terraform CLI; the provider constraint pins thegoogleprovider, and the provider is where breaking changes live- They are two interchangeable names for the same single version setting, so whichever one you happen to declare in the terraform block works on its own and the other can simply be left out entirely
required_versionpins thegoogleprovider and the provider constraint pins the CLI binary- One is exclusively for OpenTofu and the other only for Terraform core
Why stage a major provider upgrade across stacks instead of doing all of them in one PR?
- So a regression hits one low-stakes stack you can compare against the rest, not every stack in the org at once
- Because Terraform hard-refuses to upgrade more than one state stack within a single twenty-four-hour window and returns a cooldown error if you try the second one too soon
- Because a shared lock file can only record one provider version across all stacks
- Staging is purely a cosmetic style preference with no real operational benefit
What is the provider upgrade guide for?
- It lists the renamed and removed arguments and changed defaults for a major, so you read it before bumping the constraint
- It is auto-generated from your own plan output after the upgrade has run, listing each resource that already broke along with the line in your config that triggered the failure
- It is a high-level marketing changelog of new features with no actionable operational content
- It tells you which Terraform CLI core version the new provider requires
Why clear deprecation warnings from minor releases as they appear?
- They name arguments the next major will remove, so clearing them early makes the major a small, known edit instead of a scavenger hunt
- Each warning hard-blocks
applywith a non-zero exit code until you edit the flagged argument and re-plan, so ignoring even a single one halts all further work on that stack until it is cleared - They flag a security vulnerability in the resource that must be patched and applied immediately
- They are emitted only by OpenTofu and can be safely ignored when you run Terraform core
You got correct