hashicorp/terraform · error
unable to retrieve file from S3 bucket
Error message
unable to retrieve file from S3 bucket '%s' with key '%s': %w
What it means
Thrown inside unlockWithFile when the S3 GetObject for the `.tflock` lock file fails. Before deleting the lock file, Terraform reads it to verify the lock ID matches and to populate lockErr.Info with the lock holder's details. If the read itself fails, the unlock aborts before the DeleteObject.
Solutions
- Check whether the lock file still exists: `aws s3api head-object --bucket <bucket> --key <path>.tflock` — if it 404s, the lock is effectively clear and the error is benign.
- Confirm s3:GetObject is granted on the lock key for the principal (the unlock needs read-then-delete).
- If using SSE-C, supply the identical customer key used at lock time (customer_encryption_key backend attribute).
- If the bucket or key path is wrong, correct the backend config and re-run force-unlock.
- If the file genuinely should exist, inspect the wrapped AWS error code to resolve the GetObject failure.
Example fix
# benign case — file already gone; confirm and clear DDB lock only aws s3api head-object --bucket tf-state-prod --key prod/terraform.tflock.tflock || \ echo "lock file absent; S3 side is clear"
Defensive patterns
Strategy: validation
Validate before calling
// Before unlock, check the lock file exists; treat NotFound as 'already clear'.
func lockFileExists(ctx context.Context, c *s3.Client, bucket, lockKey string) (bool, error) {
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 false, nil }
return false, err
}
return true, nil
} Try / catch
// Treat NoSuchKey as already-unlocked (benign); only hard-fail on other errors.
_, err := c.s3Client.GetObject(ctx, getInput)
if err != nil {
var nsk *s3types.NoSuchKey
var apiErr smithy.APIError
switch {
case errors.As(err, &nsk):
return nil // lock file already gone
case errors.As(err, &apiErr) && apiErr.ErrorCode() == "NotFound":
return nil
default:
return fmt.Errorf("unable to retrieve file from S3 bucket '%s' with key '%s': %w", c.bucketName, c.lockFilePath, err)
}
} Prevention
- Grant s3:GetObject on the `.tflock` key in the apply role.
- Keep SSE-C customer keys stable so encrypted lock files remain readable.
- Use `terraform force-unlock <id>` which surfaces a clear lock-ID rather than guessing.
- Avoid concurrent force-unlocks that delete the lock file out from under each other.
When it happens
Trigger: c.s3Client.GetObject at client.go:520 returns an error. Triggers: the lock file was already deleted (NoSuchKey — common after a concurrent force-unlock), the bucket no longer exists, AccessDenied on s3:GetObject for the lock key, or SSE-C customer key mismatch causing decryption failure on an encrypted lock file.
Common situations: Concurrent force-unlock already removed the file, a bucket policy denies GetObject but allows DeleteObject (asymmetric), SSE-C customer key changed between lock and unlock, or the wrong bucket/key path is configured so the lock file is absent.
Related errors
- failed to delete the lock file
- failed to unlock S3
- failed to read the body of the S3 object
- failed to retrieve lock info for lock ID
- failed to unlock both S3 and DynamoDB: S3 error
AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11).
Data as JSON: /api/errors/7983453f7cf0e22e.
Report an issue: GitHub.
Appendix: source
Thrown at internal/backend/remote-state/s3/client.go:522
//
// 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 {
getInput := &s3.GetObjectInput{
Bucket: aws.String(c.bucketName),
Key: aws.String(c.lockFilePath),
}
if c.serverSideEncryption && c.customerEncryptionKey != nil {
getInput.SSECustomerKey = aws.String(base64.StdEncoding.EncodeToString(c.customerEncryptionKey))
getInput.SSECustomerAlgorithm = aws.String(s3EncryptionAlgorithm)
getInput.SSECustomerKeyMD5 = aws.String(c.getSSECustomerKeyMD5())
}
getOutput, err := c.s3Client.GetObject(ctx, getInput)
if err != nil {
return fmt.Errorf("unable to retrieve file from S3 bucket '%s' with key '%s': %w", c.bucketName, c.lockFilePath, err)
}
defer func() {
if cerr := getOutput.Body.Close(); cerr != nil {
log.Warn(fmt.Sprintf("failed to close S3 object body: %v", cerr))
}
}()
data, err := io.ReadAll(getOutput.Body)
if err != nil {
return fmt.Errorf("failed to read the body of the S3 object: %w", err)
}
lockInfo := &statemgr.LockInfo{}
if err := json.Unmarshal(data, lockInfo); err != nil {
return fmt.Errorf("failed to unmarshal JSON data into LockInfo struct: %w", err)
}
lockErr.Info = lockInfo
View on GitHub (pinned to d32a084675)