{"record":{"id":"bfb2cc9fdc189067","repo":"hashicorp/terraform","slug":"failed-to-unlock-dynamodb-v","errorCode":null,"errorMessage":"failed to unlock DynamoDB: %v","messagePattern":"failed to unlock DynamoDB: (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/backend/remote-state/s3/client.go","lineNumber":495,"sourceCode":"\n\t// Double unlocking: DynamoDB + file\n\tlog.Info(\"Attempting to unlock remote state (S3 Native and DynamoDB)...\")\n\n\tferr := c.unlockWithFile(ctx, id, lockErr, log)\n\tderr := c.unlockWithDynamoDB(ctx, id, lockErr)\n\n\tif ferr != nil && derr != nil {\n\t\tlockErr.Err = fmt.Errorf(\"failed to unlock both S3 and DynamoDB: S3 error: %v, DynamoDB error: %v\", ferr, derr)\n\t\treturn lockErr\n\t}\n\n\tif ferr != nil {\n\t\tlockErr.Err = fmt.Errorf(\"failed to unlock S3: %v\", ferr)\n\t\treturn lockErr\n\t}\n\n\tif derr != nil {\n\t\tlockErr.Err = fmt.Errorf(\"failed to unlock DynamoDB: %v\", derr)\n\t\treturn lockErr\n\t}\n\n\tlog.Info(\"Unlocked remote state (S3 Native and DynamoDB)\")\n\treturn nil\n}\n\n// unlockWithFile attempts to unlock the remote state by deleting the lock file from Amazon S3.\n//\n// This method is used when the S3 native locking mechanism is in use, which uses a `.tflock` file\n// to manage state locking. The function deletes the lock file to release the lock, allowing other\n// Terraform clients to acquire the lock on the same state file.\nfunc (c *RemoteClient) unlockWithFile(ctx context.Context, id string, lockErr *statemgr.LockError, log hclog.Logger) error {\n\tgetInput := &s3.GetObjectInput{\n\t\tBucket: aws.String(c.bucketName),\n\t\tKey:    aws.String(c.lockFilePath),\n\t}\n","sourceCodeStart":477,"sourceCodeEnd":513,"githubUrl":"https://github.com/hashicorp/terraform/blob/d32a084675427f5ac3f7d2868578ef8b2c1dc525/internal/backend/remote-state/s3/client.go#L477-L513","documentation":"Dual-lock unlock where the S3 file unlock succeeded but the DynamoDB unlock (unlockWithDynamoDB) failed. The S3 `.tflock` is gone but the DynamoDB LockID row remains, so the state is half-unlocked and other clients may still see the DDB lock. lockErr carries the wrapped DynamoDB failure.","triggerScenarios":"unlockWithDynamoDB returns an error while unlockWithFile succeeded. Triggers: getLockInfoWithDynamoDB fails (GetItem error, table gone, AccessDenied), the DDB lock ID does not match the provided id, or the final DeleteItem on the DDB row fails (throttling, permissions revoked, table deleted between lock and unlock).","commonSituations":"DynamoDB table deleted or renamed after the lock was taken, IAM principal lost dynamodb:DeleteItem mid-run, provisioned-capacity throttling on the lock table, or a concurrent force-unlock already cleared the DDB row so getLockInfoWithDynamoDB returns no match.","solutions":["Manually delete the stale DynamoDB row: `aws dynamodb delete-item --table-name <table> --key '{\"LockID\":{\"S\":\"<bucket>/<path>\"}}'`.","Confirm the table still exists in the configured region/profile: `aws dynamodb describe-table --table-name <table>`.","Verify dynamodb:GetItem + dynamodb:DeleteItem permissions on the table for the principal.","If throttling, switch the lock table to on-demand billing or raise write capacity, then retry force-unlock.","Re-run `terraform force-unlock <id>` once the underlying DDB access is restored."],"exampleFix":"# S3 lock cleared but DynamoDB row lingers — remove it\naws dynamodb delete-item \\\n  --table-name terraform-locks \\\n  --key '{\"LockID\":{\"S\":\"tf-state-prod/prod/terraform.tfstate\"}}'","handlingStrategy":"retry","validationCode":"// Validate DynamoDB lock-table access before unlock.\nfunc canUnlockDDB(ctx context.Context, c *dynamodb.Client, table, lockID string) error {\n  if _, err := c.DescribeTable(ctx, &dynamodb.DescribeTableInput{TableName: &table}); err != nil {\n    return err\n  }\n  return nil\n}","typeGuard":null,"tryCatchPattern":"// On DDB-only unlock failure, retry, then surface the lock ID + row key for manual cleanup.\nif derr != nil {\n  if err2 := c.unlockWithDynamoDB(ctx, id, lockErr); err2 == nil { derr = nil }\n}\nif derr != nil {\n  lockErr.Err = fmt.Errorf(\"failed to unlock DynamoDB: %v; delete row LockID=%s in table %s\", derr, lockPath, ddbTable)\n  return lockErr\n}","preventionTips":["Grant dynamodb:GetItem + dynamodb:DeleteItem on the lock table in the apply role.","Use on-demand billing or adequate write capacity to avoid throttling during unlock.","Run `terraform force-unlock <id>` after crashes to clear DDB rows promptly.","Never delete/recreate the lock table without coordinating with active applies."],"tags":["locking","dynamodb","remote-state","unlock","partial-failure","iam"],"backgroundTag":null,"analyzedSha":"d32a084675427f5ac3f7d2868578ef8b2c1dc525","analyzedAt":"2026-08-11T18:43:52.779Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}