hashicorp/terraform · error

failed to unlock S3

Error message

failed to unlock S3: %v

What it means

Dual-lock unlock where the DynamoDB unlock succeeded but the S3 file unlock (unlockWithFile) failed. The state is half-unlocked: the DynamoDB row is gone but the S3 `.tflock` object remains, so other Terraform clients will still be blocked by the S3 lock. lockErr is returned carrying the wrapped S3 failure.

Solutions

  1. Re-run `terraform force-unlock <id>`; if DynamoDB is already clear it will only need to clear the S3 file.
  2. Manually delete the lingering `.tflock` object: `aws s3api delete-object --bucket <bucket> --key <path>.tflock`.
  3. Verify s3:GetObject + s3:DeleteObject permissions on the lock key for the running principal.
  4. If SSE-C is configured, ensure the same customer key is supplied at unlock as at lock time.
  5. Inspect the wrapped S3 error to distinguish NoSuchKey (benign — already gone) from AccessDenied (fix IAM).

Example fix

# the DynamoDB lock cleared but the S3 .tflock object lingers — remove it
aws s3api delete-object \
  --bucket tf-state-prod \
  --key 'prod/terraform.tflock.tflock'
Defensive patterns

Strategy: retry

Validate before calling

// Validate S3 lock-file access before unlock.
func canUnlockS3(ctx context.Context, c *s3.Client, bucket, lockKey string) error {
  // needs both Get (to verify ID) and Delete
  if _, err := c.HeadObject(ctx, &s3.HeadObjectInput{Bucket: &bucket, Key: &lockKey}); err != nil {
    var apiErr smithy.APIError
    if errors.As(err, &apiErr) && apiErr.ErrorCode() != "NotFound" { return err }
  }
  return nil
}

Try / catch

// On S3-only unlock failure, retry once, then surface the lock ID for manual cleanup.
if ferr != nil {
  // one retry for transient S3
  if err2 := c.unlockWithFile(ctx, id, lockErr, log); err2 == nil { ferr = nil }
}
if ferr != nil {
  lockErr.Err = fmt.Errorf("failed to unlock S3: %v; run `terraform force-unlock %s` or delete %s/%s.tflock", ferr, id, bucket, lockKey)
  return lockErr
}

Prevention

When it happens

Trigger: unlockWithFile returns an error while unlockWithDynamoDB succeeded. Triggers inside unlockWithFile: GetObject on the lock file fails (NoSuchKey if already deleted, AccessDenied), the lock file body fails to read or parse, the lock ID in the file does not match the provided id, or the final DeleteObject fails.

Common situations: A concurrent force-unlock already deleted the lock file (NoSuchKey on GetObject), SSE-C key mismatch prevents reading the lock file, IAM lost s3:GetObject or s3:DeleteObject but still has DynamoDB rights, or the lock file was corrupted.

Related errors


AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11). Data as JSON: /api/errors/2b3e9bd1b858515b. Report an issue: GitHub.

Appendix: source

Thrown at internal/backend/remote-state/s3/client.go:490

		}

		log.Info("Unlocked remote state (DynamoDB only)")
		return nil
	}

	// Double unlocking: DynamoDB + file
	log.Info("Attempting to unlock remote state (S3 Native and DynamoDB)...")

	ferr := c.unlockWithFile(ctx, id, lockErr, log)
	derr := c.unlockWithDynamoDB(ctx, id, lockErr)

	if ferr != nil && derr != nil {
		lockErr.Err = fmt.Errorf("failed to unlock both S3 and DynamoDB: S3 error: %v, DynamoDB error: %v", ferr, derr)
		return lockErr
	}

	if ferr != nil {
		lockErr.Err = fmt.Errorf("failed to unlock S3: %v", ferr)
		return lockErr
	}

	if derr != nil {
		lockErr.Err = fmt.Errorf("failed to unlock DynamoDB: %v", derr)
		return lockErr
	}

	log.Info("Unlocked remote state (S3 Native and DynamoDB)")
	return nil
}

// unlockWithFile attempts to unlock the remote state by deleting the lock file from Amazon S3.
//
// This method is used when the S3 native locking mechanism is in use, which uses a `.tflock` file
// to manage state locking. The function deletes the lock file to release the lock, allowing other
// Terraform clients to acquire the lock on the same state file.
func (c *RemoteClient) unlockWithFile(ctx context.Context, id string, lockErr *statemgr.LockError, log hclog.Logger) error {

View on GitHub (pinned to d32a084675)