Skip to content

fix(state): resolve state bucket region in exports index store - #824

Merged
go-to-k merged 2 commits into
mainfrom
fix/819-exports-index-bucket-region
Jun 13, 2026
Merged

fix(state): resolve state bucket region in exports index store#824
go-to-k merged 2 commits into
mainfrom
fix/819-exports-index-bucket-region

Conversation

@go-to-k

@go-to-k go-to-k commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Summary

The exports index store (ExportIndexStore, used for Fn::ImportValue cross-stack reference tracking) used an S3 client pinned to the CLI base region, so it hit 301 PermanentRedirect ("must be addressed using the specified endpoint; continuing without index update") when the state bucket lived in another region. This is the same class as #803 (just fixed for LockManager) — surfaced by the automated cross-region-state-bucket integ added in #803's PR.

Fix (mirrors #803 / #60)

Ported the ensureClientForBucket() pattern into src/state/export-index-store.ts (contained — no ripple to the 4 call sites): before readIndexRaw / writeIndex, resolve the bucket region via the process-wide-cached resolveBucketRegion (shares the backend's/lock manager's GetBucketLocation), and if it differs, build a private region-corrected S3Client reusing caller credentials, without destroying the shared AwsClients.s3. Memoized + single-flight; degrades gracefully for test doubles lacking config.region().

Test plan

  • Unit (tests/unit/state/export-index-store.test.ts): 301→rebuild→success for index read + write (original client never used after rebuild); region resolved once (cached); existing 17 tests untouched.
  • Real-AWS (cross-region-state-bucket, extended with a CfnOutput + exportName so the index write/remove path runs): deploy + destroy against a temporary us-west-2 bucket with AWS_REGION=us-east-1PASS, 1 deleted, 0 errors, the log shows [ExportIndexStore] ... building a region-corrected S3 client for index operations, the _index/{region}/exports.json object is written, and verify.sh asserts no 301 warning on deploy or destroy. Temp bucket cleaned.

Independent review

Code review clean (1 nit: this is the 3rd copy of ensureClientForBucket — extracting a shared rebuildClientForBucketRegion helper across S3StateBackend / LockManager / ExportIndexStore is a reasonable future cleanup, out of scope here).

Closes #819

go-to-k added 2 commits June 13, 2026 15:20
PR #803 fixed LockManager to resolve a cross-region state bucket's actual
region via GetBucketLocation before any S3 op. The automated
cross-region-state-bucket integ then surfaced that the exports index store
(Fn::ImportValue cross-stack reference tracking, writes
_index/{region}/exports.json) had the same unfixed bug: its S3 client was
pinned to the CLI base region, so against a bucket in another region every
index write (after deploy) and remove (after destroy) hit S3's 301
PermanentRedirect, logged as "Exports index ... failed (non-retryable): ...
must be addressed using the specified endpoint; continuing without index
update". Non-fatal by design (the canonical state.json is written through
the already-region-corrected S3StateBackend and the index self-heals), so
the run passed while the cross-region index was silently never maintained.

Port the LockManager.ensureClientForBucket() pattern into ExportIndexStore:
before its first S3 read (readIndexRaw) or write (writeIndex) it resolves the
bucket's region (cached process-wide via resolveBucketRegion, so it shares
the state backend's / lock manager's existing GetBucketLocation call) and,
if it differs from the supplied client's region, builds a private
replacement S3Client for that region. The replacement reuses the caller's
resolved credentials (so --profile / static creds carry over without
threading client options through the four store call sites) and does NOT
destroy the shared AwsClients.s3 instance other components still hold. The
resolution is memoized + single-flight, and degrades gracefully for a test
double whose client lacks the SDK config.region() shape. Contained inside
the store, with no ripple to deploy.ts / destroy.ts / state.ts /
local-state-loader.ts.

Tests: 4 new unit tests (removeStack + updateForStack succeed through a
region-corrected client when the bucket region differs; no rebuild when the
resolved region matches; region resolved exactly once across multiple ops).
Integ: the cross-region-state-bucket fixture stack now publishes a
CloudFormation Output with an Export.Name (an export-less stack
short-circuits the index write), and verify.sh greps the deploy + destroy
output to assert the exports-index 301 warning is gone on both paths and
that _index/{region}/exports.json was written to the cross-region bucket on
deploy. New scenario tag exports-index-region-resolve.

Closes #819
@go-to-k
go-to-k force-pushed the fix/819-exports-index-bucket-region branch from 9a6d8a7 to c1cdac0 Compare June 13, 2026 06:21
@go-to-k
go-to-k merged commit 663f6bd into main Jun 13, 2026
5 checks passed
@go-to-k
go-to-k deleted the fix/819-exports-index-bucket-region branch June 13, 2026 06:25
github-actions Bot pushed a commit that referenced this pull request Jun 13, 2026
## [0.220.4](v0.220.3...v0.220.4) (2026-06-13)

### Bug Fixes

* **state:** resolve state bucket region in exports index store ([#824](#824)) ([663f6bd](663f6bd))
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 0.220.4 🎉

The release is available on:

Your semantic-release bot 📦🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(state): exports index store does not resolve the bucket region — 301 PermanentRedirect on cross-region destroy (same class as #803)

1 participant