dánwòdánwò

Infrastructure

AWS through MiniStack

Every other AWS service your app calls, from SQS to DynamoDB, forwarded by danwo's proxy to MiniStack, a local AWS emulator, with resets, recording and faults on top.

danwo doesn't emulate AWS itself, apart from S3 and the Bedrock runtime. Every other call your app makes to an AWS host is forwarded by danwo's proxy to MiniStack, an open-source local AWS emulator. Your app keeps its AWS SDK and its real hostnames.

How it works

The proxy picks a route by hostname, and the most specific one wins:

Your appAWS SDK, real hostsdanwo proxyroutes by hostnamedanwo's S3 emulators3.amazonaws.com, if s3Service runsdanwo's model API emulatorbedrock-runtime.*, if llmService runsMiniStackevery other *.amazonaws.com
The most specific host wins, so S3 and Bedrock stay with danwo whatever order the services are listed in.
Calls toGo to
S3, when s3Service runsdanwo's S3 emulator, with signature checks and presigned URLs.
The Bedrock runtime, when llmService runsdanwo's model API emulator, answered from scripts.
Every other AWS hostMiniStack, with the original Host header kept.

MiniStack reads the service and region from the host and the request, as AWS does. So a call to the SQS host in us-east-1 reaches MiniStack's SQS, and your app's code doesn't change.

Set up

There are two ways to run MiniStack. With docker: true, danwo starts a container for each worker and removes it when the worker stops:

danwo.config.ts
import { appEnvironment, awsService, defineConfig } from "@danwo/sdk"

export default defineConfig({
  testDir: "tests",
  environment: () =>
    appEnvironment({
      app: {
        command: "npm run dev",
        url: "http://localhost:3000",
        env: { AWS_ACCESS_KEY_ID: "test", AWS_SECRET_ACCESS_KEY: "test", AWS_REGION: "us-east-1" },
      },
      services: [awsService({ docker: true })],
    }),
})

Or point endpoint at a MiniStack or LocalStack you already run:

danwo.config.ts · your own MiniStack
awsService({ endpoint: "http://127.0.0.1:4566" })

For a test of AWS alone, with no app to start, awsEnvironment() takes the same options and starts MiniStack behind a proxy of its own.

OptionWhat it does
dockerStart a MiniStack container per worker, removed on close. true runs the pinned image; { image } runs another.
endpointA MiniStack or LocalStack you run; http://127.0.0.1:4566 by default. Ignored with docker.
hostsThe hosts sent to MiniStack; every *.amazonaws.com by default.
resetWipe MiniStack's state on every reset; on by default.

docker: true needs Docker on the machine, and waits for MiniStack's health check before the first test. The pinned image is MiniStack 1.5.28, exported as MINISTACK_IMAGE.

What the app gets

danwo doesn't hand the app AWS credentials. MiniStack doesn't check them, so any value works, but the AWS SDK still wants some:

VariableSet byHolds
HTTPS_PROXY, HTTP_PROXYdanwoThe proxy every call goes through.
NODE_EXTRA_CA_CERTSdanwoThe proxy's certificate, so HTTPS to AWS hosts is trusted.
NODE_OPTIONSdanwodanwo's preload, for the clock and the proxy.
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEYYouAny value, such as test.
AWS_REGIONYouThe region your app calls.

Don't set AWS_ENDPOINT_URL: the app should call the real hosts, so the proxy sees each call.

Seed it

MiniStack has no seed in danwo's config, and a reset wipes it. Create queues, tables and buckets from the test, against aws.endpoint, as the example below does.

Supported services

MiniStack 1.5.28 reports these services as available. The ones marked are those danwo's own tests run against it.

GroupServices
MessagingSQS (tested), SNS, EventBridge, EventBridge Pipes and Scheduler, Kinesis, Firehose, MQ, MSK
DataDynamoDB and its streams, RDS and RDS Data, Aurora DSQL, DocumentDB, ElastiCache, OpenSearch
StorageS3 (tested, when danwo's S3 emulator isn't running), EFS, S3 Tables, ECR, Backup
ComputeLambda, ECS, EKS, EC2, Batch, Step Functions, Auto Scaling, Elastic Load Balancing, EMR
Identity and secretsIAM, STS, Cognito, KMS, Secrets Manager, ACM, Systems Manager
Email and APIsSES, API Gateway, AppSync, CloudFront, Route 53, WAF
OperationsCloudWatch and its logs, CloudFormation, CloudTrail, Config, Organizations
AI and moreBedrock and its agents, Transcribe, Translate, Location, IoT, Glue, Athena

How fully each one behaves is MiniStack's to say; see its own documentation. Bedrock runtime calls go to danwo's model API emulator instead, when it runs.

What danwo adds

On top of MiniStackWhat it means
RoutingThe app calls real AWS hosts; nothing in its config points at MiniStack.
RecordingEvery AWS call goes through the proxy, so it's in the proxy log and the flight record.
ResetsA reset wipes MiniStack, so a test that needs a clean start gets one.
IsolationWith docker: true, each worker has its own MiniStack, so one worker's reset never touches another's state.
FaultsThe fault matrix plans failures on every AWS call.

The reset is global to one MiniStack. With a shared endpoint and several workers, turn reset off or use docker: true. See Isolation for when resets run.

Use it in a test

An app that reads a report from DynamoDB and queues its export on SQS. A reset wipes MiniStack, so the test makes the table, the queue and the report first, then asks SQS what was queued:

tests/export.danwo.ts
import { type AwsFixtures, test as base } from "@danwo/sdk"

const test = base.extend<AwsFixtures>()

/** Calls one of MiniStack's JSON APIs directly; it doesn't check signatures. */
async function call(endpoint: string, target: string, body: unknown): Promise<unknown> {
  const answer = await fetch(endpoint, {
    method: "POST",
    headers: { "content-type": "application/x-amz-json-1.0", "x-amz-target": target },
    body: JSON.stringify(body),
  })

  return answer.json()
}

test("queues the export of a stored report", async ({ app, screen, expect, aws }) => {
  await call(aws.endpoint, "DynamoDB_20120810.CreateTable", {
    TableName: "reports",
    KeySchema: [{ AttributeName: "id", KeyType: "HASH" }],
    AttributeDefinitions: [{ AttributeName: "id", AttributeType: "S" }],
    BillingMode: "PAY_PER_REQUEST",
  })
  await call(aws.endpoint, "AmazonSQS.CreateQueue", { QueueName: "exports" })
  await call(aws.endpoint, "DynamoDB_20120810.PutItem", {
    TableName: "reports",
    Item: { id: { S: "report-7" }, title: { S: "October sales" } },
  })

  await app.open("/reports/report-7")
  await expect(screen.getByText("October sales")).toBeVisible()
  await screen.getByRole("button", { name: "Export" }).click()
  await expect(screen.getByText("Export queued")).toBeVisible()

  const queued = (await call(aws.endpoint, "AmazonSQS.ReceiveMessage", {
    QueueUrl: `${aws.endpoint}/000000000000/exports`,
    MaxNumberOfMessages: 10,
  })) as { Messages?: { Body: string }[] }

  expect(queued.Messages?.map((message) => message.Body)).toEqual(["report-7"])
})

Every call the app made, to the DynamoDB host and the SQS host, is in the proxy log as routed.

Then run the matrix on it:

danwo matrix tests/export.danwo.ts

It replays the test with each AWS call failing in turn. A lost SendMessage answer is the one to watch: does the app retry and queue the export twice?

Faults

AWS calls get the plain faults the matrix gives any routed host:

CallFaults the matrix tries
A POST, PUT or DELETE to an AWS host500, connection reset, answer lost after MiniStack acted
A GET or HEAD500, connection reset

SQS and DynamoDB send reads as POST too, so their reads also get the lost answer.

Fidelity

Not checked by danwo. There's no fidelity suite for MiniStack; how closely it follows AWS is MiniStack's. danwo's own S3 emulator has its own suite.

Not covered yet

  • IAM: MiniStack runs with auth off, so a missing permission never fails
  • AWS-shaped faults, such as SQS throttling or DynamoDB's throughput errors
  • Services MiniStack lacks, and parts of the ones it has
  • Seeding MiniStack from danwo's config

Next

On this page

Run it on every pull request

A GitHub check and one comment per pull request, with the flight records attached.

Set up CI