DocsAWS 101BlogServices

Architecture

MiniStack runs as a single hypercorn ASGI process on one port. Every AWS service shares that port. This page explains how a request flows from SDK to service handler.

Single ASGI gateway

The entry point is an ASGI3 application at ministack.app:app. It serves all 60+ services from one port — 4566 by default (GATEWAY_PORT). There are no per-service ports, no reverse proxy, no sidecars. The gateway:

  • Parses the HTTP request (http.request body frames merged into one buffer).
  • Runs tiered detection to pick a service.
  • Lazy-imports that service's handler module the first time it's needed.
  • Calls the handler and writes the response.

Concurrency is cooperative — async handlers run on the event loop, sync handlers offload to a thread-pool executor sized by MINISTACK_WORKER_THREADS (default 64).

Startup lifecycle

On lifespan.startup:

  1. Banner logged (version + port).
  2. Thread-pool executor installed.
  3. init.d scripts executed synchronously (blocks startup).
  4. If PERSIST_STATE=1, on-disk state is restored into every multi-tenant map.
  5. Gateway signals lifespan.startup.complete — traffic starts flowing.
  6. ready.d scripts fire as a background task; /_ministack/ready flips to 200 when they finish.

On lifespan.shutdown: if persistence is enabled, each service flushes its maps to STATE_DIR. Healthy sidecar containers (RDS, EKS, ElastiCache, ECS) are left running so a restart can reattach them. A built-in reaper reclaims unambiguous garbage — created/dead containers, and exited ones no live service record references — on a periodic pass and in a boot sweep of the previous run's orphans, scoped by instance labels so parallel MiniStacks never touch each other's containers. Full teardown at test end remains the Testcontainers module's stop() (or yours, in plain Docker).

Request routing tiers

Routing happens in four tiers. Earlier tiers win — important for endpoints that would otherwise collide with service namespaces.

Tier 1 — pre-body routes

Handled without reading the request body. Health checks, OPTIONS (CORS), Lambda code downloads, Cognito well-known / JWKS, SES message inspection, admin reset.

Tier 2 — post-body shortcuts

Body is parsed, then special endpoints resolve before generic dispatch: Cognito OAuth2 (/oauth2/*, /login, /logout), runtime config.

Tier 3 — special data-plane

Host- and path-based routing for services that must dispatch before the generic step: S3 Control, RDS Data API, SES v2 REST, API Gateway execute-api, virtual-hosted S3, ALB data-plane.

Tier 4 — generic service dispatch

Router picks a service by inspecting, in order:

  • X-Amz-Target header (JSON services — e.g. DynamoDB_20120810.PutItem).
  • Authorization header credential scope (SigV4 service segment — e.g. .../us-east-1/s3/aws4_request).
  • Action= query parameter or form field (Query-protocol services — IAM, EC2, CFN, SNS, SQS legacy).
  • URL path pattern (REST services — Route53 /2013-04-01, API Gateway /restapis, etc.).
  • Host header patterns, for hosts this stack actually serves.
  • Nothing matched → the request defaults to S3 (same fallback as LocalStack).

Once a service is resolved, the gateway calls service_module.handle(scope, request) (or the per-service equivalent). That handler dispatches to an operation via a _HANDLERS dict, action map, or if/elif chain — see each service page for specifics.

Service registry

The SERVICE_REGISTRY dict in ministack/app.py maps the service name → module path. Modules are loaded on first use (_get_module()), not at startup, so cold-start cost is bounded. Unused services add nothing to RSS.

SERVICES=s3,sqs,lambda shrinks the registry to a subset — handy for test matrices that exercise a narrow slice.

State & persistence

All service state is in-memory Python dicts. Every multi-tenant service wraps its root dicts in AccountScopedDict (see Multi-tenancy), which adds transparent (account_id, key) partitioning.

Persistence layers — see Configuration → Persistence for details:

  • Service state (PERSIST_STATE=1) — JSON snapshots of each service's maps to STATE_DIR.
  • S3 object bodies (S3_PERSIST=1) — object bytes to S3_DATA_DIR.
  • RDS database data (RDS_PERSIST=1) — Docker named volumes instead of tmpfs.

Reset

POST /_ministack/reset calls _reset_all_state(), which iterates every registered service and wipes its in-memory state. A global reset lock serializes concurrent resets so no service sees a half-reset world. Add ?init=1 to re-run init.d scripts after the wipe.

Init & ready scripts

MiniStack scans two directory pairs for executables on startup. The LocalStack-compatible pair and the MiniStack-native pair both work; scripts in either are executed.

PhasePathsWhen
init.d/docker-entrypoint-initaws.d/, /etc/localstack/init/boot.d/Before the gateway accepts traffic. Synchronous — startup blocks until done.
ready.d/docker-entrypoint-initaws.d/ready.d/, /etc/localstack/init/ready.d/After startup, as a background task. /_ministack/ready flips to 200 once all finish.

Scripts inherit a pre-populated environment: AWS_ACCESS_KEY_ID=test, AWS_SECRET_ACCESS_KEY=test, AWS_DEFAULT_REGION=us-east-1, AWS_ENDPOINT_URL=http://localhost:4566. Point the AWS CLI at those and your seed scripts just work.

Want to see the dispatch for your own service? Set LOG_LEVEL=DEBUG — every request logs the detected service, operation, and account ID.