{"record":{"id":"9ea3c904700c8109","repo":"hashicorp/terraform","slug":"failed-to-clean-up-file-lock-after-dynamodb-lock-e","errorCode":null,"errorMessage":"failed to clean up file lock after DynamoDB lock error: %v; original error: %w","messagePattern":"failed to clean up file lock after DynamoDB lock error: (.+?); original error: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/backend/remote-state/s3/client.go","lineNumber":336,"sourceCode":"\t\tlog.Info(\"Attempting to lock remote state (DynamoDB only)...\")\n\t\tif err := c.lockWithDynamoDB(ctx, info); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tlog.Info(\"Locked remote state (DynamoDB only)\")\n\t\treturn info.ID, nil\n\t}\n\n\t// double locking: dynamodb + file (design decision: both must succeed)\n\tlog.Info(\"Attempting to lock remote state (S3 Native and DynamoDB)...\")\n\tif err := c.lockWithFile(ctx, info, log); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif err := c.lockWithDynamoDB(ctx, info); err != nil {\n\t\t// Release the file lock if attempting to acquire the DynamoDB lock fails.\n\t\tif unlockErr := c.unlockWithFile(ctx, info.ID, &statemgr.LockError{}, log); unlockErr != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to clean up file lock after DynamoDB lock error: %v; original error: %w\", unlockErr, err)\n\t\t}\n\n\t\treturn \"\", err\n\t}\n\n\tlog.Info(\"Locked remote state (S3 Native and DynamoDB)\")\n\treturn info.ID, nil\n}\n\n// lockWithFile attempts to acquire a lock on the remote state by uploading a lock file to Amazon S3.\n//\n// This method is used when the S3 native locking mechanism is in use. It uploads a lock file (JSON)\n// to an S3 bucket to establish a lock on the state file. If the lock file does not already\n// exist, the operation will succeed, acquiring the lock. If the lock file already exists, the operation\n// will fail due to a conditional write, indicating that the lock is already held by another Terraform client.\nfunc (c *RemoteClient) lockWithFile(ctx context.Context, info *statemgr.LockInfo, log hclog.Logger) error {\n\tlockFileJson, err := json.Marshal(info)\n\tif err != nil {","sourceCodeStart":318,"sourceCodeEnd":354,"githubUrl":"https://github.com/hashicorp/terraform/blob/c9def3e214014c1188faabfc4a5bde5095139765/internal/backend/remote-state/s3/client.go#L318-L354","documentation":"Thrown in RemoteClient.Lock() (s3/client.go:336) in the double-locking mode (useLockFile=true AND a dynamodb_table is configured). lockWithFile succeeded (S3 .tflock created) but lockWithDynamoDB failed, and the compensating unlockWithFile ALSO failed. The error bundles both: the cleanup failure (%v) and the original DynamoDB lock error (%w). A lock may be left in an inconsistent state.","triggerScenarios":"S3 lock file created, then DynamoDB PutItem failed (table missing/misspelled, ConditionalCheckFailedException, dynamodb:PutItem denied), and the subsequent S3 delete of the .tflock also failed (s3:DeleteObject denied, network error).","commonSituations":"Misconfigured dynamodb_table name combined with insufficient S3 delete permissions; partial AWS outage; IAM policy allows PutObject but not DeleteObject on the lock key.","solutions":["Manually delete the stale <key>.tflock object in S3 and the DynamoDB LockID row if one was created.","Fix the dynamodb_table name/permissions and the S3 delete permission, then retry.","Run `terraform force-unlock` if Terraform still reports the state as locked.","Consider using a single locking mechanism (file-only or DynamoDB-only) to avoid the double-lock cleanup path."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// Pre-check both lock destinations are writable before locking\n// _, ferr := s3Client.HeadObject(ctx, &s3.HeadObjectInput{Bucket:&bucket, Key:aws.String(lockFilePath)})\n// _, derr := dynClient.DescribeTable(ctx, &dynamodb.DescribeTableInput{TableName:aws.String(ddbTable)})","typeGuard":"// The wrapped %w is the original DynamoDB lock error (often a *statemgr.LockError)\n// var lockErr *statemgr.LockError\n// if errors.As(err, &lockErr) { /* inspect lockErr.Info / Err */ }","tryCatchPattern":"// On double-lock cleanup failure, force manual cleanup rather than retrying blindly\n// _, err := client.Lock(info)\n// if err != nil && strings.Contains(err.Error(), \"failed to clean up file lock\") {\n//   // delete stale .tflock + DynamoDB row, then force-unlock\n// }","preventionTips":["Prefer a single locking mechanism (file-only or DynamoDB-only) to avoid the cleanup path.","Ensure the role has both s3:PutObject/DeleteObject and dynamodb:PutItem/DeleteItem.","Verify the dynamodb_table name exists before enabling double locking."],"tags":["s3","locking","dynamodb","cascading","cleanup"],"analyzedSha":"c9def3e214014c1188faabfc4a5bde5095139765","analyzedAt":"2026-08-07T15:39:49.278Z","schemaVersion":2},"datasetVersion":"2026-08-07T21:17:07.882Z"}