{"record":{"id":"1df679d7381be333","repo":"hashicorp/terraform","slug":"failed-to-unlock-both-s3-and-dynamodb-s3-error","errorCode":null,"errorMessage":"failed to unlock both S3 and DynamoDB: S3 error: %v, DynamoDB error: %v","messagePattern":"failed to unlock both S3 and DynamoDB: S3 error: (.+?), DynamoDB error: (.+?)","errorType":"exception","errorClass":"statemgr.LockError","httpStatus":null,"severity":"error","filePath":"internal/backend/remote-state/s3/client.go","lineNumber":485,"sourceCode":"\tif !c.useLockFile && c.ddbTable != \"\" {\n\t\tlog.Info(\"Attempting to unlock remote state (DynamoDB only)...\")\n\t\tif err := c.unlockWithDynamoDB(ctx, id, lockErr); err != nil {\n\t\t\tlockErr.Err = err\n\t\t\treturn lockErr\n\t\t}\n\n\t\tlog.Info(\"Unlocked remote state (DynamoDB only)\")\n\t\treturn nil\n\t}\n\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.","sourceCodeStart":467,"sourceCodeEnd":503,"githubUrl":"https://github.com/hashicorp/terraform/blob/c9def3e214014c1188faabfc4a5bde5095139765/internal/backend/remote-state/s3/client.go#L467-L503","documentation":"Thrown by RemoteClient.Unlock when both S3 lock-file deletion (unlockWithFile) AND DynamoDB lock-item deletion (unlockWithDynamoDB) fail simultaneously while running in the dual-locking mode (useLockFile=true and ddbTable set). The wrapped message concatenates the two underlying AWS errors so the operator can see why each backend rejected the delete. Because the lock is held by two independent stores, a partial infrastructure outage or permission scope can cause both to fail at once.","triggerScenarios":"Calling Unlock(id) on an S3 backend configured with both useLockFile=true and a dynamodb_table, while the IAM principal lacks s3:DeleteObject on the .tflock key AND dynamodb:DeleteItem on the table, or while both AWS services are unreachable (e.g. network partition, expired STS credentials).","commonSituations":"Running 'terraform force-unlock' or an apply/destroy cleanup after the IAM policy was tightened, an STS session expired mid-operation, a bucket policy started denying the principal, or a shared CI role lost access to one of the two resources. Also seen during cross-account setups where one role has S3 access but not DynamoDB.","solutions":["Inspect the wrapped 'S3 error' and 'DynamoDB error' substrings to identify which AWS API failed for each store and address the more actionable one first.","Verify the running credentials (aws sts get-caller-identity) and confirm they have both s3:GetObject+DeleteObject on the lock key and dynamodb:GetItem+DeleteItem on the table.","Re-run the operation once the IAM/credential issue is fixed; the lock file and item still exist so Unlock can retry.","If the lock is stuck and credentials are correct, manually delete the S3 <lockFilePath> object and the DynamoDB row with LockID = <bucket>/<path> using aws-cli."],"exampleFix":"// before: principal lacks dynamodb:DeleteItem\n// after: IAM policy includes\n{\n  \"Effect\": \"Allow\",\n  \"Action\": [\"dynamodb:DeleteItem\", \"dynamodb:GetItem\"],\n  \"Resource\": \"arn:aws:dynamodb:*:*:table/my-lock-table\"\n}","handlingStrategy":"try-catch","validationCode":"// Pre-flight: confirm principal can reach and write both lock stores\nimport (\n  \"github.com/aws/aws-sdk-go-v2/service/s3\"\n  \"github.com/aws/aws-sdk-go-v2/service/dynamodb\"\n)\n\nfunc preflightLockStores(ctx context.Context, s3c *s3.Client, ddbs *dynamodb.Client, bucket, lockKey, table, lockID string) error {\n  if _, err := s3c.HeadObject(ctx, &s3.HeadObjectInput{Bucket: &bucket, Key: &lockKey}); err != nil {\n    return fmt.Errorf(\"s3 preflight: %w\", err)\n  }\n  if _, err := ddbs.DescribeTable(ctx, &dynamodb.DescribeTableInput{TableName: &table}); err != nil {\n    return fmt.Errorf(\"dynamodb preflight: %w\", err)\n  }\n  return nil\n}","typeGuard":null,"tryCatchPattern":"// Unlock can return a *statemgr.LockError; inspect .Err and .Info\nimport \"github.com/hashicorp/terraform/statemgr\"\n\nerr := client.Unlock(id)\nif le, ok := err.(*statemgr.LockError); ok {\n    log.Printf(\"unlock failed: %v (held by %s)\", le.Err, le.Info.Who)\n    // branch on wrapped error codes before retrying\n}","preventionTips":["Scope a single IAM policy to grant both s3 and dynamodb lock actions so neither side fails in isolation.","Run a pre-apply preflight that HeadObjects the lock file and DescribeTables the lock table.","Use the same region/endpoint for S3 and DynamoDB to avoid split-permission failures.","Automate force-unlock only after confirming the lock ID matches the stored one."],"tags":["s3","dynamodb","terraform-state","locking","iam","aws"],"analyzedSha":"c9def3e214014c1188faabfc4a5bde5095139765","analyzedAt":"2026-08-07T15:39:49.278Z","schemaVersion":2},"datasetVersion":"2026-08-07T20:17:04.800Z"}