hashicorp/terraform · error · statemgr.LockError

failed to unlock S3: %v

Error message

failed to unlock S3: %v

What it means

Returned by RemoteClient.Unlock in dual-locking mode when only the S3 lock-file deletion failed while the DynamoDB delete succeeded. It wraps the original error from unlockWithFile, which itself may be a GetObject, read, unmarshal, ID-mismatch, or DeleteObject failure. Because DynamoDB unlocked cleanly, only the S3 side needs remediation.

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 c9def3e214)

Solutions

  1. Read the wrapped S3 error code: NoSuchKey means the file is already gone (safe to ignore), AccessDenied points to IAM/bucket policy.
  2. Confirm the S3 IAM principal has s3:GetObject and s3:DeleteObject on arn:aws:s3:::<bucket>/<lockFilePath>.
  3. If SSE-C is configured, verify the customer key provided to the backend matches the one used to create the lock file.
  4. Manually delete the orphaned .tflock object if the auto-delete keeps failing, then re-run the operation.

Example fix

// before: bucket policy denies DeleteObject on *.tflock
// after
{
  "Effect": "Allow",
  "Action": ["s3:GetObject", "s3:DeleteObject"],
  "Resource": "arn:aws:s3:::my-state-bucket/*.tflock"
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Confirm s3:DeleteObject + GetObject on the lock key before unlock
func canDeleteLockFile(ctx context.Context, s3c *s3.Client, bucket, lockKey string) error {
  if _, err := s3c.GetObject(ctx, &s3.GetObjectInput{Bucket: &bucket, Key: &lockKey}); err != nil {
    return fmt.Errorf("cannot read lock file: %w", err)
  }
  return nil // DeleteObject grant verified by IAM policy review, not API
}

Try / catch

err := client.Unlock(id)
if le, ok := err.(*statemgr.LockError); ok && strings.Contains(le.Err.Error(), "failed to unlock S3") {
    // only S3 failed; DynamoDB is clean, retry S3 delete or remediate IAM
}

Prevention

When it happens

Trigger: Unlock(id) with useLockFile=true && ddbTable set, where DynamoDB DeleteItem succeeds but S3 GetObject or DeleteObject on the .tflock file returns an error (NoSuchKey, AccessDenied, KMS/MFA mismatch, or a stale lock ID that no longer matches the file contents).

Common situations: The .tflock object was already deleted out-of-band (manual cleanup) so GetObject fails; the S3 bucket policy changed; SSE-C customer key no longer matches; or another process raced to delete the lock file first.

Related errors


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