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
- Verify s3:DeleteObject permission on the lock key for the principal via the IAM policy simulator.
- 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.
- Retry the delete directly via CLI: `aws s3api delete-object --bucket <bucket> --key <path>.tflock`.
- Inspect the wrapped AWS error code to distinguish AccessDenied from ObjectLocked.
- 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
- Grant s3:DeleteObject on the `.tflock` key in the apply role.
- Avoid enabling S3 Object Lock on the state bucket's lock-key prefix, or exclude lock files from retention.
- Retry deletes once for transient S3 errors before escalating.
- Use `terraform force-unlock <id>` rather than manual deletion.
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
- failed to unlock S3
- unable to retrieve file from S3 bucket
- failed to read the body of the S3 object
- failed to retrieve lock info for lock ID
- failed to unlock both S3 and DynamoDB: S3 error
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)