diff --git a/.changes/v1.15/ENHANCEMENTS-20260226-102105.yaml b/.changes/v1.15/ENHANCEMENTS-20260226-102105.yaml new file mode 100644 index 0000000000..93936a0662 --- /dev/null +++ b/.changes/v1.15/ENHANCEMENTS-20260226-102105.yaml @@ -0,0 +1,5 @@ +kind: ENHANCEMENTS +body: 'backend/s3: Improved performance when using workspaces by replacing workspace listing with a direct state file check' +time: 2026-02-26T10:21:05.578904Z +custom: + Issue: "33137" diff --git a/internal/backend/remote-state/s3/backend_state.go b/internal/backend/remote-state/s3/backend_state.go index 35d1481e72..a180781e46 100644 --- a/internal/backend/remote-state/s3/backend_state.go +++ b/internal/backend/remote-state/s3/backend_state.go @@ -193,18 +193,10 @@ func (b *Backend) StateMgr(name string) (statemgr.Full, tfdiags.Diagnostics) { // If we need to force-unlock, but for some reason the state no longer // exists, the user will have to use aws tools to manually fix the // situation. - existing, wDiags := b.Workspaces() - diags = diags.Append(wDiags) - if wDiags.HasErrors() { - return nil, diags - } - - exists := false - for _, s := range existing { - if s == name { - exists = true - break - } + ctx := context.TODO() + exists, err := client.Exists(ctx) + if err != nil { + return nil, diags.Append(err) } // We need to create the object so it's listed by States. diff --git a/internal/backend/remote-state/s3/client.go b/internal/backend/remote-state/s3/client.go index 66a9db6fe2..88266e9d80 100644 --- a/internal/backend/remote-state/s3/client.go +++ b/internal/backend/remote-state/s3/client.go @@ -187,6 +187,31 @@ func (c *RemoteClient) get(ctx context.Context) (*remote.Payload, error) { return payload, nil } +func (c *RemoteClient) Exists(ctx context.Context) (bool, error) { + headInput := &s3.HeadObjectInput{ + Bucket: aws.String(c.bucketName), + Key: aws.String(c.path), + } + if c.serverSideEncryption && c.customerEncryptionKey != nil { + headInput.SSECustomerKey = aws.String(base64.StdEncoding.EncodeToString(c.customerEncryptionKey)) + headInput.SSECustomerAlgorithm = aws.String(s3EncryptionAlgorithm) + headInput.SSECustomerKeyMD5 = aws.String(c.getSSECustomerKeyMD5()) + } + + _, err := c.s3Client.HeadObject(ctx, headInput) + if err != nil { + switch { + case IsA[*s3types.NoSuchBucket](err): + return false, fmt.Errorf(errS3NoSuchBucket, c.bucketName, err) + case IsA[*s3types.NotFound](err): + return false, nil + } + return false, fmt.Errorf("Unable to access object %q in S3 bucket %q: %w", c.path, c.bucketName, err) + } + + return true, nil +} + func (c *RemoteClient) Put(data []byte) tfdiags.Diagnostics { var diags tfdiags.Diagnostics return diags.Append(c.put(data))