hashicorp/terraform · error

failed to delete the lock file

Error message

failed to delete the lock file: %w

What it means

Thrown inside unlockWithFile at the final step: GetObject, body read, JSON parse, and ID match all succeeded, but the DeleteObject call to remove the lock file failed. The lock is verified-owned but still present, so the state remains locked.

Solutions

  1. Verify s3:DeleteObject permission on the lock key for the principal via the IAM policy simulator.
  2. If S3 Object Lock is enabled on the bucket, check for a legal hold or retention on the lock object: `aws s3api head-object-legal-hold` / `get-object-retention`; remove the hold or wait out retention, or use a different lock strategy.
  3. Retry the delete directly via CLI: `aws s3api delete-object --bucket <bucket> --key <path>.tflock`.
  4. Inspect the wrapped AWS error code to distinguish AccessDenied from ObjectLocked.
  5. Once the object is gone, the unlock is complete; re-run the apply to confirm the lock clears.

Example fix

# principal could read but not delete — remove directly and fix IAM
aws s3api delete-object --bucket tf-state-prod --key prod/terraform.tflock.tflock
# then grant s3:DeleteObject on the lock key in the IAM policy
Defensive patterns

Strategy: retry

Validate before calling

// Validate DeleteObject permission via a dry-run-ish check: confirm IAM via the simulator is ideal;
// here, confirm the bucket/key is delete-reachable (absence of legal hold).
func deletable(ctx context.Context, c *s3.Client, bucket, lockKey string) error {
  if _, err := c.GetObjectLegalHold(ctx, &s3.GetObjectLegalHoldInput{Bucket: &bucket, Key: &lockKey}); err != nil {
    var apiErr smithy.APIError
    if errors.As(err, &apiErr) && apiErr.ErrorCode() == "NoSuchObjectLockConfiguration" { return nil }
    return err
  }
  return errors.New("legal hold is ON; delete will fail")
}

Try / catch

// Retry the delete once for transient errors; surface AccessDenied/ObjectLocked distinctly.
if _, err := c.s3Client.DeleteObject(ctx, delInput); err != nil {
  var apiErr smithy.APIError
  if errors.As(err, &apiErr) {
    switch apiErr.ErrorCode() {
    case "AccessDenied", "ObjectLocked":
      return fmt.Errorf("failed to delete the lock file (%s): %w; check IAM/legal-hold", apiErr.ErrorCode(), err)
    }
  }
  // transient — retry once
  if _, err2 := c.s3Client.DeleteObject(ctx, delInput); err2 != nil {
    return fmt.Errorf("failed to delete the lock file: %w", err2)
  }
}

Prevention

When it happens

Trigger: c.s3Client.DeleteObject at client.go:547 returns an error. Triggers: IAM principal lost s3:DeleteObject on the key, a bucket policy denies deletes, object-lock retention/legal hold on the lock object blocks deletion, a transient S3 error, or the bucket was deleted between Get and Delete.

Common situations: Asymmetric IAM policy granting GetObject but not DeleteObject, S3 Object Lock (compliance mode) on the bucket preventing lock-file deletion, permissions narrowed mid-run, or a regional S3 hiccup.

Related errors


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

Appendix: source

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

	lockInfo := &statemgr.LockInfo{}
	if err := json.Unmarshal(data, lockInfo); err != nil {
		return fmt.Errorf("failed to unmarshal JSON data into LockInfo struct: %w", err)
	}
	lockErr.Info = lockInfo

	// Verify that the provided lock ID matches the lock ID of the retrieved lock file.
	if lockInfo.ID != id {
		return fmt.Errorf("lock ID '%s' does not match the existing lock ID '%s'", id, lockInfo.ID)
	}

	// Delete the lock file to release the lock.
	_, err = c.s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{
		Bucket: aws.String(c.bucketName),
		Key:    aws.String(c.lockFilePath),
	})

	if err != nil {
		return fmt.Errorf("failed to delete the lock file: %w", err)
	}

	log.Debug(fmt.Sprintf("Deleted lock file: '%q'", c.lockFilePath))

	return nil
}

func (c *RemoteClient) unlockWithDynamoDB(ctx context.Context, id string, lockErr *statemgr.LockError) error {
	// TODO: store the path and lock ID in separate fields, and have proper
	// projection expression only delete the lock if both match, rather than
	// checking the ID from the info field first.
	lockInfo, err := c.getLockInfoWithDynamoDB(ctx)
	if err != nil {
		return fmt.Errorf("failed to retrieve lock info for lock ID %q: %s", id, err)
	}
	lockErr.Info = lockInfo

	if lockInfo.ID != id {

View on GitHub (pinned to d32a084675)