hashicorp/terraform · critical

%v Additionally, unlocking the state file on Google Clou

Error message

%v
				Additionally, unlocking the state file on Google Cloud Storage failed:

				Error message: %q
				Lock ID (gen): %v
				Lock file URL: %v

				You may have to force-unlock this state in order to use it again.
				The GCloud backend acquires a lock during initialization to ensure
				the initial state file is created.

What it means

StateMgr initializes a fresh state when none exists; to do so safely it locks first, then writes+persists, then unlocks. If WriteState or PersistState fails AND the subsequent st.Unlock(lockID) also fails, this composite message is returned: the original base error plus the unlock failure, the generated lock ID (GCS object generation number), and the lock file URL. The user is told they may need to force-unlock.

Source

Thrown at internal/backend/remote-state/gcs/backend_state.go:136

		lockID, err := st.Lock(lockInfo)
		if err != nil {
			return nil, diags.Append(err)
		}

		// Local helper function so we can call it multiple places
		unlock := func(baseErr error) error {
			if err := st.Unlock(lockID); err != nil {
				const unlockErrMsg = `%v
				Additionally, unlocking the state file on Google Cloud Storage failed:

				Error message: %q
				Lock ID (gen): %v
				Lock file URL: %v

				You may have to force-unlock this state in order to use it again.
				The GCloud backend acquires a lock during initialization to ensure
				the initial state file is created.`
				return fmt.Errorf(unlockErrMsg, baseErr, err.Error(), lockID, c.lockFileURL())
			}

			return baseErr
		}

		if err := st.WriteState(states.NewState()); err != nil {
			unlockErr := unlock(err)
			return nil, diags.Append(unlockErr)
		}
		if err := st.PersistState(nil); err != nil {
			unlockErr := unlock(err)
			return nil, diags.Append(unlockErr)
		}

		// Unlock, the state should now be initialized
		if err := unlock(nil); err != nil {
			return nil, diags.Append(err)
		}

View on GitHub (pinned to c9def3e214)

Solutions

  1. Read the %q inner unlock message and the Lock ID: if the lock file still exists, run 'terraform force-unlock <Lock ID>' against the affected workspace.
  2. Resolve the root baseErr (the %v at the start): e.g. fix encryption_key, restore write permissions, retry the init.
  3. Verify the SA still holds storage.objects.create/delete on the bucket and prefix; confirm the .tflock object's current generation with 'gsutil stat gs://<bucket>/<prefix>/<ws>.tflock'.
  4. Re-run 'terraform init' once the underlying cause is fixed; the next init will either reuse or overwrite the partially-initialized state.

Example fix

# recover
terraform workspace select <ws>
terraform force-unlock 1234567890   # the Lock ID (gen) from the message
terraform init
Defensive patterns

Strategy: try-catch

Validate before calling

// Ensure CSEK and IAM are stable before init to avoid write-then-unlock-both-fail
if err := validateCSEKAndIAM(); err != nil { return err }

Try / catch

if diags := backend.StateMgr(ws); diags.HasErrors() {
    msg := diags.Err().Error()
    if strings.Contains(msg, "force-unlock") {
        id := extractLockID(msg) // parse 'Lock ID (gen): N'
        runTerraformForceUnlock(id)
    }
}

Prevention

When it happens

Trigger: During 'terraform init' on an empty workspace: the lock is acquired (a .tflock object is created with DoesNotExist precondition), then WriteState/PersistState fails (e.g. encryption key mismatch, bucket write permission revoked mid-flight), and the unlock delete targeting that generation also fails (concurrent modification, permission loss, transient GCS error).

Common situations: CSEK key was changed between lock and write; SA's storage.objects.delete was removed; transient GCS 5xx during the narrow init window; the .tflock was externally deleted/rewritten so the generation-match precondition on Unlock fails.

Related errors


AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07). Data as JSON: /api/errors/84cf92b4afbfd62e. Report an issue: GitHub.