DocsAWS 101BlogServices

Multi-tenancy

MiniStack is a single process, but each request is tagged with an account ID and region. State is partitioned per account — giving you LocalStack-Pro-style namespace isolation without spinning up multiple emulators.

Account inference

MiniStack picks an account ID from (in order):

  • Access key ID — if the request carries a SigV4 Authorization header and the access key is a 12-digit string, that's the account ID. Lets you drive separate accounts with e.g. AWS_ACCESS_KEY_ID=111111111111 vs 222222222222.
  • Custom tokens with an embedded account — some tooling passes session tokens that include the account.
  • Default000000000000 (the historical LocalStack default). Some code paths still use 123456789012; both normalize to the same pool under AccountScopedDict.
No signature check. MiniStack does not validate SigV4 signatures. The access key is read only to derive the account — any string works.

Regions

The region comes from the SigV4 credential scope (.../us-east-1/s3/aws4_request). Unsigned callers fall back to MINISTACK_REGION (default us-east-1).

Regional ARNs, endpoint URLs, and STS GetCallerIdentity responses all honor the per-request region. Two callers signing with different regions from the same process get region-correct responses — no single global region setting is used at request time.

Since 1.4.0, state is additionally isolated per region. As of 1.5.4 every regional service is region-isolated except S3 and Aurora DSQL — two clients signing with different regions see fully independent resources, and cross-region ARN references return the same errors real AWS returns. IAM, STS, CloudFront, Route 53, and Organizations are global services in AWS, so their state is account-scoped and shared across regions by design — that is correct, not a pending gap. For S3 and Aurora DSQL, use unique resource names if you exercise two regions within one account. (The per-release rollout, 1.4.0 through 1.4.12, is in the changelog.)

AccountScopedDict

AccountScopedDict (in ministack/core/) is a dict wrapper that transparently partitions keys by the current request's account ID. A service module writes self._tables[table_name] = … as if it's a flat dict; the wrapper stores it under (account_id, table_name). When account 111…111 reads _tables, it sees only its own entries.

# Inside a service module
self._queues = AccountScopedDict()
self._queues[name] = queue_data   # scoped to request.account_id automatically

The request's account is set via a context-local (set_request_account_id()) at the top of the router, so handlers can stay oblivious.

Leaky services

The account leaks tracked here in earlier releases (CloudWatch metrics, EventBridge internal maps, the SES message store, and ElastiCache cluster tracking) were all resolved by the 1.4.x account- and region-isolation work — each now uses an account- and region-scoped store. If you find a service where two accounts still see each other's data, please file an issue.

See Known limitations for the full cross-service gap list.

Using multiple accounts

Set distinct access keys for each logical account. No other configuration is needed.

export AWS_ACCESS_KEY_ID=111111111111
aws --endpoint-url=http://localhost:4566 s3 mb s3://account-one-bucket

export AWS_ACCESS_KEY_ID=222222222222
aws --endpoint-url=http://localhost:4566 s3 mb s3://account-two-bucket
aws --endpoint-url=http://localhost:4566 s3 ls   # only sees account-two-bucket

In Boto3/SDKs, construct one client per account with its own credential pair. The secret key is ignored but must be set — test is fine.

Tests that assert isolation: you can validate two accounts cannot see each other's resources by round-tripping a Create → List from the other account. If any service still bleeds across accounts, file an issue.