Terraform Professional study materials
Terraform Professional study resources

Lock File Churn, an Accidental Upgrade, and Tests That Need Real VPCs

Terraform Professional · Put your knowledge to work in context.

All case studies

Lock File Churn, an Accidental Upgrade, and Tests That Need Real VPCs

Repo infra-live: required_version = ">= 0.13" and provider hashicorp/aws constrained ~> 4.0. .terraform.lock.hcl was committed from a linux/amd64 CI runner only, with no platform entries for macOS. On a new Apple Silicon laptop terraform init rewrote the lock file, and a reviewer merged it as noise. CI then ran terraform init -upgrade inherited from an old script, resolved AWS provider 4.67, and the next plan showed 12 in-place changes to aws_eks_node_group that nobody asked for. Separately, terraform test files under modules/vpc/tests are unusable because every run block creates real VPCs and the CI role has no VPC permissions.

1. Explain the lock-file churn, give the command that should have been used, and state the CI rule.

Read the worked answer

.terraform.lock.hcl records, per provider, the selected version plus the package hashes for each platform it was generated for. A linux/amd64 lock has no darwin_arm64 entry, so init on the laptop is forced to add hashes for the package it downloads - that is expected behaviour, not corruption. The reviewer merging it as noise meant the repo now carries a lock that was never deliberately generated, and nobody can tell which hashes were reviewed.

The right command, run once on any machine, produces a complete and deterministic lock:

terraform providers lock \
  -platform=linux_amd64 \
  -platform=darwin_arm64 \
  -platform=darwin_amd64

This downloads the packages for each platform and writes all h1:/zh: hashes at once, so a developer's init no longer modifies the file. (If a provider's zh: entries change unexpectedly on a providers lock, that is a supply-chain signal - treat it as an incident, not a re-lock.)

CI rules:

  • terraform init in CI, never terraform init -upgrade. Upgrade is a deliberate, reviewed act performed in a branch.
  • If init in CI reports the lock file is inconsistent, fail the build. That message means the committed lock does not match the configuration's constraints - a real bug, not a lock to delete and regenerate.

2. Evaluate the version constraints and explain how the provider bump reached production. What constraints would you set?

Read the worked answer

~> 4.0 permits every 4.x release - 4.0 through 4.67 and beyond. That is 'any minor', which for a provider that ships behaviour changes in minors is effectively unpinned. Combined with init -upgrade in CI, the pipeline re-resolved the newest allowed version on every run: the constraint chose the ceiling, and -upgrade walked to it. The 12 aws_eks_node_group changes are consistent with an upstream provider changing a default or normalizing an argument between minors - legitimate for the provider, unacceptable to discover in a production plan.

required_version = ">= 0.13" is a separate problem: it declares compatibility with 0.13 and asserts nothing about 1.x behaviour, so nobody knows which Terraform features the config may use (and no 1.x-only syntax is guaranteed safe to introduce).

What I would set:

  • Terraform: required_version = ">= 1.6.0, < 2.0.0" - a floor for the features you use, and a ceiling that makes a major upgrade deliberate.
  • Provider: pin the minor, version = "~> 4.67.0", which allows patch releases only. The lock file already pins the exact version; the constraint's job is to keep an accidental re-resolution from crossing a minor.
  • Upgrades as a process: a branch, terraform providers lock -upgrade for the new version, and a plan against production-state read-only to review the diff. Twelve unexpected node group changes then appear in a PR instead of a deploy.

3. How would you make the module's tests runnable in CI without VPC permissions, and what should those tests actually assert?

Read the worked answer

The tests are doing an integration test's job. terraform test (1.6+) runs runs in a real provider by default; Terraform 1.7+ adds mock_provider, which is what this needs:

# tests/vpc.tftest.hcl
mock_provider "aws" {}

run "defaults" {
  command = plan
  assert {
    condition     = aws_vpc.this.cidr_block == "10.0.0.0/16"
    error_message = "vpc cidr not derived from var.vpc_cidr"
  }
}

Mocks generate fake values for computed attributes, so plan runs are fast, credential-free, and safe in a shared CI role. Where a test genuinely needs a value that only the API provides (an ARN formatted a particular way), use override_resource to inject it - that keeps the assertion meaningful instead of asserting against a mock's random string.

What these tests should assert - the module's contract, not AWS:

  • Input validation and expect_failures for the validation rules (bad CIDR, empty AZ list), so guards are proven rather than assumed.
  • Resource addressing stability: that the default call produces the expected known addresses, which is the cheapest defence against a count→for_each regression like the one this repo already lived through.
  • Output shape (the map keys consumers depend on), which is the part consumers actually break against.

Keep a small number of real-apply tests, in a dedicated account, gated to a nightly schedule - not in the PR path. Everything else should be a mock-backed command = plan.