hashicorp/terraform · error

unable to retrieve file from S3 bucket '%s' with key '%s': %

Error message

unable to retrieve file from S3 bucket '%s' with key '%s': %w

What it means

Thrown inside unlockWithFile when S3 GetObject on the lock file (.tflock) fails before the file can be read or deleted. The message reports bucket, key, and the underlying AWS error, so the operator can tell whether it is a permissions, missing-object, or connectivity problem. This blocks the S3 side of the unlock flow.

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 c9def3e214)

Solutions

  1. aws s3api get-object --bucket <bucket> --key <lockFilePath> to reproduce and read the exact AWS error code.
  2. If NoSuchKey, the lock is effectively released; re-run the Terraform operation.
  3. Confirm IAM s3:GetObject permission and the correct region/endpoint for the bucket.
  4. For SSE-C, ensure the customer key matches; for SSE-KMS, ensure kms:Decrypt is granted.

Example fix

// before: missing s3:GetObject on the lock path
// after
{
  "Effect": "Allow",
  "Action": "s3:GetObject",
  "Resource": "arn:aws:s3:::my-state-bucket/*.tflock"
}
Defensive patterns

Strategy: validation

Validate before calling

// Head the lock file before attempting unlock to surface a clean error
func lockFileExists(ctx context.Context, s3c *s3.Client, bucket, lockKey string) (bool, error) {
  if _, err := s3c.HeadObject(ctx, &s3.HeadObjectInput{Bucket: &bucket, Key: &lockKey}); err != nil {
    var nsk *types.NotFound
    if errors.As(err, &nsk) || strings.Contains(err.Error(), "NotFound") {
      return false, nil
    }
    return false, err
  }
  return true, nil
}

Try / catch

// Differentiate missing (benign) from real errors
import "github.com/aws/smithy-go"

var ae smithy.APIError
if errors.As(err, &ae) && ae.ErrorCode() == "NotFound" {
    // lock already gone
} else {
    // real failure, remediate
}

Prevention

When it happens

Trigger: Calling Unlock -> unlockWithFile where c.s3Client.GetObject returns an error for bucket c.bucketName and key c.lockFilePath: NoSuchKey, NoSuchBucket, AccessDenied, InvalidObjectName, or a transient networking/STS failure.

Common situations: Lock file already removed by a concurrent unlock or manual cleanup; bucket deleted or renamed; SSE-C key mismatch; wrong region configured for the bucket; or KMS key disabled so GetObject is denied.

Related errors


AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07). Data as JSON: /api/errors/7983453f7cf0e22e. Report an issue: GitHub.