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 46 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. Sidecar containers (RDS, EKS, ElastiCache, ECS) are left running — reaping is the Testcontainers module's responsibility (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).
  • Host header (for *.amazonaws.com-style virtual hosts).
  • URL path pattern (REST services — Route53 /2013-04-01, API Gateway /restapis, etc.).

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.