iac(tofu): pin providers where OpenTofu actually reads them + commit locks - #1108
Merged
Conversation
…locks
infra/tofu/shared/versions.tf pinned google/google-beta at ~> 6.0 and said the
pins were "used only in envs/gcp-* and modules that explicitly declare
provider". Neither half was true. OpenTofu has no include mechanism for root
modules: required_providers is read only from the directory `tofu init` runs
in. infra/tofu/shared/ is a sibling root with no backend and no resources; the
envs reference ../../modules/*, never that path. So the file was never loaded
by any env, and none of envs/{local,gcp-landing,shared,prod} declared providers
of its own.
Measured with OpenTofu 1.8.3 (the TOFU_VERSION in tofu-plan.yml), clean init:
envs/prod and envs/shared resolved google + google-beta 7.42.0 — a full major
above the intent written in shared/versions.tf. envs/local floated
local/null/random. With .terraform.lock.hcl gitignored, every CI run
re-resolved from the registry, so a green `tofu validate` guaranteed nothing
about the next run.
Strategy: required_providers per env root, not a shared file. OpenTofu offers
no native include, and the alternatives are worse. A symlinked or duplicated
shared versions.tf forces every root to carry the union of providers, and a
required_providers entry is installed and locked even when nothing references
it (verified) — envs/local, documented as "no cloud provider required", would
have started depending on google. Per-root files also match what
environments/*/versions.tf and bootstrap/*/main.tf already do, so this makes
envs/* consistent with the rest of the tree instead of adding a second pattern.
Each env declares only the providers it uses:
envs/local local ~> 2.5, null ~> 3.2, random ~> 3.6
envs/gcp-landing google + google-beta ~> 6.0
envs/shared google + google-beta ~> 6.0
envs/prod google + google-beta ~> 6.0
6.x matches environments/gcp-{gke,ephemeral,persistent}, which were already
pinned there.
shared/versions.tf keeps required_version and drops required_providers: that
root has no resources, and declaring providers made `tofu init` fetch and lock
six of them for a configuration that uses none. The comment now states what the
directory is and is not, and points at the files that carry the real pins.
Stop gitignoring .terraform.lock.hcl. The pins bound a range; the lock is what
makes resolution reproducible, and it carries registry checksums so init
verifies what it downloads. Locks for the four envs were generated with
OpenTofu 1.8.3 via `tofu providers lock` for linux_amd64 (CI), darwin_arm64 and
darwin_amd64, so init works on CI and on either Mac. They are 2-4 KB each and
did not change when init was re-run against them.
Verified locally, OpenTofu 1.8.3, .terraform removed, no plugin cache, lock
committed — the CI sequence:
envs/local init ok, validate ok local 2.9.0 null 3.3.0 random 3.9.0
envs/gcp-landing init ok, validate ok google + google-beta 6.50.0
envs/shared init ok, validate ok google + google-beta 6.50.0
envs/prod init ok, validate ok google + google-beta 6.50.0
shared/ init ok, validate ok zero providers (was six)
The 7.42.0 -> 6.50.0 move produced no change in validate output: every root
that was clean at 7.42.0 is clean at 6.50.0, with no new errors or warnings.
envs/gcp-landing was checked at both majors explicitly and is clean at both.
`tofu plan` was not run — it needs GCP credentials — so apply-time differences
between the majors are unmeasured.
Contributor
There was a problem hiding this comment.
Pull request overview
Note
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Moves provider version constraints from an unused “shared” root into the actual envs/* OpenTofu roots, and enables committing lock files by removing .terraform.lock.hcl from .gitignore to make provider resolution reproducible.
Changes:
- Remove
required_providersfrominfra/tofu/shared/versions.tfand document why it is not an include point. - Add per-root
versions.tffiles forinfra/tofu/envs/{local,gcp-landing,shared,prod}with explicit provider pins. - Update
infra/tofu/.gitignoreto stop ignoring.terraform.lock.hcland record a lock regeneration command.
Reviewed changes
Copilot reviewed 6 out of 10 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| infra/tofu/shared/versions.tf | Drops unused provider pins from the shared/ root and adds explanatory documentation. |
| infra/tofu/envs/shared/versions.tf | Introduces root-level provider pins for envs/shared so tofu init reads them. |
| infra/tofu/envs/prod/versions.tf | Introduces root-level provider pins for envs/prod to prevent provider major drift. |
| infra/tofu/envs/local/versions.tf | Pins only local/dev providers for the cloud-free envs/local root. |
| infra/tofu/envs/gcp-landing/versions.tf | Introduces root-level provider pins for envs/gcp-landing. |
| infra/tofu/.gitignore | Un-ignores lock files and documents how to generate multi-platform locks. |
Files not reviewed (4)
- infra/tofu/envs/gcp-landing/.terraform.lock.hcl: Generated file
- infra/tofu/envs/local/.terraform.lock.hcl: Generated file
- infra/tofu/envs/prod/.terraform.lock.hcl: Generated file
- infra/tofu/envs/shared/.terraform.lock.hcl: Generated file
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+3
to
+5
| # OpenTofu has no include mechanism for root modules: `required_providers` is | ||
| # only read from the root it lives in. Every root under infra/tofu/ therefore | ||
| # carries its own versions.tf. infra/tofu/shared/ is NOT included by anything — |
| # | ||
| # google is held at 6.x to match environments/gcp-* (gcp-gke, gcp-ephemeral, | ||
| # gcp-persistent). Before this file existed the root was unconstrained and | ||
| # resolved whatever was newest that day — 7.42.0 as of 2026-07-29, a full major |
Comment on lines
+16
to
+18
| # Regenerate with the TOFU_VERSION in .github/workflows/tofu-plan.yml: | ||
| # tofu providers lock \ | ||
| # -platform=linux_amd64 -platform=darwin_arm64 -platform=darwin_amd64 |
mdheller
added a commit
that referenced
this pull request
Aug 1, 2026
…alidated bar as envs/* #1108 pinned providers and committed locks for the four `envs/*` roots CI validates, and deliberately left the other nine unlocked. This closes that gap for the six `environments/*` and three `bootstrap/*` roots, so every cloud root in the tree is offline-validated on the same terms: `required_version` present, `required_providers` pinned, lock committed, `tofu init -backend=false && tofu validate` clean. All measurement with OpenTofu 1.8.3 — the TOFU_VERSION in tofu-plan.yml — from a clean .terraform with no plugin cache. Four asymmetries closed: * bootstrap/* declared no `required_version`. It was the only tier without one. The terraform block moves to a versions.tf in each root, so all 14 roots now declare versions in the same filename, and `>= 1.8.0` matches every peer. * Three google majors in one estate, none of it recorded. bootstrap/gcp moves ~> 5.0 -> ~> 6.0, joining environments/gcp-* and (via #1108) envs/*. One google major, on purpose. Validated at BOTH 5.45.2 and 6.50.0: clean, no warnings either side. Same call for azurerm, which was split 3.x/4.x for the same non-reason: bootstrap/azure moves ~> 3.0 -> ~> 4.0 to match environments/azure-aks. Also validated at both majors. * environments/aws-eks pulled tls, time, null and cloudinit transitively from terraform-aws-modules/{vpc,eks} under floor-only constraints — `tls >= 3.0.0` was resolving 4.3.0 and would have crossed into 5.x unannounced. A floor without a ceiling is not a pin. All four are now declared at the root with ceilings; tls and null match the constraints already written in shared/versions.tf. No resolved version changed. * bootstrap/aws was the only root validating with a warning: aws_s3_bucket_lifecycle_configuration with neither `filter` nor `prefix`, which the provider says "will be an error in a future version". `filter {}` is the documented way to say "all objects", which is what the rule already meant. Warning gone. Also fixed: environments/gcp-{ephemeral,persistent} carry their own .gitignore that ignores .terraform.lock.hcl. A nested .gitignore overrides its parent, so #1108's un-ignore does not reach them and both locks would have been dropped silently by `git add`. Those two lines are removed; state stays ignored. Locks use the same platform set as #1108 (linux_amd64, darwin_arm64, darwin_amd64) — verified 3 h1 hashes per provider across all 21 entries — and are stable: a second clean init leaves all eight byte-identical. environments/ibm-iks is deliberately untouched. It is the one root that cannot init, #1103 fixes the cause and #1107 the validate errors, and both are open against it. Its lock belongs to whichever of those lands; the generated content is in the PR body. Evidence is local only. GitHub Actions is at a spend cap with hundreds of jobs queued and none running, so no CI run backs any of this.
mdheller
added a commit
that referenced
this pull request
Aug 1, 2026
…alidated bar as envs/* (#1115) #1108 pinned providers and committed locks for the four `envs/*` roots CI validates, and deliberately left the other nine unlocked. This closes that gap for the six `environments/*` and three `bootstrap/*` roots, so every cloud root in the tree is offline-validated on the same terms: `required_version` present, `required_providers` pinned, lock committed, `tofu init -backend=false && tofu validate` clean. All measurement with OpenTofu 1.8.3 — the TOFU_VERSION in tofu-plan.yml — from a clean .terraform with no plugin cache. Four asymmetries closed: * bootstrap/* declared no `required_version`. It was the only tier without one. The terraform block moves to a versions.tf in each root, so all 14 roots now declare versions in the same filename, and `>= 1.8.0` matches every peer. * Three google majors in one estate, none of it recorded. bootstrap/gcp moves ~> 5.0 -> ~> 6.0, joining environments/gcp-* and (via #1108) envs/*. One google major, on purpose. Validated at BOTH 5.45.2 and 6.50.0: clean, no warnings either side. Same call for azurerm, which was split 3.x/4.x for the same non-reason: bootstrap/azure moves ~> 3.0 -> ~> 4.0 to match environments/azure-aks. Also validated at both majors. * environments/aws-eks pulled tls, time, null and cloudinit transitively from terraform-aws-modules/{vpc,eks} under floor-only constraints — `tls >= 3.0.0` was resolving 4.3.0 and would have crossed into 5.x unannounced. A floor without a ceiling is not a pin. All four are now declared at the root with ceilings; tls and null match the constraints already written in shared/versions.tf. No resolved version changed. * bootstrap/aws was the only root validating with a warning: aws_s3_bucket_lifecycle_configuration with neither `filter` nor `prefix`, which the provider says "will be an error in a future version". `filter {}` is the documented way to say "all objects", which is what the rule already meant. Warning gone. Also fixed: environments/gcp-{ephemeral,persistent} carry their own .gitignore that ignores .terraform.lock.hcl. A nested .gitignore overrides its parent, so #1108's un-ignore does not reach them and both locks would have been dropped silently by `git add`. Those two lines are removed; state stays ignored. Locks use the same platform set as #1108 (linux_amd64, darwin_arm64, darwin_amd64) — verified 3 h1 hashes per provider across all 21 entries — and are stable: a second clean init leaves all eight byte-identical. environments/ibm-iks is deliberately untouched. It is the one root that cannot init, #1103 fixes the cause and #1107 the validate errors, and both are open against it. Its lock belongs to whichever of those lands; the generated content is in the PR body. Evidence is local only. GitHub Actions is at a spend cap with hundreds of jobs queued and none running, so no CI run backs any of this.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The gap
infra/tofu/shared/versions.tfpinned google/google-beta at~> 6.0and described them as "used only in envs/gcp- and modules that explicitly declare provider"*.Both halves were false. OpenTofu has no include mechanism for root modules —
required_providersis read only from the directorytofu initruns in.infra/tofu/shared/is a sibling root with no backend and no resources; the envs reference../../modules/*and never that path. So the file was never loaded by any env, and none ofenvs/{local,gcp-landing,shared,prod}declared providers of its own.A version constraint that constrains nothing, documented as constraining something.
Measured with OpenTofu 1.8.3 (the
TOFU_VERSIONintofu-plan.yml), clean init, no plugin cache:envs/locallocal/null/randomunconstrained~> 2.5/~> 3.2/~> 3.6→ 2.9.0 / 3.3.0 / 3.9.0envs/gcp-landingrequired_providers~> 6.0→ 6.50.0envs/shared~> 6.0→ 6.50.0envs/prod~> 6.0→ 6.50.0shared/envs/prod— the production root — was resolving a full major above the intent written inshared/versions.tf. And with.terraform.lock.hclgitignored, every CI run re-resolved from the registry: a greentofu validateguaranteed nothing about the next run.Strategy:
required_providersper env rootNot a shared file. OpenTofu offers no native include, and every "shared" alternative is worse:
versions.tfforces every root to carry the union of providers. Arequired_providersentry is installed and locked even when nothing references it — verified directly: a config whose only resource israndom_passwordstill downloaded and locked google 6.50.0 when google was listed.envs/localis documented as "No cloud provider required"; a shared file would have made the local k3d dev loop depend on the google provider.Per-root files also match what
environments/*/versions.tfandbootstrap/*/main.tfalready do, so this makesenvs/*consistent with the rest of the tree rather than introducing a second pattern. And the pins are self-enforcing — OpenTofu fails init if they're violated — so no drift-guard job is needed to give them teeth.6.x matches
environments/gcp-{gke,ephemeral,persistent}, which were already pinned there.shared/versions.tfkeepsrequired_versionand dropsrequired_providers: that root has no resources. The comment now states what the directory is and is not, and points at the files carrying the real pins.Lock files: stop gitignoring them
Recommendation taken: commit them. The pins bound a range; the lock is what makes resolution reproducible, and it carries registry checksums so
initverifies what it downloads.Tradeoff:
tofu providers lockmust be re-run), and PR conflicts when two branches bump the same provider. Multi-arch entries are required orinitfails on the platform you forgot.tofu providers lock -platform=linux_amd64 -platform=darwin_arm64 -platform=darwin_amd64, covering CI and both Mac architectures. They are 2–4 KB each. Re-runninginitagainst them produced no diff, so they are stable, not churning.infra/tofu/.gitignorenext to the un-ignore, so the next person doesn't have to rediscover it.Locks are committed for the 4
envs/*roots only — the ones CI validates, where the guarantee is load-bearing.environments/*andbootstrap/*are deliberately left unlocked in this PR (CI never touches them, and two of those paths belong to other lanes).Behaviour change from the major-version move: none observed at
validateThe 7.42.0 → 6.50.0 move produced no change in validate output. Every root clean at 7.42.0 is clean at 6.50.0 — no new errors, no new warnings.
envs/gcp-landingwas checked at both majors explicitly (~> 7.0and~> 6.0) and is clean at both.Unmeasured:
tofu planwas not run — it needs GCP credentials. Schema-level compatibility is confirmed; apply-time differences between google 6.x and 7.x (changed defaults, new required fields) are not covered by this evidence.Also unverified: CI itself. GitHub Actions is at a spend cap (456 queued, 0 running), so no workflow ran for this branch. Everything above is local, with
.terraformremoved and no plugin cache, replicating the CI command sequence — but it is not a CI result.Caveat on the
gcp-landingmeasurementenvs/gcp-landingcannottofu initonmainat all —main.tf:215has invalid HCL (threshold_rules { threshold_percent = 0.8 spend_basis = ... }, a single-line block with two arguments). That is the bug #1103 fixes. Its post-pin numbers above were therefore measured with #1103'sgcp-landinghunk applied locally; that hunk is not part of this PR. The lock file is unaffected — it is a function ofrequired_providers, and the resource bodies introduce no additional providers.Unvalidated-root survey (requested; not fixed here)
tofu-plan.ymlvalidates 4 of 14 configuration roots. Here is what the other 10 do today under OpenTofu 1.8.3, clean init:bootstrap/aws~> 5.0)aws_s3_bucket_lifecycle_configuration.tofu_statehas nofilter/prefix— "will be an error in a future version of the provider"bootstrap/azure~> 3.0)environments/azure-akspins~> 4.0bootstrap/gcp~> 5.0)environments/aws-eksterraform-aws-modules/{vpc,eks}carry minimum-only constraints (>= 3.0.0etc.) and still floatenvironments/azure-aksenvironments/gcp-ephemeralenvironments/gcp-gkeenvironments/gcp-persistentenvironments/ibm-ikshashicorp/ibm, which does not exist. Fixed by #1103'smodules/ibm-trusted-profilerequired_providers. Untouched here — another lane owns it.shared/Two findings worth their own tickets:
bootstrap/gcpat 5.x,environments/gcp-*and nowenvs/*at 6.x. Deliberate or not, nothing records which.shared/andenvs/sharedshare a name and nothing else. CI's step is literally labelledvalidate — sharedand validatesenvs/shared. Anyone reading the workflow would reasonably concludeinfra/tofu/shared/was covered. It never was. That naming collision is plausibly why the false comment survived.Also:
bootstrap/*set norequired_version, unlike every other root.Recommendation for the
tofu-plan.ymllane (#1097 — not edited here)Widen
fmt-and-validateto all 14 roots, or at minimum addbootstrap/*andenvironments/gcp-*. A matrix over root directories would replace the four hand-written steps and stop new roots from landing unvalidated.environments/ibm-ikswill need #1103 first.Note
tofu fmt -check -recursive infra/tofu/currently fails onmainfor three files — all owned by #1103 (environments/aws-eks/main.tf,modules/github-oidc-aws/main.tf,modules/ibm-trusted-profile/variables.tf). Everything this PR touches is fmt-clean.Lane hygiene
Rebased on
origin/main@ 55a3d08.git merge-treeagainst #1103 and #1097 is clean with zero overlapping files. Stayed out ofenvironments/ibm-iksandmodules/ibm-trusted-profileentirely.🤖 Generated with Claude Code