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:
| Calls to | Go to |
|---|---|
S3, when s3Service runs | danwo's S3 emulator, with signature checks and presigned URLs. |
The Bedrock runtime, when llmService runs | danwo's model API emulator, answered from scripts. |
| Every other AWS host | MiniStack, 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:
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:
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.
| Option | What it does |
|---|---|
docker | Start a MiniStack container per worker, removed on close. true runs the pinned image; { image } runs another. |
endpoint | A MiniStack or LocalStack you run; http://127.0.0.1:4566 by default. Ignored with docker. |
hosts | The hosts sent to MiniStack; every *.amazonaws.com by default. |
reset | Wipe 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:
| Variable | Set by | Holds |
|---|---|---|
HTTPS_PROXY, HTTP_PROXY | danwo | The proxy every call goes through. |
NODE_EXTRA_CA_CERTS | danwo | The proxy's certificate, so HTTPS to AWS hosts is trusted. |
NODE_OPTIONS | danwo | danwo's preload, for the clock and the proxy. |
AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY | You | Any value, such as test. |
AWS_REGION | You | The 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.
| Group | Services |
|---|---|
| Messaging | SQS (tested), SNS, EventBridge, EventBridge Pipes and Scheduler, Kinesis, Firehose, MQ, MSK |
| Data | DynamoDB and its streams, RDS and RDS Data, Aurora DSQL, DocumentDB, ElastiCache, OpenSearch |
| Storage | S3 (tested, when danwo's S3 emulator isn't running), EFS, S3 Tables, ECR, Backup |
| Compute | Lambda, ECS, EKS, EC2, Batch, Step Functions, Auto Scaling, Elastic Load Balancing, EMR |
| Identity and secrets | IAM, STS, Cognito, KMS, Secrets Manager, ACM, Systems Manager |
| Email and APIs | SES, API Gateway, AppSync, CloudFront, Route 53, WAF |
| Operations | CloudWatch and its logs, CloudFormation, CloudTrail, Config, Organizations |
| AI and more | Bedrock 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 MiniStack | What it means |
|---|---|
| Routing | The app calls real AWS hosts; nothing in its config points at MiniStack. |
| Recording | Every AWS call goes through the proxy, so it's in the proxy log and the flight record. |
| Resets | A reset wipes MiniStack, so a test that needs a clean start gets one. |
| Isolation | With docker: true, each worker has its own MiniStack, so one worker's reset never touches another's state. |
| Faults | The 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:
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.tsIt 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:
| Call | Faults the matrix tries |
|---|---|
A POST, PUT or DELETE to an AWS host | 500, connection reset, answer lost after MiniStack acted |
A GET or HEAD | 500, 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