hashicorp/terraform · error
failed to unlock S3
Error message
failed to unlock S3: %v
What it means
Dual-lock unlock where the DynamoDB unlock succeeded but the S3 file unlock (unlockWithFile) failed. The state is half-unlocked: the DynamoDB row is gone but the S3 `.tflock` object remains, so other Terraform clients will still be blocked by the S3 lock. lockErr is returned carrying the wrapped S3 failure.
Solutions
- Re-run `terraform force-unlock <id>`; if DynamoDB is already clear it will only need to clear the S3 file.
- Manually delete the lingering `.tflock` object: `aws s3api delete-object --bucket <bucket> --key <path>.tflock`.
- Verify s3:GetObject + s3:DeleteObject permissions on the lock key for the running principal.
- If SSE-C is configured, ensure the same customer key is supplied at unlock as at lock time.
- Inspect the wrapped S3 error to distinguish NoSuchKey (benign — already gone) from AccessDenied (fix IAM).
Example fix
# the DynamoDB lock cleared but the S3 .tflock object lingers — remove it aws s3api delete-object \ --bucket tf-state-prod \ --key 'prod/terraform.tflock.tflock'
Defensive patterns
Strategy: retry
Validate before calling
// Validate S3 lock-file access before unlock.
func canUnlockS3(ctx context.Context, c *s3.Client, bucket, lockKey string) error {
// needs both Get (to verify ID) and Delete
if _, err := c.HeadObject(ctx, &s3.HeadObjectInput{Bucket: &bucket, Key: &lockKey}); err != nil {
var apiErr smithy.APIError
if errors.As(err, &apiErr) && apiErr.ErrorCode() != "NotFound" { return err }
}
return nil
} Try / catch
// On S3-only unlock failure, retry once, then surface the lock ID for manual cleanup.
if ferr != nil {
// one retry for transient S3
if err2 := c.unlockWithFile(ctx, id, lockErr, log); err2 == nil { ferr = nil }
}
if ferr != nil {
lockErr.Err = fmt.Errorf("failed to unlock S3: %v; run `terraform force-unlock %s` or delete %s/%s.tflock", ferr, id, bucket, lockKey)
return lockErr
} Prevention
- Grant s3:GetObject + s3:DeleteObject on the `.tflock` key in the apply role.
- Avoid S3 Object Lock retention on the lock-key prefix, or exclude it via a prefix policy.
- Keep SSE-C customer keys stable across lock/unlock.
- Use `terraform force-unlock <id>` rather than manual deletion whenever possible.
When it happens
Trigger: unlockWithFile returns an error while unlockWithDynamoDB succeeded. Triggers inside unlockWithFile: GetObject on the lock file fails (NoSuchKey if already deleted, AccessDenied), the lock file body fails to read or parse, the lock ID in the file does not match the provided id, or the final DeleteObject fails.
Common situations: A concurrent force-unlock already deleted the lock file (NoSuchKey on GetObject), SSE-C key mismatch prevents reading the lock file, IAM lost s3:GetObject or s3:DeleteObject but still has DynamoDB rights, or the lock file was corrupted.
Related errors
- failed to delete the lock file
- failed to unlock DynamoDB
- unable to retrieve file from S3 bucket
- failed to read the body of the S3 object
- failed to retrieve lock info for lock ID
AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11).
Data as JSON: /api/errors/2b3e9bd1b858515b.
Report an issue: GitHub.
Appendix: source
Thrown at internal/backend/remote-state/s3/client.go:490
}
log.Info("Unlocked remote state (DynamoDB only)")
return nil
}
// Double unlocking: DynamoDB + file
log.Info("Attempting to unlock remote state (S3 Native and DynamoDB)...")
ferr := c.unlockWithFile(ctx, id, lockErr, log)
derr := c.unlockWithDynamoDB(ctx, id, lockErr)
if ferr != nil && derr != nil {
lockErr.Err = fmt.Errorf("failed to unlock both S3 and DynamoDB: S3 error: %v, DynamoDB error: %v", ferr, derr)
return lockErr
}
if ferr != nil {
lockErr.Err = fmt.Errorf("failed to unlock S3: %v", ferr)
return lockErr
}
if derr != nil {
lockErr.Err = fmt.Errorf("failed to unlock DynamoDB: %v", derr)
return lockErr
}
log.Info("Unlocked remote state (S3 Native and DynamoDB)")
return nil
}
// unlockWithFile attempts to unlock the remote state by deleting the lock file from Amazon S3.
//
// This method is used when the S3 native locking mechanism is in use, which uses a `.tflock` file
// to manage state locking. The function deletes the lock file to release the lock, allowing other
// Terraform clients to acquire the lock on the same state file.
func (c *RemoteClient) unlockWithFile(ctx context.Context, id string, lockErr *statemgr.LockError, log hclog.Logger) error {View on GitHub (pinned to d32a084675)