hashicorp/terraform · error
failed to clean up file lock after DynamoDB lock error
Error message
failed to clean up file lock after DynamoDB lock error: %v; original error: %w
What it means
Only reachable when BOTH S3 native file locking and DynamoDB locking are enabled (useLockFile=true and ddbTable set). Terraform acquires the file lock first, then the DynamoDB lock; if the DynamoDB lock fails it attempts to roll back (release) the file lock. This error means the DynamoDB lock acquisition failed AND the compensating file-unlock also failed, so the system is left holding an S3 lock with no DynamoDB lock — the rollback itself broke.
Solutions
- Run `terraform force-unlock <id>` with the lock ID shown in the DynamoDB failure to clear the orphaned S3 lock file.
- Inspect the S3 lock file directly: `aws s3api get-object --bucket <bucket> --key <path>.tflock -` to confirm which client owns it and whether the ID matches.
- Verify the SSE-C customer key is stable across operations if customer_encryption_key is set — it must be identical for lock and unlock.
- Confirm the IAM principal still has s3:GetObject + s3:DeleteObject on the lock key at unlock time (a policy change mid-run is a common cause).
- If the DynamoDB lock was the original failure, address that first (it is the %w root cause); clearing DDB may be needed too.
Example fix
# clear the orphaned S3 lock file left behind by the failed rollback terraform force-unlock <lock-id> # or manually delete the .tflock object if the ID is unknown aws s3api delete-object --bucket tf-state-prod --key prod/terraform.tflock.tflock
Defensive patterns
Strategy: retry
Validate before calling
// Validate symmetric access (lock + unlock) before enabling dual locking.
func validateDualLockAccess(ctx context.Context, s3c *s3.Client, ddb *dynamodb.Client, bucket, lockKey, table string) error {
if _, err := s3c.HeadObject(ctx, &s3.HeadObjectInput{Bucket: &bucket, Key: &lockKey}); err != nil {
// NoSuchKey is fine; AccessDenied is not
var apiErr smithy.APIError
if errors.As(err, &apiErr) && apiErr.ErrorCode() != "NotFound" { return err }
}
if _, err := ddb.DescribeTable(ctx, &dynamodb.DescribeTableInput{TableName: &table}); err != nil {
return err
}
return nil
} Try / catch
// On rollback failure, surface BOTH errors and emit the lock ID so force-unlock is possible.
if unlockErr := c.unlockWithFile(ctx, info.ID, &statemgr.LockError{}, log); unlockErr != nil {
return "", fmt.Errorf(
"failed to clean up file lock after DynamoDB lock error: %v; original error: %w; " +
"run `terraform force-unlock %s` to recover", unlockErr, err, info.ID)
} Prevention
- Avoid enabling both use_lockfile and dynamodb_table unless you need belt-and-suspenders locking — each adds a failure surface.
- Keep IAM symmetric: the role needs Get+Put+Delete on the S3 lock key AND Put+Get+Delete on the DDB row.
- Never rotate SSE-C customer keys between lock and unlock within a single apply.
- Educate operators to use `terraform force-unlock <id>` rather than manually deleting locks.
When it happens
Trigger: lockWithDynamoDB at client.go:333 fails AND the cleanup unlockWithFile at client.go:335 also fails. Triggers: DynamoDB ConditionalCheckFailed (someone else holds the DDB lock) combined with an S3 problem during cleanup (GetObject on the lock file fails, permissions revoked mid-run, or the lock file was concurrently deleted by another client).
Common situations: Two operators applying simultaneously where the loser hits DDB ConditionalCheckFailed and then a transient S3 blunder prevents cleanup; IAM permissions narrowed between the lock and unlock steps; SSE-C key rotated between operations so the cleanup GetObject fails decryption.
Related errors
- failed to unlock both S3 and DynamoDB: S3 error
- failed to delete the lock file
- failed to read the body of the S3 object
- failed to retrieve lock info for lock ID
- failed to store state MD5
AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11).
Data as JSON: /api/errors/9ea3c904700c8109.
Report an issue: GitHub.
Appendix: source
Thrown at internal/backend/remote-state/s3/client.go:336
log.Info("Attempting to lock remote state (DynamoDB only)...")
if err := c.lockWithDynamoDB(ctx, info); err != nil {
return "", err
}
log.Info("Locked remote state (DynamoDB only)")
return info.ID, nil
}
// double locking: dynamodb + file (design decision: both must succeed)
log.Info("Attempting to lock remote state (S3 Native and DynamoDB)...")
if err := c.lockWithFile(ctx, info, log); err != nil {
return "", err
}
if err := c.lockWithDynamoDB(ctx, info); err != nil {
// Release the file lock if attempting to acquire the DynamoDB lock fails.
if unlockErr := c.unlockWithFile(ctx, info.ID, &statemgr.LockError{}, log); unlockErr != nil {
return "", fmt.Errorf("failed to clean up file lock after DynamoDB lock error: %v; original error: %w", unlockErr, err)
}
return "", err
}
log.Info("Locked remote state (S3 Native and DynamoDB)")
return info.ID, nil
}
// lockWithFile attempts to acquire a lock on the remote state by uploading a lock file to Amazon S3.
//
// This method is used when the S3 native locking mechanism is in use. It uploads a lock file (JSON)
// to an S3 bucket to establish a lock on the state file. If the lock file does not already
// exist, the operation will succeed, acquiring the lock. If the lock file already exists, the operation
// will fail due to a conditional write, indicating that the lock is already held by another Terraform client.
func (c *RemoteClient) lockWithFile(ctx context.Context, info *statemgr.LockInfo, log hclog.Logger) error {
lockFileJson, err := json.Marshal(info)
if err != nil {View on GitHub (pinned to d32a084675)