{"record":{"id":"d94905ca8cab1e4e","repo":"hashicorp/terraform","slug":"failed-to-delete-the-lock-file-w","errorCode":null,"errorMessage":"failed to delete the lock file: %w","messagePattern":"failed to delete the lock file: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/backend/remote-state/s3/client.go","lineNumber":553,"sourceCode":"\tlockInfo := &statemgr.LockInfo{}\n\tif err := json.Unmarshal(data, lockInfo); err != nil {\n\t\treturn fmt.Errorf(\"failed to unmarshal JSON data into LockInfo struct: %w\", err)\n\t}\n\tlockErr.Info = lockInfo\n\n\t// Verify that the provided lock ID matches the lock ID of the retrieved lock file.\n\tif lockInfo.ID != id {\n\t\treturn fmt.Errorf(\"lock ID '%s' does not match the existing lock ID '%s'\", id, lockInfo.ID)\n\t}\n\n\t// Delete the lock file to release the lock.\n\t_, err = c.s3Client.DeleteObject(ctx, &s3.DeleteObjectInput{\n\t\tBucket: aws.String(c.bucketName),\n\t\tKey:    aws.String(c.lockFilePath),\n\t})\n\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to delete the lock file: %w\", err)\n\t}\n\n\tlog.Debug(fmt.Sprintf(\"Deleted lock file: '%q'\", c.lockFilePath))\n\n\treturn nil\n}\n\nfunc (c *RemoteClient) unlockWithDynamoDB(ctx context.Context, id string, lockErr *statemgr.LockError) error {\n\t// TODO: store the path and lock ID in separate fields, and have proper\n\t// projection expression only delete the lock if both match, rather than\n\t// checking the ID from the info field first.\n\tlockInfo, err := c.getLockInfoWithDynamoDB(ctx)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to retrieve lock info for lock ID %q: %s\", id, err)\n\t}\n\tlockErr.Info = lockInfo\n\n\tif lockInfo.ID != id {","sourceCodeStart":535,"sourceCodeEnd":571,"githubUrl":"https://github.com/hashicorp/terraform/blob/d32a084675427f5ac3f7d2868578ef8b2c1dc525/internal/backend/remote-state/s3/client.go#L535-L571","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"# principal could read but not delete — remove directly and fix IAM\naws s3api delete-object --bucket tf-state-prod --key prod/terraform.tflock.tflock\n# then grant s3:DeleteObject on the lock key in the IAM policy","handlingStrategy":"retry","validationCode":"// Validate DeleteObject permission via a dry-run-ish check: confirm IAM via the simulator is ideal;\n// here, confirm the bucket/key is delete-reachable (absence of legal hold).\nfunc deletable(ctx context.Context, c *s3.Client, bucket, lockKey string) error {\n  if _, err := c.GetObjectLegalHold(ctx, &s3.GetObjectLegalHoldInput{Bucket: &bucket, Key: &lockKey}); err != nil {\n    var apiErr smithy.APIError\n    if errors.As(err, &apiErr) && apiErr.ErrorCode() == \"NoSuchObjectLockConfiguration\" { return nil }\n    return err\n  }\n  return errors.New(\"legal hold is ON; delete will fail\")\n}","typeGuard":null,"tryCatchPattern":"// Retry the delete once for transient errors; surface AccessDenied/ObjectLocked distinctly.\nif _, err := c.s3Client.DeleteObject(ctx, delInput); err != nil {\n  var apiErr smithy.APIError\n  if errors.As(err, &apiErr) {\n    switch apiErr.ErrorCode() {\n    case \"AccessDenied\", \"ObjectLocked\":\n      return fmt.Errorf(\"failed to delete the lock file (%s): %w; check IAM/legal-hold\", apiErr.ErrorCode(), err)\n    }\n  }\n  // transient — retry once\n  if _, err2 := c.s3Client.DeleteObject(ctx, delInput); err2 != nil {\n    return fmt.Errorf(\"failed to delete the lock file: %w\", err2)\n  }\n}","preventionTips":["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."],"tags":["locking","s3","remote-state","deleteobject","iam","object-lock","unlock"],"backgroundTag":null,"analyzedSha":"d32a084675427f5ac3f7d2868578ef8b2c1dc525","analyzedAt":"2026-08-11T18:43:52.779Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}