<p align="center">
  <img src="ministack_logo.png" alt="MiniStack — Free Open-Source AWS Emulator" width="400"/>
</p>

<h1 align="center">MiniStack</h1>
<p align="center"><strong>Free, open-source local AWS emulator. Free forever.</strong></p>
<p align="center">56+ AWS services on a single port · Terraform compatible · Real databases · MIT licensed</p>

<p align="center">
  <a href="https://github.com/ministackorg/ministack/releases"><img src="https://img.shields.io/github/v/release/ministackorg/ministack" alt="GitHub release"></a>
  <a href="https://github.com/ministackorg/ministack/actions"><img src="https://img.shields.io/github/actions/workflow/status/ministackorg/ministack/ci.yml?branch=master" alt="Build"></a>
  <a href="https://hub.docker.com/r/ministackorg/ministack"><img src="https://img.shields.io/docker/pulls/ministackorg/ministack" alt="Docker Pulls"></a>
  <a href="https://hub.docker.com/r/ministackorg/ministack"><img src="https://img.shields.io/docker/image-size/ministackorg/ministack/latest" alt="Docker Image Size"></a>
  <a href="https://github.com/ministackorg/ministack/blob/master/LICENSE"><img src="https://img.shields.io/github/license/ministackorg/ministack" alt="License"></a>
  <img src="https://img.shields.io/badge/python-3.12-blue" alt="Python">
</p>

<p align="center">
  <a href="https://ministack.org">Ministack.org</a> · <a href="https://hub.docker.com/r/ministackorg/ministack">Docker Hub</a> · <a href="https://www.linkedin.com/company/ministackorg/">LinkedIn</a>
</p>

---

## Why MiniStack?

LocalStack recently moved its core services behind a paid plan. If you relied on LocalStack Community for local development and CI/CD pipelines, MiniStack is your free alternative.

- **56+ AWS services** emulated on a single port (4566)
- **Drop-in compatible** — works with `boto3`, AWS CLI, Terraform, CDK, Pulumi, any SDK
- **Real infrastructure** — RDS spins up actual Postgres/MySQL containers, ElastiCache spins up real Redis, Athena runs real SQL via DuckDB (full image only), ECS runs real Docker containers
- **Tiny footprint** — ~270MB image, ~30MB RAM at idle vs LocalStack's ~1GB image and ~500MB RAM
- **Fast startup** — under 2 seconds, HTTP/2 (h2c) supported
- **MIT licensed** — use it, fork it, contribute to it

---

## Quick Start

```bash
# Option 1: PyPI (simplest)
pip install ministack
ministack
# Runs on http://localhost:4566 — use GATEWAY_PORT=XXXX to change

# Option 2: Docker Hub
docker run -p 4566:4566 ministackorg/ministack

# Option 2b: Docker Hub with real infrastructure (RDS, ECS, Lambda containers)
docker run -p 4566:4566 -v /var/run/docker.sock:/var/run/docker.sock ministackorg/ministack

# Option 2c: Full image — Debian/glibc base with DuckDB (Athena), psycopg2, pymysql.
# Larger (~360 MB vs ~110 MB) but enables Athena and native PostgreSQL/MySQL drivers
# that don't ship musllinux wheels. Reports `edition: full` on /_ministack/health.
docker run -p 4566:4566 ministackorg/ministack:full

# Option 3: Clone and build
git clone https://github.com/ministackorg/ministack
cd ministack
docker compose up -d

# Verify (any option)
curl http://localhost:4566/_ministack/health
```

That's it. No account, no API key, no sign-up.

---

## Internal API

MiniStack exposes internal endpoints for test automation:

```bash
# Health check — returns service status
curl http://localhost:4566/_ministack/health

# Reset all state — wipe every service back to empty (useful between test runs)
curl -X POST http://localhost:4566/_ministack/reset

# Reset and re-run init scripts (boot.d + ready.d)
curl -X POST http://localhost:4566/_ministack/reset?init=1

# Runtime config — change service-level settings without restart
curl -X POST http://localhost:4566/_ministack/config \
  -H "Content-Type: application/json" \
  -d '{"lambda_svc.LAMBDA_EXECUTOR": "docker"}'

# Inspect emails sent via SES — returns every message grouped by account
curl http://localhost:4566/_ministack/ses/messages

# Filter by account (12-digit access-key ID used as the account ID)
curl "http://localhost:4566/_ministack/ses/messages?account=000000000000"
```

The reset endpoint is especially useful in CI pipelines and test suites — call it in `setUp`/`beforeEach` to get a clean environment for every test without restarting the container. Add `?init=1` to re-run your init scripts after the reset, restoring any resources they create (VPCs, queues, seed data, etc.).

The config endpoint supports these keys:

| Key | Description |
|-----|-------------|
| `lambda_svc.LAMBDA_EXECUTOR` | Lambda execution mode (`local` or `docker`) |
| `athena.ATHENA_ENGINE` | Athena query engine (`duckdb` or `mock`) |
| `athena.ATHENA_DATA_DIR` | Directory for Athena DuckDB data files |
| `stepfunctions._sfn_mock_config` | SFN mock config (AWS SFN Local compatible) |
| `stepfunctions._SFN_WAIT_SCALE` | Scale factor for Wait state durations and retry sleeps (`0` = skip all waits) |

To set region or account ID, use environment variables at startup:

```bash
docker run -p 4566:4566 \
  -e MINISTACK_REGION=eu-west-1 \
  -e MINISTACK_ACCOUNT_ID=123456789012 \
  ministackorg/ministack
```

Or use the multi-tenancy feature — a 12-digit access key automatically becomes the account ID (see [Multi-Tenancy](#multi-tenancy) below).

Also compatible with LocalStack's health endpoint:

```bash
curl http://localhost:4566/_localstack/health
curl http://localhost:4566/health
```

---

## Multi-Tenancy

MiniStack supports lightweight multi-tenancy without any configuration. If the `AWS_ACCESS_KEY_ID` is a **12-digit number**, it is used as the **Account ID** for all ARN generation. Non-numeric keys (like `test`) fall back to the `MINISTACK_ACCOUNT_ID` env var or `000000000000`.

```bash
# Team A — gets account 111111111111
export AWS_ACCESS_KEY_ID=111111111111
export AWS_SECRET_ACCESS_KEY=anything
aws --endpoint-url=http://localhost:4566 sts get-caller-identity
# → { "Account": "111111111111", ... }

# Team B — gets account 222222222222
export AWS_ACCESS_KEY_ID=222222222222
export AWS_SECRET_ACCESS_KEY=anything
aws --endpoint-url=http://localhost:4566 sts get-caller-identity
# → { "Account": "222222222222", ... }
```

All ARNs and resource state (SQS queues, Lambda functions, IAM roles, S3 buckets, DynamoDB tables, etc.) are fully isolated per account. Resources with the same name in different accounts never collide. This allows multiple developers or CI pipelines to share a single MiniStack endpoint with complete tenant isolation — no extra setup needed.

| Access Key | Account ID Used |
|---|---|
| `111111111111` | `111111111111` |
| `048408301323` | `048408301323` |
| `test` | `000000000000` (default) |
| `AKIAIOSFODNN7EXAMPLE` | `000000000000` (default) |

**Terraform** — set `access_key` in your provider block:
```hcl
provider "aws" {
  access_key = "048408301323"
  secret_key = "test"
  region     = "us-east-1"
  endpoints { ... }
}
```

**boto3** — pass `aws_access_key_id`:
```python
boto3.client("s3",
    endpoint_url="http://localhost:4566",
    aws_access_key_id="048408301323",
    aws_secret_access_key="test",
)
```

---

## Using with AWS CLI

```bash
# Option A — environment variables (no profile needed)
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_DEFAULT_REGION=us-east-1

aws --endpoint-url=http://localhost:4566 s3 mb s3://my-bucket
aws --endpoint-url=http://localhost:4566 sqs create-queue --queue-name my-queue
aws --endpoint-url=http://localhost:4566 dynamodb list-tables
aws --endpoint-url=http://localhost:4566 sts get-caller-identity

# Option B — named profile (must pass --profile on every command)
aws configure --profile local
# AWS Access Key ID: test
# AWS Secret Access Key: test
# Default region: us-east-1
# Default output format: json

aws --profile local --endpoint-url=http://localhost:4566 s3 mb s3://my-bucket
aws --profile local --endpoint-url=http://localhost:4566 s3 cp ./file.txt s3://my-bucket/
aws --profile local --endpoint-url=http://localhost:4566 sqs create-queue --queue-name my-queue
aws --profile local --endpoint-url=http://localhost:4566 dynamodb list-tables
aws --profile local --endpoint-url=http://localhost:4566 sts get-caller-identity
```

### awslocal wrapper

```bash
chmod +x bin/awslocal
./bin/awslocal s3 ls
./bin/awslocal dynamodb list-tables
```

---

## Using with boto3

```python
import boto3

# All clients use the same endpoint
def client(service):
    return boto3.client(
        service,
        endpoint_url="http://localhost:4566",
        aws_access_key_id="test",
        aws_secret_access_key="test",
        region_name="us-east-1",
    )

# S3
s3 = client("s3")
s3.create_bucket(Bucket="my-bucket")
s3.put_object(Bucket="my-bucket", Key="hello.txt", Body=b"Hello, MiniStack!")
obj = s3.get_object(Bucket="my-bucket", Key="hello.txt")
print(obj["Body"].read())  # b'Hello, MiniStack!'

# SQS
sqs = client("sqs")
q = sqs.create_queue(QueueName="my-queue")
sqs.send_message(QueueUrl=q["QueueUrl"], MessageBody="hello")
msgs = sqs.receive_message(QueueUrl=q["QueueUrl"])
print(msgs["Messages"][0]["Body"])  # hello

# DynamoDB
ddb = client("dynamodb")
ddb.create_table(
    TableName="Users",
    KeySchema=[{"AttributeName": "userId", "KeyType": "HASH"}],
    AttributeDefinitions=[{"AttributeName": "userId", "AttributeType": "S"}],
    BillingMode="PAY_PER_REQUEST",
)
ddb.put_item(TableName="Users", Item={"userId": {"S": "u1"}, "name": {"S": "Alice"}})

# SSM Parameter Store
ssm = client("ssm")
ssm.put_parameter(Name="/app/db/host", Value="localhost", Type="String")
param = ssm.get_parameter(Name="/app/db/host")
print(param["Parameter"]["Value"])  # localhost

# Secrets Manager
sm = client("secretsmanager")
sm.create_secret(Name="db-password", SecretString='{"password":"s3cr3t"}')

# Kinesis
kin = client("kinesis")
kin.create_stream(StreamName="events", ShardCount=1)
kin.put_record(StreamName="events", Data=b'{"event":"click"}', PartitionKey="user1")

# EventBridge
eb = client("events")
eb.put_events(Entries=[{
    "Source": "myapp",
    "DetailType": "UserSignup",
    "Detail": '{"userId": "123"}',
    "EventBusName": "default",
}])

# Step Functions
sfn = client("stepfunctions")
sfn.create_state_machine(
    name="my-workflow",
    definition='{"StartAt":"Hello","States":{"Hello":{"Type":"Pass","End":true}}}',
    roleArn="arn:aws:iam::000000000000:role/role",
)

# Step Functions — TestState API (test a single state in isolation)
# Note: inject_host_prefix=False prevents boto3 from prepending "sync-" to the hostname
from botocore.config import Config as BotoConfig
sfn_test = client("stepfunctions", config=BotoConfig(inject_host_prefix=False))

result = sfn_test.test_state(
    definition='{"Type":"Pass","Result":{"greeting":"hello"},"End":true}',
    input='{"name":"world"}',
)
print(result["status"])  # SUCCEEDED
print(result["output"])  # {"greeting": "hello"}

# TestState with mock — test error handling without calling real services
result = sfn_test.test_state(
    definition=json.dumps({
        "Type": "Task",
        "Resource": "arn:aws:lambda:us-east-1:000000000000:function:my-fn",
        "Catch": [{"ErrorEquals": ["States.ALL"], "Next": "Fallback"}],
        "End": True
    }),
    input='{}',
    inspectionLevel="DEBUG",
    mock={"errorOutput": {"error": "ServiceError", "cause": "Timeout"}},
)
print(result["status"])  # CAUGHT_ERROR
print(result["nextState"])  # Fallback

# EC2
ec2 = client("ec2")
reservation = ec2.run_instances(
    ImageId="ami-00000001",
    MinCount=1,
    MaxCount=1,
    InstanceType="t3.micro",
)
instance_id = reservation["Instances"][0]["InstanceId"]
print(instance_id)  # i-xxxxxxxxxxxxxxxxx

# Security Groups
sg = ec2.create_security_group(GroupName="my-sg", Description="My SG")
ec2.authorize_security_group_ingress(
    GroupId=sg["GroupId"],
    IpPermissions=[{"IpProtocol": "tcp", "FromPort": 80, "ToPort": 80,
                    "IpRanges": [{"CidrIp": "0.0.0.0/0"}]}],
)

# VPC / Subnet
vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16")
subnet = ec2.create_subnet(
    VpcId=vpc["Vpc"]["VpcId"],
    CidrBlock="10.0.1.0/24",
    AvailabilityZone="us-east-1a",
)
```

---

## Supported Services

### Core Services

| Service | Operations | Notes |
|---------|-----------|-------|
| **S3** | CreateBucket, DeleteBucket, ListBuckets, HeadBucket, PutObject, GetObject, DeleteObject, HeadObject, CopyObject, ListObjects v1/v2, DeleteObjects, GetBucketVersioning, PutBucketVersioning, GetBucketEncryption, PutBucketEncryption, DeleteBucketEncryption, GetBucketLifecycleConfiguration, PutBucketLifecycleConfiguration, DeleteBucketLifecycle, GetBucketCors, PutBucketCors, DeleteBucketCors, GetBucketAcl, PutBucketAcl, GetBucketTagging, PutBucketTagging, DeleteBucketTagging, GetBucketPolicy, PutBucketPolicy, DeleteBucketPolicy, GetBucketNotificationConfiguration, PutBucketNotificationConfiguration, GetBucketLogging, PutBucketLogging, ListObjectVersions, CreateMultipartUpload, UploadPart, CompleteMultipartUpload, AbortMultipartUpload, PutObjectLockConfiguration, GetObjectLockConfiguration, PutObjectRetention, GetObjectRetention, PutObjectLegalHold, GetObjectLegalHold, PutBucketReplication, GetBucketReplication, DeleteBucketReplication, GetObjectTagging, PutObjectTagging, DeleteObjectTagging | Optional disk persistence via `S3_PERSIST=1`; Object Lock with retention & legal hold enforcement on delete; object tags are versioned (`?tagging&versionId=…` reads, writes, and deletes the per-version tag set) |
| **SQS** | CreateQueue, DeleteQueue, ListQueues, GetQueueUrl, GetQueueAttributes, SetQueueAttributes, PurgeQueue, SendMessage, ReceiveMessage, DeleteMessage, ChangeMessageVisibility, ChangeMessageVisibilityBatch, SendMessageBatch, DeleteMessageBatch, TagQueue, UntagQueue, ListQueueTags | Both Query API and JSON protocol; FIFO queues with deduplication; DLQ support |
| **SNS** | CreateTopic, DeleteTopic, ListTopics, GetTopicAttributes, SetTopicAttributes, Subscribe, Unsubscribe, ListSubscriptions, ListSubscriptionsByTopic, GetSubscriptionAttributes, SetSubscriptionAttributes, ConfirmSubscription, Publish, PublishBatch, TagResource, UntagResource, ListTagsForResource, CreatePlatformApplication, CreatePlatformEndpoint | SNS→SQS fanout delivery; SNS→Lambda fanout (synchronous invocation); FIFO topics with 5-minute deduplication, sequence numbers, content-based deduplication, and subscription validation |
| **DynamoDB** | CreateTable, UpdateTable, DeleteTable, DescribeTable, ListTables, PutItem, GetItem, DeleteItem, UpdateItem, Query, Scan, BatchWriteItem, BatchGetItem, TransactWriteItems, TransactGetItems, DescribeTimeToLive, UpdateTimeToLive, DescribeContinuousBackups, UpdateContinuousBackups, DescribeEndpoints, TagResource, UntagResource, ListTagsOfResource, EnableKinesisStreamingDestination, DisableKinesisStreamingDestination, DescribeKinesisStreamingDestination, UpdateKinesisStreamingDestination | TTL enforced via thread-safe background reaper (60s cadence); DynamoDB Streams — `StreamSpecification` emits INSERT/MODIFY/REMOVE records on all write operations, respects `StreamViewType`; Kinesis streaming destinations (`aws_dynamodb_kinesis_streaming_destination`) fan item mutations out into any Kinesis stream by ARN while the destination is ACTIVE |
| **DynamoDB Streams** | ListStreams, DescribeStream, GetShardIterator, GetRecords | Reads records emitted by the main DynamoDB service via `boto3.client("dynamodbstreams")` — single synthetic shard per stream; `TRIM_HORIZON`/`LATEST`/`AT_SEQUENCE_NUMBER`/`AFTER_SEQUENCE_NUMBER` iterator types; `NEW_AND_OLD_IMAGES`, `NEW_IMAGE`, `OLD_IMAGE`, `KEYS_ONLY` view types; opaque base64 iterator tokens |
| **Lambda** | CreateFunction, DeleteFunction, GetFunction, GetFunctionConfiguration, ListFunctions, Invoke, UpdateFunctionCode, UpdateFunctionConfiguration, AddPermission, RemovePermission, GetPolicy, ListVersionsByFunction, PublishVersion, CreateAlias, GetAlias, UpdateAlias, DeleteAlias, ListAliases, TagResource, UntagResource, ListTags, CreateEventSourceMapping, DeleteEventSourceMapping, GetEventSourceMapping, ListEventSourceMappings, UpdateEventSourceMapping, CreateFunctionUrlConfig, GetFunctionUrlConfig, UpdateFunctionUrlConfig, DeleteFunctionUrlConfig, ListFunctionUrlConfigs, PutFunctionConcurrency, GetFunctionConcurrency, DeleteFunctionConcurrency, PutFunctionEventInvokeConfig, GetFunctionEventInvokeConfig, DeleteFunctionEventInvokeConfig, PublishLayerVersion, GetLayerVersion, GetLayerVersionByArn, ListLayerVersions, DeleteLayerVersion, ListLayers, AddLayerVersionPermission, RemoveLayerVersionPermission, GetLayerVersionPolicy, CheckpointDurableExecution, GetDurableExecution, GetDurableExecutionState, GetDurableExecutionHistory, ListDurableExecutionsByFunction, StopDurableExecution, SendDurableExecutionCallbackSuccess, SendDurableExecutionCallbackFailure, SendDurableExecutionCallbackHeartbeat | Python and Node.js runtimes execute with warm worker pool; `provided.al2023`/`provided.al2` runtimes execute via Docker RIE (Go, Rust, C++ support); `Publish=True` creates immutable numbered versions; Code via `ZipFile`, `S3Bucket`/`S3Key` (with optional `S3ObjectVersion`), or `ImageUri` (Docker image); `PackageType: Image` pulls and invokes user-provided Docker images via Lambda RIE; SQS, Kinesis, and DynamoDB Streams event source mappings; Function URL CRUD; Lambda Layers CRUD; Aliases; Concurrency; EventInvokeConfig; **Durable Functions** — `CreateFunction` accepts `DurableConfig`; checkpoint/state/history/list/stop ops at the preview API (`2025-12-01`); external `SendCallback{Success,Failure,Heartbeat}` resume the SDK across invocations; resume scheduler fires WAIT expiries, callback timeouts, and step-retry backoffs; verified against the official `aws-durable-execution-sdk-python` and `aws-durable-execution-sdk-java` |
| **IAM** | CreateUser, GetUser, ListUsers, DeleteUser, CreateRole, GetRole, ListRoles, DeleteRole, CreatePolicy, GetPolicy, DeletePolicy, AttachRolePolicy, DetachRolePolicy, PutRolePolicy, GetRolePolicy, DeleteRolePolicy, ListRolePolicies, ListAttachedRolePolicies, CreateAccessKey, ListAccessKeys, DeleteAccessKey, UpdateAccessKey, GetAccessKeyLastUsed, CreateInstanceProfile, GetInstanceProfile, DeleteInstanceProfile, AddRoleToInstanceProfile, RemoveRoleFromInstanceProfile, ListInstanceProfiles, CreateGroup, GetGroup, AddUserToGroup, RemoveUserFromGroup, CreateServiceLinkedRole, DeleteServiceLinkedRole, GetServiceLinkedRoleDeletionStatus, CreateOpenIDConnectProvider, TagRole, UntagRole, TagUser, UntagUser, TagPolicy, UntagPolicy | |
| **STS** | GetCallerIdentity, AssumeRole, GetSessionToken, AssumeRoleWithWebIdentity | |
| **IMDS** (EC2 Instance Metadata) | `PUT /latest/api/token`, `GET /latest/meta-data/instance-id`, `GET /latest/meta-data/iam/security-credentials/`, `GET /latest/meta-data/iam/security-credentials/<role>`, `GET /latest/meta-data/iam/info`, `GET /latest/meta-data/placement/{region,availability-zone,...}`, `GET /latest/dynamic/instance-identity/document` | IMDSv1 + IMDSv2; default credential chain falls through to a `ministack-instance-role` document with `ASIA*` session creds. Point SDKs at ministack via `AWS_EC2_METADATA_SERVICE_ENDPOINT=http://localhost:4566` (or `ec2_metadata_service_endpoint` in `~/.aws/config`); set `MINISTACK_IMDS_V2_REQUIRED=1` to require the token PUT |
| **ECS Task Metadata V4** | `GET /v4/<token>`, `GET /v4/<token>/task`, `GET /v4/<token>/stats`, `GET /v4/<token>/task/stats` | Per-container token injected as `ECS_CONTAINER_METADATA_URI_V4` on every container started by `RunTask`. `/task` returns sibling containers in the same task. Containers reach the gateway via `host.docker.internal` (mapped through `extra_hosts: host-gateway`, so it works on user-defined Docker networks); `networkMode: host` containers use loopback. Volatile by design (stripped on persistence, cleared by `/_ministack/reset`) |
| **ECS Container Credentials** | `GET /v2/credentials/<uuid>` | The path real ECS exposes via `AWS_CONTAINER_CREDENTIALS_RELATIVE_URI=/v2/credentials/<uuid>` (resolved by SDKs against `169.254.170.2`). MiniStack serves the same path on the gateway and returns the AWS-strict 5-field credentials document (`AccessKeyId`, `SecretAccessKey`, `Token`, `Expiration`, `RoleArn`) — distinct from the IMDS shape served at `/latest/meta-data/iam/security-credentials/<role>`. `RunTask` injects `AWS_CONTAINER_CREDENTIALS_FULL_URI`, `AWS_CONTAINER_AUTHORIZATION_TOKEN` (satisfies botocore's allow-list for non-loopback gateway hosts), and `AWS_ENDPOINT_URL` so unmodified SDKs inside the task fetch credentials and route service calls through MiniStack with no client config |
| **SecretsManager** | CreateSecret, GetSecretValue, ListSecrets, DeleteSecret, UpdateSecret, DescribeSecret, PutSecretValue, UpdateSecretVersionStage, RestoreSecret, RotateSecret, GetRandomPassword, ListSecretVersionIds, ReplicateSecretToRegions, TagResource, UntagResource, PutResourcePolicy, GetResourcePolicy, DeleteResourcePolicy, ValidateResourcePolicy | |
| **CloudWatch Logs** | CreateLogGroup, DeleteLogGroup, DescribeLogGroups, CreateLogStream, DeleteLogStream, DescribeLogStreams, PutLogEvents, GetLogEvents, FilterLogEvents, PutRetentionPolicy, DeleteRetentionPolicy, PutSubscriptionFilter, DeleteSubscriptionFilter, DescribeSubscriptionFilters, PutMetricFilter, DeleteMetricFilter, DescribeMetricFilters, TagLogGroup, UntagLogGroup, ListTagsLogGroup, TagResource, UntagResource, ListTagsForResource, StartQuery, GetQueryResults, StopQuery, PutDestination, DeleteDestination, DescribeDestinations, PutDestinationPolicy | `FilterLogEvents` supports `*`/`?` globs, multi-term AND, `-term` exclusion |

### Extended Services

| Service | Operations | Notes |
|---------|-----------|-------|
| **SSM Parameter Store** | PutParameter, GetParameter, GetParameters, GetParametersByPath, DeleteParameter, DeleteParameters, DescribeParameters, GetParameterHistory, LabelParameterVersion, AddTagsToResource, RemoveTagsFromResource, ListTagsForResource | Supports String, SecureString, StringList |
| **EventBridge** | CreateEventBus, UpdateEventBus, DeleteEventBus, ListEventBuses, DescribeEventBus, PutRule, DeleteRule, ListRules, DescribeRule, EnableRule, DisableRule, PutTargets, RemoveTargets, ListTargetsByRule, ListRuleNamesByTarget, PutEvents, TestEventPattern, TagResource, UntagResource, ListTagsForResource, CreateArchive, DeleteArchive, DescribeArchive, UpdateArchive, ListArchives, PutPermission, RemovePermission, CreateConnection, DescribeConnection, DeleteConnection, UpdateConnection, DeauthorizeConnection, ListConnections, CreateApiDestination, DescribeApiDestination, DeleteApiDestination, UpdateApiDestination, ListApiDestinations, StartReplay, DescribeReplay, ListReplays, CancelReplay, CreateEndpoint, DeleteEndpoint, DescribeEndpoint, ListEndpoints, UpdateEndpoint, ActivateEventSource, DeactivateEventSource, DescribeEventSource, CreatePartnerEventSource, DeletePartnerEventSource, DescribePartnerEventSource, ListPartnerEventSources, ListPartnerEventSourceAccounts, ListEventSources, PutPartnerEvents | Lambda target dispatch on PutEvents; S3 EventBridge notifications; archives capture matching events and `StartReplay` re-dispatches them to the destination bus in a background thread; SaaS/partner APIs are control-plane stubs |
| **Kinesis** | CreateStream, DeleteStream, DescribeStream, DescribeStreamSummary, ListStreams, ListShards, PutRecord, PutRecords, GetShardIterator, GetRecords, IncreaseStreamRetentionPeriod, DecreaseStreamRetentionPeriod, MergeShards, SplitShard, UpdateShardCount, StartStreamEncryption, StopStreamEncryption, EnableEnhancedMonitoring, DisableEnhancedMonitoring, RegisterStreamConsumer, DeregisterStreamConsumer, ListStreamConsumers, DescribeStreamConsumer, AddTagsToStream, RemoveTagsFromStream, ListTagsForStream | Partition key → shard routing; AWS limits enforced (1 MB/record, 500 records/batch, 5 MB payload, 256-char partition key) |
| **CloudWatch Metrics** | PutMetricData, GetMetricStatistics, GetMetricData, ListMetrics, PutMetricAlarm, PutCompositeAlarm, DescribeAlarms, DescribeAlarmsForMetric, DescribeAlarmHistory, DeleteAlarms, SetAlarmState, EnableAlarmActions, DisableAlarmActions, TagResource, UntagResource, ListTagsForResource, PutDashboard, GetDashboard, DeleteDashboards, ListDashboards | CBOR and JSON protocol |
| **SES** | SendEmail, SendRawEmail, SendTemplatedEmail, SendBulkTemplatedEmail, VerifyEmailIdentity, VerifyEmailAddress, VerifyDomainIdentity, VerifyDomainDkim, ListIdentities, ListVerifiedEmailAddresses, GetIdentityVerificationAttributes, GetIdentityDkimAttributes, DeleteIdentity, GetSendQuota, GetSendStatistics, SetIdentityNotificationTopic, SetIdentityFeedbackForwardingEnabled, CreateConfigurationSet, DeleteConfigurationSet, DescribeConfigurationSet, ListConfigurationSets, CreateTemplate, GetTemplate, UpdateTemplate, DeleteTemplate, ListTemplates | Emails stored in-memory, not sent |
| **SES v2** | SendEmail, CreateEmailIdentity, GetEmailIdentity, DeleteEmailIdentity, ListEmailIdentities, CreateConfigurationSet, GetConfigurationSet, DeleteConfigurationSet, ListConfigurationSets, GetAccount, PutAccountSuppressionAttributes, ListSuppressedDestinations | REST API (`/v2/email/`); identities auto-verified; emails stored in-memory, not sent |
| **ACM** | RequestCertificate, DescribeCertificate, ListCertificates, DeleteCertificate, GetCertificate, ImportCertificate, AddTagsToCertificate, RemoveTagsFromCertificate, ListTagsForCertificate, UpdateCertificateOptions, RenewCertificate, ResendValidationEmail | Certificates auto-issued; DNS validation records generated; supports SANs |
| **Backup** | CreateBackupVault, DescribeBackupVault, DeleteBackupVault, ListBackupVaults, CreateBackupPlan, GetBackupPlan, UpdateBackupPlan, DeleteBackupPlan, ListBackupPlans, ListBackupPlanVersions, CreateBackupSelection, GetBackupSelection, DeleteBackupSelection, ListBackupSelections, StartBackupJob, StopBackupJob, DescribeBackupJob, ListBackupJobs, TagResource, UntagResource, ListTags | In-memory; jobs complete immediately; vaults and plans participate in Resource Groups Tagging API |
| **WAF v2** | CreateWebACL, GetWebACL, UpdateWebACL, DeleteWebACL, ListWebACLs, AssociateWebACL, DisassociateWebACL, GetWebACLForResource, ListResourcesForWebACL, CreateIPSet, GetIPSet, UpdateIPSet, DeleteIPSet, ListIPSets, CreateRuleGroup, GetRuleGroup, UpdateRuleGroup, DeleteRuleGroup, ListRuleGroups, TagResource, UntagResource, ListTagsForResource, CheckCapacity, DescribeManagedRuleGroup | LockToken enforced on Update/Delete; resource associations tracked |
| **Step Functions** | CreateStateMachine, DeleteStateMachine, DescribeStateMachine, UpdateStateMachine, ListStateMachines, StartExecution, StartSyncExecution, StopExecution, DescribeExecution, DescribeStateMachineForExecution, ListExecutions, GetExecutionHistory, SendTaskSuccess, SendTaskFailure, SendTaskHeartbeat, CreateActivity, DeleteActivity, DescribeActivity, ListActivities, GetActivityTask, TestState, TagResource, UntagResource, ListTagsForResource | Full ASL interpreter; Retry/Catch; waitForTaskToken; Activities (worker pattern); Pass/Task/Choice/Wait/Succeed/Fail/Map/Parallel; TestState API with mock and inspectionLevel support; SFN_MOCK_CONFIG for AWS SFN Local compatible mock testing; intrinsic functions (States.StringToJson, States.JsonToString, States.JsonMerge, States.Format); nested startExecution.sync |
| **API Gateway v2** | CreateApi, GetApi, GetApis, UpdateApi, DeleteApi, CreateRoute, GetRoute, GetRoutes, UpdateRoute, DeleteRoute, CreateIntegration, GetIntegration, GetIntegrations, UpdateIntegration, DeleteIntegration, CreateRouteResponse, GetRouteResponse, GetRouteResponses, UpdateRouteResponse, DeleteRouteResponse, CreateIntegrationResponse, GetIntegrationResponse, GetIntegrationResponses, UpdateIntegrationResponse, DeleteIntegrationResponse, CreateStage, GetStage, GetStages, UpdateStage, DeleteStage, CreateDeployment, GetDeployment, GetDeployments, DeleteDeployment, CreateAuthorizer, GetAuthorizer, GetAuthorizers, UpdateAuthorizer, DeleteAuthorizer, TagResource, UntagResource, GetTags, PostToConnection, GetConnection, DeleteConnection | **HTTP API** and **WebSocket API** (`protocolType=WEBSOCKET`); Lambda proxy (`AWS_PROXY`), HTTP proxy (`HTTP_PROXY`), and MOCK integrations; HTTP data plane via `{apiId}.execute-api.localhost` Host header or path-based `/_aws/execute-api/{apiId}/{stage}/{path}` (no DNS/Host override needed — works from browsers on macOS and strict clients); `$default` stage served from the URL root (no stage segment in the path); per-API `corsConfiguration` applied to preflights + dispatched responses; request parameter mapping for HTTP_PROXY (`append/overwrite/remove` for headers/querystring plus `overwrite:path`) with context variables including `$context.authorizer.jwt.claims`; JWT data-plane authorization for HTTP routes (issuer/audience/time/scope checks) and claim propagation to integrations; qualified-alias integration URIs (`arn:...:function:<name>:<alias>`) resolve to the alias's target version; WebSocket data plane on the same two URL forms, with `$connect` / `$disconnect` / `$default` / custom-action routing, `$request.body.*` RouteSelectionExpression, `@connections` management API (PostToConnection / GetConnection / DeleteConnection), per-connection outbox for server-side push; `{param}` / `{proxy+}` matching; JWT/Lambda authorizer CRUD; pin `apiId` across runs with the `ms-custom-id` tag |
| **API Gateway v1** | CreateRestApi, GetRestApi, GetRestApis, UpdateRestApi, DeleteRestApi, CreateResource, GetResource, GetResources, UpdateResource, DeleteResource, PutMethod, GetMethod, DeleteMethod, UpdateMethod, PutMethodResponse, GetMethodResponse, DeleteMethodResponse, PutIntegration, GetIntegration, DeleteIntegration, UpdateIntegration, PutIntegrationResponse, GetIntegrationResponse, DeleteIntegrationResponse, CreateDeployment, GetDeployment, GetDeployments, UpdateDeployment, DeleteDeployment, CreateStage, GetStage, GetStages, UpdateStage, DeleteStage, CreateAuthorizer, GetAuthorizer, GetAuthorizers, UpdateAuthorizer, DeleteAuthorizer, CreateModel, GetModel, GetModels, DeleteModel, CreateApiKey, GetApiKey, GetApiKeys, UpdateApiKey, DeleteApiKey, CreateUsagePlan, GetUsagePlan, GetUsagePlans, UpdateUsagePlan, DeleteUsagePlan, CreateUsagePlanKey, GetUsagePlanKeys, DeleteUsagePlanKey, CreateDomainName, GetDomainName, GetDomainNames, DeleteDomainName, CreateBasePathMapping, GetBasePathMapping, GetBasePathMappings, DeleteBasePathMapping, TagResource, UntagResource, GetTags | REST API (v1) protocol; Lambda proxy format 1.0 (AWS_PROXY), HTTP proxy (HTTP_PROXY), MOCK integration; data plane via `{apiId}.execute-api.localhost` Host header, path-based `/_aws/execute-api/{apiId}/{stage}/{path}`, or legacy `/restapis/{apiId}/{stage}/_user_request_/{path}`; qualified-alias integration URIs (`arn:...:function:<name>:<alias>`) resolve to the alias's target version; resource tree with `{param}` and `{proxy+}` path matching; JSON Patch for all PATCH operations; state persistence; pin `id` across runs with the `ms-custom-id` tag |
| **ELBv2 / ALB** | CreateLoadBalancer, DescribeLoadBalancers, DeleteLoadBalancer, DescribeLoadBalancerAttributes, ModifyLoadBalancerAttributes, CreateTargetGroup, DescribeTargetGroups, ModifyTargetGroup, DeleteTargetGroup, DescribeTargetGroupAttributes, ModifyTargetGroupAttributes, CreateListener, DescribeListeners, ModifyListener, DeleteListener, CreateRule, DescribeRules, ModifyRule, DeleteRule, SetRulePriorities, RegisterTargets, DeregisterTargets, DescribeTargetHealth, AddTags, RemoveTags, DescribeTags | Control plane + data plane; ALB→Lambda live traffic routing; `path-pattern`, `host-header`, `http-method`, `query-string`, `http-header` rule conditions; `forward`, `redirect`, `fixed-response` actions; data plane via `{lb-name}.alb.localhost` Host header or `/_alb/{lb-name}/` path prefix |
| **KMS** | CreateKey, ListKeys, DescribeKey, GetPublicKey, Sign, Verify, Encrypt, Decrypt, GenerateDataKey, GenerateDataKeyWithoutPlaintext, CreateAlias, DeleteAlias, ListAliases, UpdateAlias, EnableKeyRotation, DisableKeyRotation, GetKeyRotationStatus, GetKeyPolicy, PutKeyPolicy, ListKeyPolicies, EnableKey, DisableKey, ScheduleKeyDeletion, CancelKeyDeletion, TagResource, UntagResource, ListResourceTags | 27 actions; RSA (2048/4096), ECC (SECG_P256K1, NIST P-256/384/521), and symmetric keys; PKCS1v15, PSS, and ECDSA signing; envelope encryption; alias resolution; key rotation; key policies; tags; enable/disable/schedule deletion; full Terraform `aws_kms_key` compatible; `cryptography` package included in Docker image |
| **CloudFront** | CreateDistribution, GetDistribution, GetDistributionConfig, ListDistributions, UpdateDistribution, DeleteDistribution, CreateInvalidation, ListInvalidations, GetInvalidation, CreateOriginAccessControl, GetOriginAccessControl, GetOriginAccessControlConfig, ListOriginAccessControls, UpdateOriginAccessControl, DeleteOriginAccessControl, CreateFunction, DeleteFunction, DescribeFunction, GetFunction, ListFunctions, PublishFunction, UpdateFunction, **KeyValueStore (mgmt)**: CreateKeyValueStore, DescribeKeyValueStore, ListKeyValueStores, UpdateKeyValueStore, DeleteKeyValueStore, TagResource, UntagResource, ListTagsForResource | REST/XML API; ETag-based optimistic concurrency; Origin config round-trip; Origin Access Control (OAC) with SigV4 signing for S3, MediaStore, Lambda, MediaPackageV2 origins; **CloudFront Functions** are API stubs (in-memory code + DEVELOPMENT/LIVE ETags for Terraform `aws_cloudfront_function`) with `KeyValueStoreAssociations` round-tripped — no `TestFunction`, no viewer-request JS execution at the edge; `UpdateFunction` clears the emulated LIVE stage until `PublishFunction` runs again |
| **CloudFront KeyValueStore (data plane)** | DescribeKeyValueStore, ListKeys, GetKey, PutKey, DeleteKey, UpdateKeys | Separate `cloudfront-keyvaluestore` SDK service (REST/JSON, signing name `cloudfront-keyvaluestore`); ETag-based optimistic concurrency on every mutating op; `UpdateKeys` is atomic (validates whole batch, rejects on first invalid entry); `ListKeys` paginates with opaque `NextToken` capped at 50 results per AWS spec; bridges to the management plane via the shared KVS ARN |
| **CloudTrail** | LookupEvents, CreateTrail, DeleteTrail, GetTrail, DescribeTrails, ListTrails, UpdateTrail, GetTrailStatus, StartLogging, StopLogging, PutEventSelectors, GetEventSelectors, AddTags, ListTags, RemoveTags | In-memory audit log; recording opt-in via `CLOUDTRAIL_RECORDING=1` (or runtime config endpoint); per-account ring buffer (`CLOUDTRAIL_MAX_EVENTS=10000`); `LookupEvents` supports all 8 AWS `LookupAttributes`; `IsLogging` flips on Start/StopLogging |
| **Resource Groups** | CreateGroup, GetGroup, DeleteGroup, UpdateGroup, ListGroups, GetGroupQuery, UpdateGroupQuery, GetGroupConfiguration, PutGroupConfiguration, GroupResources, UngroupResources, ListGroupResources, ListGroupingStatuses, SearchResources, Tag, Untag, GetTags, GetAccountSettings, UpdateAccountSettings | 19 of 23 spec ops; tag-sync ops omitted (not exposed by AWS CLI / Terraform); `Group` accepts bare name or full ARN |
| **Cost & Usage Reports** | DeleteReportDefinition, DescribeReportDefinitions, ListTagsForResource, ModifyReportDefinition, PutReportDefinition, TagResource, UntagResource | 7 of 7 spec ops |
| **Inspector2** | Enable, Disable, ListFindings, BatchGetFindingDetails, ListCoverage, ListCoverageStatistics, ListFindingAggregations, SearchVulnerabilities, TagResource, UntagResource, ListTagsForResource, CreateFilter, ListFilters, DeleteFilter | 14 operations; deterministic stub vulnerability findings for ECR images, Lambda functions, and EC2 instances; filtering, sorting, pagination |


### CloudFormation

| Feature | Details |
|---------|---------|
| **Stack Operations** | CreateStack, UpdateStack, DeleteStack, DescribeStacks, ListStacks, DescribeStackEvents, DescribeStackResource, DescribeStackResources, GetTemplate, ValidateTemplate, GetTemplateSummary |
| **Change Sets** | CreateChangeSet, DescribeChangeSet, ExecuteChangeSet, DeleteChangeSet, ListChangeSets |
| **Exports** | ListExports — cross-stack references via `Fn::ImportValue` |
| **Template Formats** | JSON and YAML (including `!Ref`, `!Sub`, `!GetAtt` shorthand tags) |
| **Intrinsic Functions** | Ref, Fn::GetAtt, Fn::Join, Fn::Sub (both forms), Fn::Select, Fn::Split, Fn::If, Fn::Equals, Fn::And, Fn::Or, Fn::Not, Fn::Base64, Fn::FindInMap, Fn::ImportValue, Fn::GetAZs, Fn::Cidr |
| **Pseudo-Parameters** | AWS::StackName, AWS::StackId, AWS::Region, AWS::AccountId, AWS::URLSuffix, AWS::Partition, AWS::NoValue |
| **Parameters** | Default values, AllowedValues validation, NoEcho masking, String/Number/CommaDelimitedList types |
| **Conditions** | Fn::Equals, Fn::And, Fn::Or, Fn::Not — conditional resource creation |
| **Rollback** | Configurable via `DisableRollback` — on failure, previously created resources are cleaned up in reverse dependency order |
| **Async Status** | Stacks deploy asynchronously (`CREATE_IN_PROGRESS` → `CREATE_COMPLETE`) — poll with DescribeStacks |

**Supported Resource Types:**

| Resource Type | Ref Returns | GetAtt |
|---------------|-------------|--------|
| `AWS::S3::Bucket` | Bucket name | Arn, DomainName, RegionalDomainName, WebsiteURL |
| `AWS::SQS::Queue` | Queue URL | Arn, QueueName, QueueUrl |
| `AWS::SNS::Topic` | Topic ARN | TopicArn, TopicName |
| `AWS::SNS::Subscription` | Subscription ARN | — |
| `AWS::DynamoDB::Table` | Table name | Arn, StreamArn |
| `AWS::Lambda::Function` | Function name | Arn |
| `AWS::IAM::Role` | Role name | Arn, RoleId |
| `AWS::IAM::Policy` | Policy ARN | — |
| `AWS::IAM::InstanceProfile` | Profile name | Arn |
| `AWS::SSM::Parameter` | Parameter name | Type, Value |
| `AWS::Logs::LogGroup` | Log group name | Arn |
| `AWS::Events::EventBus` | EventBus name | Arn, Name |
| `AWS::Events::Rule` | Rule name | Arn |
| `AWS::Kinesis::Stream` | Stream name | Arn, StreamId |
| `AWS::Lambda::Permission` | Statement ID | — |
| `AWS::Lambda::Version` | Version ARN | Version |
| `AWS::Lambda::Alias` | Alias ARN | — |
| `AWS::Lambda::EventSourceMapping` | UUID | — |
| `AWS::S3::BucketPolicy` | Bucket name | — |
| `AWS::SQS::QueuePolicy` | Policy ID | — |
| `AWS::SNS::TopicPolicy` | Policy ID | — |
| `AWS::ApiGateway::RestApi` | API ID | RootResourceId |
| `AWS::ApiGateway::Resource` | Resource ID | — |
| `AWS::ApiGateway::Method` | Method ID | — |
| `AWS::ApiGateway::Deployment` | Deployment ID | — |
| `AWS::ApiGateway::Stage` | Stage name | — |
| `AWS::AppConfig::Application` | Application ID | ApplicationId |
| `AWS::AppSync::GraphQLApi` | API ID | Arn, GraphQLUrl, ApiId |
| `AWS::AppSync::DataSource` | DataSource name | DataSourceArn |
| `AWS::AppSync::Resolver` | Resolver ARN | ResolverArn |
| `AWS::AppSync::GraphQLSchema` | Schema ID | — |
| `AWS::AppSync::ApiKey` | API key ID | ApiKey, Arn |
| `AWS::SecretsManager::Secret` | Secret ARN | — |
| `AWS::Cognito::UserPool` | Pool ID | Arn, ProviderName |
| `AWS::Cognito::UserPoolClient` | Client ID | — |
| `AWS::Cognito::IdentityPool` | Pool ID | — |
| `AWS::Cognito::UserPoolDomain` | Domain | — |
| `AWS::ECR::Repository` | Repo name | Arn, RepositoryUri |
| `AWS::IAM::ManagedPolicy` | Policy ARN | — |
| `AWS::KMS::Key` | Key ID | Arn, KeyId |
| `AWS::KMS::Alias` | Alias name | — |
| `AWS::EC2::VPC` | VPC ID | VpcId, DefaultSecurityGroup, DefaultNetworkAcl |
| `AWS::EC2::Subnet` | Subnet ID | SubnetId, AvailabilityZone |
| `AWS::EC2::SecurityGroup` | SG ID | GroupId, VpcId |
| `AWS::EC2::InternetGateway` | IGW ID | InternetGatewayId |
| `AWS::EC2::VPCGatewayAttachment` | Attachment ID | — |
| `AWS::EC2::RouteTable` | RTB ID | RouteTableId |
| `AWS::EC2::Route` | Route ID | — |
| `AWS::EC2::SubnetRouteTableAssociation` | Association ID | — |
| `AWS::EC2::LaunchTemplate` | LT ID | LaunchTemplateId, LaunchTemplateName, DefaultVersionNumber, LatestVersionNumber |
| `AWS::ECS::Cluster` | Cluster name | Arn, ClusterName |
| `AWS::ECS::TaskDefinition` | Task def ARN | TaskDefinitionArn |
| `AWS::ECS::Service` | Service ARN | ServiceArn, Name |
| `AWS::ElasticLoadBalancingV2::LoadBalancer` | LB ARN | Arn, DNSName, LoadBalancerFullName, CanonicalHostedZoneID, SecurityGroups |
| `AWS::ElasticLoadBalancingV2::Listener` | Listener ARN | ListenerArn, Arn |
| `AWS::Lambda::LayerVersion` | Layer version ARN | LayerVersionArn, Arn |
| `AWS::StepFunctions::StateMachine` | State machine ARN | Arn, Name |
| `AWS::Route53::HostedZone` | Zone ID | Id, NameServers |
| `AWS::Route53::RecordSet` | Record FQDN (trailing dot) | Name |
| `AWS::ApiGatewayV2::Api` | API ID | ApiId, ApiEndpoint |
| `AWS::ApiGatewayV2::Stage` | Stage ID | StageName |
| `AWS::ApiGatewayV2::Integration` | Integration ID | IntegrationId |
| `AWS::ApiGatewayV2::Route` | Route ID | RouteId |
| `AWS::SES::EmailIdentity` | Identity | EmailIdentity |
| `AWS::WAFv2::WebACL` | WebACL ID | Arn, Id |
| `AWS::CloudFront::Distribution` | Distribution ID | Arn, DomainName, Id |
| `AWS::CloudWatch::Alarm` | Alarm name | Arn |
| `AWS::RDS::DBCluster` | Cluster ID | Arn, Endpoint.Address, Endpoint.Port, ReadEndpoint.Address |
| `AWS::AutoScaling::AutoScalingGroup` | ASG name | Arn |
| `AWS::AutoScaling::LaunchConfiguration` | LC name | Arn |
| `AWS::AutoScaling::ScalingPolicy` | Policy ARN | Arn, PolicyName |
| `AWS::AutoScaling::LifecycleHook` | Hook name | LifecycleHookName |
| `AWS::AutoScaling::ScheduledAction` | Action ARN | Arn, ScheduledActionName |
| `AWS::Scheduler::Schedule` | Schedule name | Arn |
| `AWS::Scheduler::ScheduleGroup` | Group name | Arn |
| `AWS::CloudFormation::WaitCondition` | Condition ID | — |
| `AWS::CloudFormation::WaitConditionHandle` | Handle URL | — |
| `AWS::CloudFormation::Stack` (nested) | Child stack ARN | `Outputs.<Name>` — each child stack Output |

Unsupported resource types fail with `CREATE_FAILED` (or `ROLLBACK_COMPLETE` if rollback is enabled), so templates with unsupported types won't silently succeed.

### Infrastructure Services

| Service | Operations | Notes |
|---------|-----------|-------|
| **ECS** | CreateCluster, DeleteCluster, DescribeClusters, ListClusters, UpdateCluster, UpdateClusterSettings, PutClusterCapacityProviders, RegisterTaskDefinition, DeregisterTaskDefinition, DescribeTaskDefinition, ListTaskDefinitions, ListTaskDefinitionFamilies, DeleteTaskDefinitions, CreateService, DeleteService, DescribeServices, UpdateService, ListServices, ListServicesByNamespace, RunTask, StopTask, DescribeTasks, ListTasks, ExecuteCommand, UpdateTaskProtection, GetTaskProtection, CreateCapacityProvider, UpdateCapacityProvider, DeleteCapacityProvider, DescribeCapacityProviders, TagResource, UntagResource, ListTagsForResource, ListAccountSettings, PutAccountSetting, PutAccountSettingDefault, DeleteAccountSetting, PutAttributes, DeleteAttributes, ListAttributes, DescribeServiceDeployments, ListServiceDeployments, DescribeServiceRevisions, SubmitTaskStateChange, SubmitContainerStateChange, SubmitAttachmentStateChanges, DiscoverPollEndpoint | 47 actions; `RunTask` starts real Docker containers via Docker socket; full Terraform ECS coverage |
| **RDS** | CreateDBInstance, DeleteDBInstance, DescribeDBInstances, ModifyDBInstance, StartDBInstance, StopDBInstance, RebootDBInstance, CreateDBInstanceReadReplica, RestoreDBInstanceFromDBSnapshot, CreateDBCluster, DeleteDBCluster, DescribeDBClusters, ModifyDBCluster, StartDBCluster, StopDBCluster, CreateDBSnapshot, DeleteDBSnapshot, DescribeDBSnapshots, CreateDBClusterSnapshot, DescribeDBClusterSnapshots, DeleteDBClusterSnapshot, CreateDBSubnetGroup, DeleteDBSubnetGroup, DescribeDBSubnetGroups, ModifyDBSubnetGroup, CreateDBParameterGroup, DeleteDBParameterGroup, DescribeDBParameterGroups, DescribeDBParameters, ModifyDBParameterGroup, CreateDBClusterParameterGroup, DescribeDBClusterParameterGroups, DeleteDBClusterParameterGroup, DescribeDBClusterParameters, ModifyDBClusterParameterGroup, CreateOptionGroup, DeleteOptionGroup, DescribeOptionGroups, DescribeOptionGroupOptions, ListTagsForResource, AddTagsToResource, RemoveTagsFromResource, DescribeDBEngineVersions, DescribeOrderableDBInstanceOptions, CreateGlobalCluster, DescribeGlobalClusters, DeleteGlobalCluster, RemoveFromGlobalCluster, ModifyGlobalCluster | `CreateDBInstance` spins up real Postgres/MySQL Docker container, returns actual `host:port` endpoint |
| **ElastiCache** | CreateCacheCluster, DeleteCacheCluster, DescribeCacheClusters, ModifyCacheCluster, RebootCacheCluster, CreateReplicationGroup, DeleteReplicationGroup, DescribeReplicationGroups, ModifyReplicationGroup, IncreaseReplicaCount, DecreaseReplicaCount, CreateCacheSubnetGroup, DescribeCacheSubnetGroups, ModifyCacheSubnetGroup, DeleteCacheSubnetGroup, CreateCacheParameterGroup, DescribeCacheParameterGroups, ModifyCacheParameterGroup, ResetCacheParameterGroup, DeleteCacheParameterGroup, DescribeCacheParameters, DescribeCacheEngineVersions, CreateUser, DescribeUsers, DeleteUser, ModifyUser, CreateUserGroup, DescribeUserGroups, DeleteUserGroup, ModifyUserGroup, ListTagsForResource, AddTagsToResource, RemoveTagsFromResource, CreateSnapshot, DeleteSnapshot, DescribeSnapshots, DescribeEvents | `CreateCacheCluster` spins up real Redis/Memcached Docker container |
| **Glue** | CreateDatabase, DeleteDatabase, GetDatabase, GetDatabases, UpdateDatabase, CreateTable, DeleteTable, GetTable, GetTables, UpdateTable, BatchDeleteTable, CreatePartition, DeletePartition, GetPartition, GetPartitions, BatchCreatePartition, BatchGetPartition, CreatePartitionIndex, GetPartitionIndexes, CreateConnection, DeleteConnection, GetConnection, GetConnections, CreateCrawler, DeleteCrawler, GetCrawler, GetCrawlers, UpdateCrawler, StartCrawler, StopCrawler, GetCrawlerMetrics, CreateJob, DeleteJob, GetJob, GetJobs, UpdateJob, StartJobRun, GetJobRun, GetJobRuns, BatchStopJobRun, CreateTrigger, GetTrigger, DeleteTrigger, UpdateTrigger, StartTrigger, StopTrigger, ListTriggers, BatchGetTriggers, GetTriggers, CreateWorkflow, GetWorkflow, DeleteWorkflow, UpdateWorkflow, StartWorkflowRun, CreateSecurityConfiguration, DeleteSecurityConfiguration, GetSecurityConfiguration, GetSecurityConfigurations, CreateClassifier, GetClassifier, GetClassifiers, DeleteClassifier, TagResource, UntagResource, GetTags | Python shell jobs actually execute via subprocess; Spark jobs run on the official `amazon/aws-glue-libs` PySpark image (`GlueVersion: 4.0` / `3.0`) on MiniStack's Docker network |
| **S3 Tables** | CreateTableBucket, ListTableBuckets, GetTableBucket, DeleteTableBucket, CreateNamespace, ListNamespaces, GetNamespace, DeleteNamespace, CreateTable, ListTables, GetTable, DeleteTable, GetTableMetadataLocation, UpdateTableMetadataLocation | Iceberg-format tables; embedded Iceberg REST catalog at `/iceberg` so Spark jobs (`spark.sql.catalog.*.type=rest`) can create / load / commit without an external catalog; data files in S3, metadata in memory |
| **Athena** | StartQueryExecution, GetQueryExecution, GetQueryResults, StopQueryExecution, ListQueryExecutions, BatchGetQueryExecution, CreateWorkGroup, DeleteWorkGroup, GetWorkGroup, ListWorkGroups, UpdateWorkGroup, CreateNamedQuery, DeleteNamedQuery, GetNamedQuery, ListNamedQueries, BatchGetNamedQuery, CreateDataCatalog, GetDataCatalog, ListDataCatalogs, DeleteDataCatalog, UpdateDataCatalog, CreatePreparedStatement, GetPreparedStatement, DeletePreparedStatement, ListPreparedStatements, GetTableMetadata, ListTableMetadata, TagResource, UntagResource, ListTagsForResource | Real SQL via **DuckDB** when installed (`pip install duckdb`), otherwise returns mock results; result pagination; column type metadata |
| **Firehose** | CreateDeliveryStream, DeleteDeliveryStream, DescribeDeliveryStream, ListDeliveryStreams, PutRecord, PutRecordBatch, UpdateDestination, TagDeliveryStream, UntagDeliveryStream, ListTagsForDeliveryStream, StartDeliveryStreamEncryption, StopDeliveryStreamEncryption | S3 destinations write records to the local S3 emulator; all other destination types buffer in-memory; concurrency-safe `UpdateDestination` via `VersionId` |
| **Route53** | CreateHostedZone, GetHostedZone, DeleteHostedZone, ListHostedZones, ListHostedZonesByName, UpdateHostedZoneComment, ChangeResourceRecordSets (CREATE/UPSERT/DELETE), ListResourceRecordSets, GetChange, CreateHealthCheck, GetHealthCheck, DeleteHealthCheck, ListHealthChecks, UpdateHealthCheck, ChangeTagsForResource, ListTagsForResource | REST/XML protocol; SOA + NS records auto-created; CallerReference idempotency; alias records, weighted/failover/latency routing; marker-based pagination |
| **EC2** | RunInstances, DescribeInstances, DescribeInstanceAttribute, DescribeInstanceTypes, DescribeVpcAttribute, TerminateInstances, StopInstances, StartInstances, RebootInstances, DescribeImages, CreateSecurityGroup, DeleteSecurityGroup, DescribeSecurityGroups, AuthorizeSecurityGroupIngress, RevokeSecurityGroupIngress, AuthorizeSecurityGroupEgress, RevokeSecurityGroupEgress, DescribeSecurityGroupRules, CreateKeyPair, DeleteKeyPair, DescribeKeyPairs, ImportKeyPair, CreateVpc, DeleteVpc, DescribeVpcs, ModifyVpcAttribute, CreateSubnet, DeleteSubnet, DescribeSubnets, ModifySubnetAttribute, CreateInternetGateway, DeleteInternetGateway, DescribeInternetGateways, AttachInternetGateway, DetachInternetGateway, CreateRouteTable, DeleteRouteTable, DescribeRouteTables, AssociateRouteTable, DisassociateRouteTable, ReplaceRouteTableAssociation, CreateRoute, ReplaceRoute, DeleteRoute, CreateNetworkInterface, DeleteNetworkInterface, DescribeNetworkInterfaces, AttachNetworkInterface, DetachNetworkInterface, CreateVpcEndpoint, DeleteVpcEndpoints, DescribeVpcEndpoints, ModifyVpcEndpoint, DescribePrefixLists, DescribeAvailabilityZones, AllocateAddress, ReleaseAddress, AssociateAddress, DisassociateAddress, DescribeAddresses, DescribeAddressesAttribute, CreateTags, DeleteTags, DescribeTags, CreateNatGateway, DescribeNatGateways, DeleteNatGateway, CreateNetworkAcl, DescribeNetworkAcls, DeleteNetworkAcl, CreateNetworkAclEntry, DeleteNetworkAclEntry, ReplaceNetworkAclEntry, ReplaceNetworkAclAssociation, CreateFlowLogs, DescribeFlowLogs, DeleteFlowLogs, CreateVpcPeeringConnection, AcceptVpcPeeringConnection, DescribeVpcPeeringConnections, DeleteVpcPeeringConnection, CreateDhcpOptions, AssociateDhcpOptions, DescribeDhcpOptions, DeleteDhcpOptions, CreateEgressOnlyInternetGateway, DescribeEgressOnlyInternetGateways, DeleteEgressOnlyInternetGateway, CreateManagedPrefixList, DescribeManagedPrefixLists, GetManagedPrefixListEntries, ModifyManagedPrefixList, DeleteManagedPrefixList, CreateVpnGateway, DescribeVpnGateways, AttachVpnGateway, DetachVpnGateway, DeleteVpnGateway, EnableVgwRoutePropagation, DisableVgwRoutePropagation, CreateCustomerGateway, DescribeCustomerGateways, DeleteCustomerGateway, DescribeInstanceCreditSpecifications, DescribeInstanceMaintenanceOptions, DescribeInstanceAutoRecoveryAttribute, ModifyInstanceMaintenanceOptions, DescribeInstanceTopology, DescribeSpotInstanceRequests, DescribeCapacityReservations, DescribeInstanceStatus, DescribeVpcClassicLink, DescribeVpcClassicLinkDnsSupport, CreateLaunchTemplate, CreateLaunchTemplateVersion, DescribeLaunchTemplates, DescribeLaunchTemplateVersions, ModifyLaunchTemplate, DeleteLaunchTemplate | 136 actions; `AuthorizeSecurityGroupIngress` is idempotent on duplicate rules (same behavior as egress; avoids Terraform re-apply failures); in-memory state only — no real VMs; CreateVpc provisions per-VPC default route table, network ACL, and security group; full Terraform VPC module v6.6.0 compatible; VPN/Customer gateways, managed prefix lists, VPC endpoints with modify support; launch templates with versioning ($Latest/$Default) |
| **EBS** | CreateVolume, DeleteVolume, DescribeVolumes, DescribeVolumeStatus, AttachVolume, DetachVolume, ModifyVolume, DescribeVolumesModifications, EnableVolumeIO, ModifyVolumeAttribute, DescribeVolumeAttribute, CreateSnapshot, DeleteSnapshot, DescribeSnapshots, CopySnapshot, ModifySnapshotAttribute, DescribeSnapshotAttribute | Part of EC2 Query/XML service; attach/detach updates volume state; snapshots stored as completed immediately; Pro-only on LocalStack — free here |
| **EFS** | CreateFileSystem, DescribeFileSystems, DeleteFileSystem, UpdateFileSystem, CreateMountTarget, DescribeMountTargets, DeleteMountTarget, DescribeMountTargetSecurityGroups, ModifyMountTargetSecurityGroups, CreateAccessPoint, DescribeAccessPoints, DeleteAccessPoint, TagResource, UntagResource, ListTagsForResource, PutLifecycleConfiguration, DescribeLifecycleConfiguration, PutBackupPolicy, DescribeBackupPolicy, DescribeAccountPreferences, PutAccountPreferences | REST/JSON `/2015-02-01/*`; CreationToken idempotency; FileSystem deletion blocked when mount targets exist; Pro-only on LocalStack — free here |
| **EMR** | RunJobFlow, DescribeCluster, ListClusters, TerminateJobFlows, ModifyCluster, SetTerminationProtection, SetVisibleToAllUsers, AddJobFlowSteps, DescribeStep, ListSteps, CancelSteps, AddInstanceFleet, ListInstanceFleets, ModifyInstanceFleet, AddInstanceGroups, ListInstanceGroups, ModifyInstanceGroups, ListBootstrapActions, AddTags, RemoveTags, GetBlockPublicAccessConfiguration, PutBlockPublicAccessConfiguration | Control plane only — no real Spark/Hadoop; clusters start in WAITING (KeepAlive=true) or TERMINATED (KeepAlive=false); steps stored as COMPLETED immediately; all three instance modes (simple, InstanceGroups, InstanceFleets); TerminationProtected enforced; Pro-only on LocalStack — free here |
| **Cognito** | **User Pools**: CreateUserPool, DeleteUserPool, DescribeUserPool, ListUserPools, UpdateUserPool, CreateUserPoolClient, DeleteUserPoolClient, DescribeUserPoolClient, ListUserPoolClients, UpdateUserPoolClient, AdminCreateUser, AdminDeleteUser, AdminGetUser, ListUsers, AdminSetUserPassword, AdminUpdateUserAttributes, AdminConfirmSignUp, AdminDisableUser, AdminEnableUser, AdminResetUserPassword, AdminUserGlobalSignOut, AdminAddUserToGroup, AdminRemoveUserFromGroup, AdminListGroupsForUser, AdminListUserAuthEvents, AdminInitiateAuth, AdminRespondToAuthChallenge, InitiateAuth, RespondToAuthChallenge, GlobalSignOut, RevokeToken, SignUp, ConfirmSignUp, ForgotPassword, ConfirmForgotPassword, ChangePassword, GetUser, UpdateUserAttributes, DeleteUser, CreateGroup, DeleteGroup, GetGroup, ListGroups, ListUsersInGroup, CreateUserPoolDomain, DeleteUserPoolDomain, DescribeUserPoolDomain, GetUserPoolMfaConfig, SetUserPoolMfaConfig, AssociateSoftwareToken, VerifySoftwareToken, AdminSetUserMFAPreference, SetUserMFAPreference, TagResource, UntagResource, ListTagsForResource; **Identity Pools**: CreateIdentityPool, DeleteIdentityPool, DescribeIdentityPool, ListIdentityPools, UpdateIdentityPool, GetId, GetCredentialsForIdentity, GetOpenIdToken, SetIdentityPoolRoles, GetIdentityPoolRoles, ListIdentities, DescribeIdentity, MergeDeveloperIdentities, UnlinkDeveloperIdentity, UnlinkIdentity, TagResource, UntagResource, ListTagsForResource; **OAuth2**: /oauth2/token (client_credentials) | Stub JWT tokens (structurally valid base64url JWTs); SRP auth returns PASSWORD_VERIFIER challenge; confirmation codes hardcoded (signup: 123456, forgot-password: 654321); TOTP SOFTWARE_TOKEN_MFA challenge flow; MFA config and per-user enrollment stored in-memory |
| **ECR** | CreateRepository, DescribeRepositories, DeleteRepository, ListImages, DescribeImages, PutImage, BatchGetImage, BatchDeleteImage, GetAuthorizationToken, GetRepositoryPolicy, SetRepositoryPolicy, DeleteRepositoryPolicy, PutLifecyclePolicy, GetLifecyclePolicy, DeleteLifecyclePolicy, ListTagsForResource, TagResource, UntagResource, PutImageTagMutability, PutImageScanningConfiguration, DescribeRegistry, GetDownloadUrlForLayer, BatchCheckLayerAvailability, InitiateLayerUpload, UploadLayerPart, CompleteLayerUpload | In-memory image registry; Docker V2 manifest support; authorization token generation; lifecycle policies; tag mutability; Pro-only on LocalStack — free here |
| **AppSync** | **GraphQL APIs**: CreateGraphQLApi, GetGraphQLApi, ListGraphQLApis, UpdateGraphQLApi, DeleteGraphQLApi, CreateApiKey, DeleteApiKey, ListApiKeys, CreateDataSource, GetDataSource, ListDataSources, DeleteDataSource, CreateResolver, GetResolver, ListResolvers, DeleteResolver, CreateType, ListTypes, GetType, TagResource, UntagResource, ListTagsForResource; **Event APIs**: CreateApi, GetApi, ListApis, UpdateApi, DeleteApi, CreateChannelNamespace, GetChannelNamespace, ListChannelNamespaces, UpdateChannelNamespace, DeleteChannelNamespace, CreateApiKey, ListApiKeys, DeleteApiKey, HTTP Publish, WebSocket subscribe/publish | GraphQL queries/mutations execute against DynamoDB resolvers; Lambda resolvers supported. Event APIs support AWS-shaped `/v2/apis` management, `/v1/apis/{apiId}/apikeys` API-key operations, `POST /event` on `*.appsync-api.*`, and realtime `*.appsync-realtime-api.*` WebSocket flows with API-key and Lambda-authorizer checks |
| **Cloud Map** | CreateHttpNamespace, CreatePrivateDnsNamespace, CreatePublicDnsNamespace, GetNamespace, ListNamespaces, DeleteNamespace, UpdateHttpNamespace, UpdatePrivateDnsNamespace, UpdatePublicDnsNamespace, CreateService, GetService, ListServices, DeleteService, UpdateService, RegisterInstance, DeregisterInstance, DiscoverInstances, DiscoverInstancesRevision, ListInstances, GetInstancesHealthStatus, UpdateInstanceCustomHealthStatus, GetServiceAttributes, UpdateServiceAttributes, DeleteServiceAttributes, GetOperation, ListOperations, TagResource, UntagResource, ListTagsForResource | DNS namespaces create Route53 hosted zones; operation tracking; Terraform `aws_service_discovery_*` compatible |
| **RDS Data API** | ExecuteStatement, BatchExecuteStatement, BeginTransaction, CommitTransaction, RollbackTransaction | Routes SQL to real Docker-backed RDS database containers; supports MySQL (pymysql) and PostgreSQL (psycopg2); REST paths (`/Execute`, `/BeginTransaction`, etc.) |
| **S3 Files** | CreateFileSystem, GetFileSystem, ListFileSystems, DeleteFileSystem, CreateMountTarget, GetMountTarget, ListMountTargets, UpdateMountTarget, DeleteMountTarget, CreateAccessPoint, GetAccessPoint, ListAccessPoints, DeleteAccessPoint, GetFileSystemPolicy, PutFileSystemPolicy, DeleteFileSystemPolicy, GetSynchronizationConfiguration, PutSynchronizationConfiguration, TagResource, UntagResource, ListTagsForResource | 21 operations; control plane for the new S3 Files service (launched April 2026); file systems, mount targets, access points, policies |
| **AutoScaling** | CreateAutoScalingGroup, DescribeAutoScalingGroups, UpdateAutoScalingGroup, DeleteAutoScalingGroup, DescribeAutoScalingInstances, CreateLaunchConfiguration, DescribeLaunchConfigurations, DeleteLaunchConfiguration, PutScalingPolicy, DescribePolicies, DeletePolicy, PutLifecycleHook, DescribeLifecycleHooks, DeleteLifecycleHook, CompleteLifecycleAction, RecordLifecycleActionHeartbeat, PutScheduledUpdateGroupAction, DescribeScheduledActions, DeleteScheduledAction, CreateOrUpdateTags, DescribeTags, DeleteTags | 23 actions; in-memory state — no real instance scaling; full ASG lifecycle (launch configs, scaling policies, lifecycle hooks, scheduled actions, tags); CDK/Terraform compatible |
| **CodeBuild** | CreateProject, BatchGetProjects, ListProjects, UpdateProject, DeleteProject, StartBuild, BatchGetBuilds, StopBuild, ListBuilds, ListBuildsForProject, BatchDeleteBuilds | 11 actions; builds complete immediately with SUCCEEDED status; project and build metadata stored in-memory |
| **AppConfig** | CreateApplication, GetApplication, ListApplications, UpdateApplication, DeleteApplication, CreateEnvironment, GetEnvironment, ListEnvironments, UpdateEnvironment, DeleteEnvironment, CreateConfigurationProfile, GetConfigurationProfile, ListConfigurationProfiles, UpdateConfigurationProfile, DeleteConfigurationProfile, CreateHostedConfigurationVersion, GetHostedConfigurationVersion, ListHostedConfigurationVersions, DeleteHostedConfigurationVersion, CreateDeploymentStrategy, GetDeploymentStrategy, ListDeploymentStrategies, UpdateDeploymentStrategy, DeleteDeploymentStrategy, StartDeployment, GetDeployment, ListDeployments, StopDeployment, TagResource, UntagResource, ListTagsForResource, StartConfigurationSession, GetLatestConfiguration | 33 operations; control plane + data plane; hosted configuration versions; deployments complete immediately; session-based configuration retrieval with token rotation |
| **Transfer Family** | CreateServer, DescribeServer, DeleteServer, ListServers, StartServer, StopServer, CreateUser, DescribeUser, DeleteUser, ListUsers, ImportSshPublicKey, DeleteSshPublicKey | 12 operations; **real SFTP listener** on `:2222` (override with `SFTP_PORT`) backed by S3 — `ls`, `put`, `get`, `mkdir`, `rename` work end-to-end against the local S3 emulator; `SFTP_PORT_PER_SERVER=1` allocates one port per `CreateServer` from `SFTP_BASE_PORT` (default 2300); SSH key auth scans every user across every server and account; `LOGICAL` and `PATH` home-directory mappings; host key persists across restarts when `PERSIST_STATE=1` |
| **IoT Core** | CreateThing, DescribeThing, ListThings, UpdateThing, DeleteThing, CreateThingType, CreateThingGroup, AddThingToThingGroup, ListThingsInThingGroup, CreateKeysAndCertificate, RegisterCertificate, UpdateCertificate, DeleteCertificate, AttachThingPrincipal, DetachThingPrincipal, ListThingPrincipals, ListPrincipalThings, CreatePolicy, CreatePolicyVersion, AttachPolicy, DetachPolicy, ListAttachedPolicies, ListTargetsForPolicy, DescribeEndpoint, Publish (HTTP), MQTT-over-WebSocket pub/sub | 24 operations + **real MQTT 3.1.1 over WebSocket** on the gateway port — clients use the address returned by `DescribeEndpoint`; local CA signs `CreateKeysAndCertificate` certificates (CA persists across restarts when `PERSIST_STATE=1`); multi-tenancy via transparent topic prefixing (same pattern as Transfer Family's shared SFTP listener); no plain TCP 1883 (real AWS IoT requires TLS or SigV4 on every connection); local CA cert available at `GET /_ministack/iot/ca.pem` for SDK trust configuration; IoT policies are stored but **not enforced** on the data plane |
| **EventBridge Scheduler** | CreateSchedule, GetSchedule, UpdateSchedule, DeleteSchedule, ListSchedules, CreateScheduleGroup, GetScheduleGroup, DeleteScheduleGroup, ListScheduleGroups, TagResource, UntagResource, ListTagsForResource | 12 actions; schedule groups with cascading deletes; `rate()`, `cron()`, `at()` expressions; group/prefix/state filters on list; default group auto-created; CFN `AWS::Scheduler::Schedule` and `AWS::Scheduler::ScheduleGroup` supported |
| **EKS** | CreateCluster, DescribeCluster, ListClusters, DeleteCluster, CreateNodegroup, DescribeNodegroup, ListNodegroups, DeleteNodegroup, TagResource, UntagResource, ListTagsForResource | 11 operations; `CreateCluster` spawns a real **k3s** container (75 MB) with a full Kubernetes API server; `kubectl`, Helm, and any K8s tooling work out of the box; cascading delete removes nodegroups and k3s container; CFN `AWS::EKS::Cluster` and `AWS::EKS::Nodegroup` supported |
| **OpenSearch Service** | CreateDomain, DescribeDomain, DescribeDomains, DeleteDomain, ListDomainNames, UpdateDomainConfig, DescribeDomainConfig, DescribeDomainChangeProgress, ListVersions, GetCompatibleVersions, AddTags, ListTags, RemoveTags | Management plane on `/2021-01-01/*` (`boto3.client("opensearch")`, SigV4 scope `es`). Default data plane is a stub endpoint (`<domain>.ministack.local:9200`) — set `OPENSEARCH_DATAPLANE=1` to spawn one real `opensearchproject/opensearch` container per `CreateDomain` (same pattern as ElastiCache/RDS); `DescribeDomain.Endpoint` then points at that container and `_cluster/health`/`/_search` work end-to-end. Add `OPENSEARCH_DASHBOARDS=1` (with dataplane on) to also spawn a per-domain `opensearch-dashboards` sidecar; `DescribeDomain.DashboardEndpoint` is populated. ARNs follow `arn:aws:es:<region>:<account>:domain/<name>`; Terraform `aws_opensearch_domain` resource compatible. |
| **Organizations** | DescribeOrganization, ListRoots, ListAccounts, ListAccountsForParent, DescribeAccount, CreateOrganizationalUnit, DescribeOrganizationalUnit, DeleteOrganizationalUnit, ListOrganizationalUnitsForParent | Single-master-account org auto-initialised on first call; nested OUs with `Path` field (additive 2026-03 AWS change); `FeatureSet=ALL` |
| **Account** | GetAccountInformation, GetContactInformation, ListRegions, GetRegionOptStatus | rest-json `/getAccountInformation`, etc.; returns `AccountState=ACTIVE` (additive 2026-04 AWS change); 31-region opt-in matrix |
| **WAF (Classic + Regional)** | List* (17 ops), Get*, GetChangeToken, GetChangeTokenStatus, GetPermissionPolicy, Create*/Update*/Delete* (stubbed) | Minimal v1 stub — empty lists for all `List*`, valid ChangeToken on Create/Update/Delete; for full features use **wafv2** (also supported) |
| **Batch** | CreateComputeEnvironment, DescribeComputeEnvironments, CreateJobQueue, DescribeJobQueues, RegisterJobDefinition, DescribeJobDefinitions, SubmitJob, DescribeJobs, ListJobs | Control-plane stub — submitted jobs immediately transition to `SUCCEEDED`; multi-revision job definitions; `jobQueue` lookup by name or ARN; account-scoped state |

---

## Real Database Endpoints (RDS)

When you create an RDS instance, MiniStack starts a real database container and returns the actual connection endpoint:

```python
import boto3
import psycopg2  # pip install psycopg2-binary

rds = boto3.client("rds", endpoint_url="http://localhost:4566",
                   aws_access_key_id="test", aws_secret_access_key="test", region_name="us-east-1")

resp = rds.create_db_instance(
    DBInstanceIdentifier="mydb",
    DBInstanceClass="db.t3.micro",
    Engine="postgres",
    MasterUsername="admin",
    MasterUserPassword="password",
    DBName="appdb",
    AllocatedStorage=20,
)

endpoint = resp["DBInstance"]["Endpoint"]
# Connect directly — it's a real Postgres instance
conn = psycopg2.connect(
    host=endpoint["Address"],   # localhost
    port=endpoint["Port"],      # 15432 (auto-assigned)
    user="admin",
    password="password",
    dbname="appdb",
)
```

Supported engines: `postgres`, `mysql`, `mariadb`, `aurora-postgresql`, `aurora-mysql`

---

## Real Redis Endpoints (ElastiCache)

```python
import boto3
import redis  # pip install redis

ec = boto3.client("elasticache", endpoint_url="http://localhost:4566",
                  aws_access_key_id="test", aws_secret_access_key="test", region_name="us-east-1")

resp = ec.create_cache_cluster(
    CacheClusterId="my-redis",
    Engine="redis",
    CacheNodeType="cache.t3.micro",
    NumCacheNodes=1,
)

node = resp["CacheCluster"]["CacheNodes"][0]["Endpoint"]
r = redis.Redis(host=node["Address"], port=node["Port"])
r.set("key", "value")
print(r.get("key"))  # b'value'
```

A Redis sidecar is also always available at `localhost:6379` when using Docker Compose.

---

## Athena with Real SQL

> **Requires the full image** (`ministackorg/ministack:full`). The default light image ships without DuckDB and returns mocked results — `SELECT 1+1` will return `1`, not `2`.

Athena queries run via DuckDB and can query files in your local S3 data directory:

```python
import boto3, time

athena = boto3.client("athena", endpoint_url="http://localhost:4566",
                      aws_access_key_id="test", aws_secret_access_key="test", region_name="us-east-1")

# Query runs real SQL via DuckDB
resp = athena.start_query_execution(
    QueryString="SELECT 42 AS answer, 'hello' AS greeting",
    ResultConfiguration={"OutputLocation": "s3://athena-results/"},
)
query_id = resp["QueryExecutionId"]

# Poll for result
while True:
    status = athena.get_query_execution(QueryExecutionId=query_id)
    if status["QueryExecution"]["Status"]["State"] == "SUCCEEDED":
        break
    time.sleep(0.1)

results = athena.get_query_results(QueryExecutionId=query_id)
for row in results["ResultSet"]["Rows"][1:]:  # skip header
    print([col["VarCharValue"] for col in row["Data"]])
# ['42', 'hello']
```

---

## ECS with Real Containers

```python
import boto3

ecs = boto3.client("ecs", endpoint_url="http://localhost:4566",
                   aws_access_key_id="test", aws_secret_access_key="test", region_name="us-east-1")

ecs.create_cluster(clusterName="dev")

ecs.register_task_definition(
    family="web",
    containerDefinitions=[{
        "name": "nginx",
        "image": "nginx:alpine",
        "cpu": 128,
        "memory": 256,
        "portMappings": [{"containerPort": 80, "hostPort": 8080}],
    }],
)

# This actually runs an nginx container via Docker
resp = ecs.run_task(cluster="dev", taskDefinition="web", count=1)
task_arn = resp["tasks"][0]["taskArn"]

# Stop it (removes the container)
ecs.stop_task(cluster="dev", task=task_arn)
```

Each container also gets the standard ECS Task Metadata V4 endpoint injected as
`ECS_CONTAINER_METADATA_URI_V4`, so anything inside the container that uses the
AWS SDK for ECS metadata (X-Ray, OpenTelemetry, application code calling
`GET $ECS_CONTAINER_METADATA_URI_V4/task`) works without changes. Alongside it,
`RunTask` injects `AWS_CONTAINER_CREDENTIALS_FULL_URI`,
`AWS_CONTAINER_AUTHORIZATION_TOKEN`, and `AWS_ENDPOINT_URL` so unmodified AWS
SDKs running inside the task fetch emulated credentials from the new
`/v2/credentials/<uuid>` endpoint and route service calls through MiniStack
end-to-end without any client config.

---

## Configuration

| Variable | Default | Description |
|----------|---------|-------------|
| `GATEWAY_PORT` | `4566` | Port to listen on. Also accepts `EDGE_PORT` (LocalStack compatibility alias) |
| `MINISTACK_HOST` | `localhost` | Hostname used in response URLs (SQS queues, SNS subscriptions, API Gateway endpoints, Lambda layers) |
| `MINISTACK_ACCOUNT_ID` | `000000000000` | Default AWS account ID. Overridden per-request when `AWS_ACCESS_KEY_ID` is a 12-digit number (see [Multi-Tenancy](#multi-tenancy)) |
| `MINISTACK_REGION` | `us-east-1` | AWS region reported in ARNs and service responses across all services |
| `LOG_LEVEL` | `INFO` | `DEBUG`, `INFO`, `WARNING`, `ERROR` |
| `S3_PERSIST` | `0` | Set `1` to persist S3 objects to disk |
| `S3_DATA_DIR` | `/tmp/ministack-data/s3` | S3 persistence directory |
| `REDIS_HOST` | `redis` | Redis host for ElastiCache fallback |
| `REDIS_PORT` | `6379` | Redis port for ElastiCache fallback |
| `RDS_BASE_PORT` | `15432` | Starting host port for RDS containers |
| `RDS_TMPFS_SIZE` | `256m` | Tmpfs size for RDS database containers (when `RDS_PERSIST=0`). Set to `2g` or higher for large databases |
| `GLUE_DOCKER_IMAGE` | (auto by `GlueVersion`) | Override the `amazon/aws-glue-libs` PySpark image used for Spark Glue jobs. Defaults: `glue_libs_4.0.0_image_01` (GlueVersion 4.0), `glue_libs_3.0.0_image_01` (GlueVersion 3.0) |
| `RDS_PERSIST` | `0` | Set `1` to use Docker named volumes for RDS containers instead of tmpfs. Storage grows dynamically with no fixed cap |
| `DOCKER_NETWORK` | _(unset)_ | Docker network for all container-backed services (RDS, EKS, ElastiCache, Lambda). Set to your Docker Compose network name so containers can reach each other. Replaces `LAMBDA_DOCKER_NETWORK` |
| `ELASTICACHE_BASE_PORT` | `16379` | Starting host port for ElastiCache containers |
| `ELASTICACHE_CLUSTER_MODE_REAL` | `0` | Set `1` (requires `DOCKER_NETWORK`) to provision real Redis Cluster replication groups: `NumNodeGroups × (1+ReplicasPerNodeGroup)` cluster-enabled nodes wired with `redis-cli --cluster create`, serving real `CLUSTER SLOTS` / `MOVED` redirects for cluster-aware clients |
| `OPENSEARCH_DATAPLANE` | `0` | Set `1` to spawn a real `opensearchproject/opensearch` container per `CreateDomain`. Default `0` returns a stub endpoint (`<domain>.ministack.local:9200`) — management plane only |
| `OPENSEARCH_BASE_PORT` | `14571` | Starting host port for per-domain OpenSearch containers when `OPENSEARCH_DATAPLANE=1` |
| `OPENSEARCH_IMAGE` | `opensearchproject/opensearch:2.15.0` | Image used when spawning per-domain OpenSearch containers |
| `OPENSEARCH_DASHBOARDS` | `0` | Set `1` (together with `OPENSEARCH_DATAPLANE=1`) to also spawn a per-domain `opensearchproject/opensearch-dashboards` sidecar; `DescribeDomain.DashboardEndpoint` is populated |
| `OPENSEARCH_DASHBOARDS_BASE_PORT` | `15601` | Starting host port for per-domain Dashboards containers |
| `OPENSEARCH_DASHBOARDS_IMAGE` | `opensearchproject/opensearch-dashboards:2.15.0` | Image used when spawning per-domain Dashboards containers |
| `MINISTACK_OPENSEARCH_ENDPOINT` | _(unset)_ | If set (e.g. `localhost:9200`), every domain returns this endpoint instead of spawning a per-domain container — useful when you bring your own cluster |
| `PERSIST_STATE` | `0` | Set `1` to persist service state across restarts |
| `STATE_DIR` | `/tmp/ministack-state` | Directory for persisted state files |
| `LAMBDA_EXECUTOR` | `local` | Lambda execution mode: `local` (subprocess) or `docker` (container). `provided` runtimes and `PackageType: Image` always use Docker |
| `LAMBDA_STRICT` | `0` | Set `1` for AWS-fidelity mode: every Lambda invocation runs in a Docker container via the AWS RIE image; in-process fallbacks are disabled. Missing Docker surfaces as `Runtime.DockerUnavailable` instead of degrading to a subprocess. Opt-in because the default install doesn't require Docker |
| `LAMBDA_DOCKER_NETWORK` | _(unset)_ | Legacy alias for `DOCKER_NETWORK` (Lambda only). Prefer `DOCKER_NETWORK` which covers all services |
| `LAMBDA_DOCKER_FLAGS` | _(unset)_ | Extra `docker run` flags injected into Lambda containers (matches LocalStack). Supports `-e` / `-v` / `--dns` / `--network` / `--cap-add` / `-m` / `--shm-size` / `--tmpfs` / `--add-host` / `--privileged` / `--read-only`. Useful for TLS proxies, custom CAs, and routed dev networks |
| `MINISTACK_IMAGE_PREFIX` | _(unset)_ | Private-registry prefix prepended to every nested container image (RDS postgres/mysql/mariadb, ElastiCache redis/memcached, EKS k3s, Lambda runtimes). Testcontainers' `hub.image.name.prefix` is auto-forwarded into this var by the Java/Python modules |
| `LAMBDA_WARM_TTL_SECONDS` | `300` | How long an idle warm Lambda container stays in the pool before the reaper evicts it |
| `LAMBDA_ACCOUNT_CONCURRENCY` | `0` | Account-level concurrent-invocation cap (0 = unbounded). Match real AWS by setting to `1000`. Used to simulate `ConcurrentInvocationLimitExceeded` throttles |
| `SFN_MOCK_CONFIG` | _(unset)_ | Path to JSON file for Step Functions mock testing; compatible with AWS SFN Local format. Also accepts `LOCALSTACK_SFN_MOCK_CONFIG` |
| `ATHENA_ENGINE` | `auto` | SQL engine for Athena: `auto`, `duckdb`, `mock` |
| `SMTP_HOST` | _(unset)_ | SMTP server for SES email relay (e.g. `mailhog:1025`). When set, SES SendEmail/SendRawEmail actually deliver mail. When unset, emails are stored in-memory only |
| `MINISTACK_APIGW_PROXY_TIMEOUT_SECONDS` | `30` | API Gateway v1/v2 HTTP `HTTP_PROXY` / `HTTP` integration: upstream request timeout (seconds) |
| `MINISTACK_APIGW_JWKS_TIMEOUT_SECONDS` | `5` | API Gateway v2 JWT authorizer: JWKS document fetch timeout (seconds) |
| `USE_SSL` | `0` | Enable HTTPS on the gateway listener. Accepts `1`, `true`, `yes`. LocalStack-compatible flag name |
| `MINISTACK_SSL_CERT` | _(unset)_ | Optional PEM-encoded server certificate path; required together with `MINISTACK_SSL_KEY`. When unset, MiniStack auto-generates a self-signed cert under `${TMPDIR}/ministack-tls/` (cached across restarts) |
| `MINISTACK_SSL_KEY` | _(unset)_ | Optional PEM-encoded private key path; required together with `MINISTACK_SSL_CERT` |
| `MINISTACK_IMDS_V2_REQUIRED` | `0` | Reject token-less GETs on `/latest/meta-data/...`. When set, callers must first `PUT /latest/api/token` and pass the token as `X-aws-ec2-metadata-token`, matching real-AWS hop-limit-1 IMDSv2-only instances |

### API Gateway HTTP proxy execution model

API Gateway v1/v2 HTTP proxy forwarding uses non-blocking event-loop semantics by offloading the upstream socket call to a worker thread. This preserves AWS-compatible response behavior while preventing long-running proxy calls from stalling unrelated requests (for example, parallel DynamoDB operations). The same non-blocking path is used for JWT **JWKS** fetches. Tune wall-clock limits with `MINISTACK_APIGW_PROXY_TIMEOUT_SECONDS` and `MINISTACK_APIGW_JWKS_TIMEOUT_SECONDS` at the bottom of the [Configuration](#configuration) table above.

### Startup Scripts

MiniStack supports two types of init scripts, with LocalStack-compatible paths:

| Phase | MiniStack path | LocalStack-compatible path |
|-------|----------------|---------------------------|
| Pre-start | `/docker-entrypoint-initaws.d/*.{sh,py}` | `/etc/localstack/init/boot.d/*.{sh,py}` |
| Post-ready | `/docker-entrypoint-initaws.d/ready.d/*.{sh,py}` | `/etc/localstack/init/ready.d/*.{sh,py}` |

Scripts from both paths are merged, deduplicated by filename, and run in alphabetical order.
If the same filename exists in both paths, the MiniStack-native path takes priority.

Init scripts automatically receive `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_DEFAULT_REGION`, and `AWS_ENDPOINT_URL` — no manual configuration needed. The `aws` CLI is bundled in the image.

Each script also gets:

| Variable | Set per | Value |
|----------|---------|-------|
| `MINISTACK_INIT_SCRIPT_DIR` | script | Absolute path of the directory the running script lives in |
| `MINISTACK_INIT_SCRIPT_PATH` | script | Absolute path of the running script |
| `MINISTACK_INIT_BOOT_DIR` | phase | Boot-phase directory (`/docker-entrypoint-initaws.d` or `/etc/localstack/init/boot.d`) when present |
| `MINISTACK_INIT_READY_DIR` | phase | Ready-phase directory (`/docker-entrypoint-initaws.d/ready.d` or `/etc/localstack/init/ready.d`) when present |

This lets a script reference sibling files without hardcoding the mount path:

```bash
# ready.d/01-create-resources.sh
aws s3 mb s3://my-bucket
aws s3 cp "${MINISTACK_INIT_SCRIPT_DIR}/seed-data.json" s3://my-bucket/
aws sqs create-queue --queue-name my-queue
```

```python
# ready.d/02-seed-data.py
import boto3, os
s3 = boto3.client("s3", endpoint_url=os.environ["AWS_ENDPOINT_URL"])
s3.put_object(Bucket="my-bucket", Key="config.json", Body=b'{"env": "local"}')
```

**Docker Compose** — mount scripts at either path:
```yaml
volumes:
  - ./init-scripts:/docker-entrypoint-initaws.d           # ministack-native
  # OR
  - ./init-scripts:/etc/localstack/init                    # localstack-compatible
```

### Athena SQL Engines

Set `ATHENA_ENGINE` to control Athena's SQL execution engine. In `auto` mode, DuckDB is used if installed, otherwise queries return mock results.

| Capability | `duckdb` | `mock` |
|---|---|---|
| Simple SELECT / expressions | Yes | Partial (regex) |
| Arithmetic, aggregations, JOINs, CTEs | Yes | No |
| Window functions, subqueries | Yes | No |
| Parquet / CSV / JSON file queries | Yes | No |
| UNNEST, ARRAY, MAP functions | Yes | No |
| APPROX\_DISTINCT, REGEXP\_EXTRACT | Yes | No |

Install DuckDB for full Athena SQL compatibility: `pip install ministack[full]`.

### State Persistence

When `PERSIST_STATE=1`, MiniStack saves service state to `STATE_DIR` on shutdown and reloads it on startup. Writes are atomic (write-to-tmp then rename) to prevent corruption on crash.

Services currently supporting persistence: **All services** — API Gateway v1/v2, ALB, ACM, AppConfig, AppSync, Athena, Cloud Map, CloudFront, CloudWatch, CloudWatch Logs, CodeBuild, Cognito, DynamoDB, EC2, ECR, ECS, EFS, EKS, ElastiCache, EMR, EventBridge, EventBridge Scheduler, Firehose, Glue, IAM/STS, Kinesis, KMS, Lambda, RDS, Route 53, S3, Secrets Manager, SES, SES v2, SNS, SQS, SSM, Step Functions, Transfer Family, WAF v2

```bash
docker run -p 4566:4566 \
  -e PERSIST_STATE=1 \
  -e STATE_DIR=/data/ministack-state \
  -v /tmp/ministack-data:/data \
  ministackorg/ministack
```

### Lambda in Docker

Set `LAMBDA_EXECUTOR=docker` to run every Lambda invocation inside an AWS-supplied runtime container instead of a local subprocess. `provided.*` runtimes and `PackageType: Image` always use Docker regardless of this setting.

Supported runtimes and the AWS public ECR images MiniStack pulls for them:

| Runtime | Image |
|---------|-------|
| `python3.8` – `python3.14` | `public.ecr.aws/lambda/python:<version>` |
| `nodejs14.x` – `nodejs24.x` | `public.ecr.aws/lambda/nodejs:<version>` |
| `provided.al2023` | `public.ecr.aws/lambda/provided:al2023` |
| `provided.al2` | `public.ecr.aws/lambda/provided:al2` |
| `provided` | `public.ecr.aws/lambda/provided:latest` |

Containers are named `lambda-<random-hex-16>` and pooled across invocations. Idle containers are reaped after `LAMBDA_WARM_TTL_SECONDS` (default 300s) — no manual cleanup needed. The pool is also drained on `/_ministack/reset`.

**Docker-in-Docker:** when MiniStack itself runs inside a container, set `LAMBDA_REMOTE_DOCKER_VOLUME_MOUNT` to a Docker named volume that is also mounted at `/var/task` inside the MiniStack container. MiniStack writes the Lambda code into the volume so the sibling Lambda container (started via the host Docker socket) can read it. Not needed when MiniStack runs directly on the host.

**Networking:** when MiniStack runs in Docker Compose, set `DOCKER_NETWORK` to the Compose network name. All container-backed services (Lambda, RDS, EKS, ElastiCache) then attach to that network so Lambda code can reach MiniStack at `http://<ministack-service-name>:4566`. The legacy `LAMBDA_DOCKER_NETWORK` is still accepted (Lambda only) as a fallback.

Example `docker-compose.yml`:

```yaml
services:
  ministack:
    image: ministackorg/ministack:latest
    container_name: infra_ministack
    ports:
      - "4566:4566"
    environment:
      LAMBDA_EXECUTOR: docker
      DOCKER_NETWORK: ${COMPOSE_PROJECT_NAME}_infra-network
      LAMBDA_REMOTE_DOCKER_VOLUME_MOUNT: ${COMPOSE_PROJECT_NAME}_lambda-docker-volume
      AWS_DEFAULT_REGION: ${AWS_REGION:-eu-central-1}
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - lambda-docker-volume:/var/task
    networks:
      - infra-network

volumes:
  lambda-docker-volume:

networks:
  infra-network:
```

Lambda code in this setup should point at the MiniStack service name, not `localhost`:

```python
boto3.client("s3", endpoint_url="http://infra_ministack:4566", ...)
```

If `/var/run/docker.sock` is root-owned on the host, add `privileged: true` to the `ministack` service so it can talk to the daemon.

### EKS with Real Kubernetes (k3s)

MiniStack's EKS spawns a real [k3s](https://k3s.io) cluster (75 MB image) when you create a cluster. `kubectl`, Helm, and any Kubernetes tooling work out of the box.

```bash
# Create an EKS cluster — k3s starts automatically
aws --endpoint-url=http://localhost:4566 eks create-cluster \
  --name my-cluster --role-arn arn:aws:iam::000000000000:role/eks \
  --resources-vpc-config subnetIds=subnet-1

# Get the k3s kubeconfig (container name follows ministack-eks-{name} pattern)
docker exec ministack-eks-my-cluster cat /etc/rancher/k3s/k3s.yaml \
  | sed "s/127.0.0.1:6443/localhost:$(docker port ministack-eks-my-cluster 6443/tcp | cut -d: -f2)/" \
  > /tmp/ministack-kubeconfig.yaml

# Use kubectl against real Kubernetes
export KUBECONFIG=/tmp/ministack-kubeconfig.yaml
kubectl get nodes          # Real k3s node, Ready status
kubectl create deployment nginx --image=nginx:alpine
kubectl get pods           # Real pod running

# Helm works too
helm repo add bitnami https://charts.bitnami.com/bitnami
helm install my-redis bitnami/redis --set auth.enabled=false

# Clean up — k3s container is removed automatically
aws --endpoint-url=http://localhost:4566 eks delete-cluster --name my-cluster
```

> **Note:** EKS requires Docker socket access (`-v /var/run/docker.sock:/var/run/docker.sock`) to spawn k3s containers. The k3s image is pulled on first `CreateCluster` call.

> **Security trade-off:** the k3s container is launched with `--privileged`. k3s server mode needs to remount `/sys/fs/cgroup`, which no granular Linux capability set permits — running unprivileged fails with `failed to evacuate root cgroup`. This grants the k3s container significant access on the Docker host. The trade-off is acceptable for local development against an emulator but should be considered before running MiniStack EKS on shared infrastructure. Omitting the Docker socket mount cleanly disables k3s and falls back to a static EKS mock.

### Lambda Warm Starts

MiniStack keeps Python and Node.js Lambda functions warm between invocations. After the first call (cold start), the handler module stays loaded in a persistent subprocess. Subsequent calls skip the import/require step, matching real AWS warm-start behaviour and making test suites significantly faster.

### Lambda Node.js Runtimes

MiniStack supports Node.js Lambda runtimes (`nodejs14.x`, `nodejs16.x`, `nodejs18.x`, `nodejs20.x`, `nodejs22.x`). Functions execute via a local `node` subprocess (or Docker when `LAMBDA_EXECUTOR=docker`) — no mocking, real JS execution.

```python
import boto3, json, zipfile, io

lam = boto3.client("lambda", endpoint_url="http://localhost:4566", region_name="us-east-1",
                   aws_access_key_id="test", aws_secret_access_key="test")

code = "exports.handler = async (event) => ({ statusCode: 200, body: JSON.stringify(event) });"
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as zf:
    zf.writestr("index.js", code)

lam.create_function(
    FunctionName="my-node-fn",
    Runtime="nodejs20.x",
    Role="arn:aws:iam::000000000000:role/r",
    Handler="index.handler",
    Code={"ZipFile": buf.getvalue()},
)

resp = lam.invoke(FunctionName="my-node-fn", Payload=json.dumps({"hello": "world"}))
print(json.loads(resp["Payload"].read()))  # {'statusCode': 200, 'body': '{"hello": "world"}'}
```

Layers that ship npm packages work too — MiniStack resolves the `nodejs/node_modules` subdirectory inside each layer zip and prepends it to the module search path.

MiniStack also sets the standard Lambda runtime environment before the handler module is loaded, including `LAMBDA_TASK_ROOT`, `AWS_LAMBDA_FUNCTION_NAME`, `AWS_LAMBDA_FUNCTION_MEMORY_SIZE`, and `_LAMBDA_FUNCTION_ARN`. That keeps import-time Lambda detection and conditional handler setup aligned with AWS warm runtime behaviour.

### Lambda Proxy (BYO container)

For languages AWS doesn't ship a runtime for (PHP, Deno, Bun, custom builds), point a function at your own running container instead of having MiniStack execute a deployment package. MiniStack forwards the Lambda event to that container as an HTTP POST and returns its JSON response as the function result.

The function is a real Lambda in MiniStack's registry — it has an ARN, shows up in `list-functions`, and works as a target for API Gateway `AWS_PROXY` integrations, EventBridge rules, SQS event sources, Step Functions tasks, and any other Lambda trigger. Only the execute hop is redirected.

Configure it per-function via the standard `CreateFunction` API:

```python
import boto3
lam = boto3.client("lambda", endpoint_url="http://localhost:4566", region_name="us-east-1",
                   aws_access_key_id="test", aws_secret_access_key="test")

lam.create_function(
    FunctionName="phpapi",
    Runtime="provided",                                # any value; ignored in proxy mode
    Role="arn:aws:iam::000000000000:role/r",
    Handler="index.handler",                           # any value; ignored in proxy mode
    Code={"ZipFile": b"PK\x05\x06" + b"\x00" * 18},    # empty zip stub
    Environment={"Variables": {
        "MINISTACK_LAMBDA_PROXY_URL": "http://host.docker.internal:9000/invoke",
    }},
)
```

Or globally via env var at startup: `MINISTACK_LAMBDA_PROXY_<func-name>=http://...`.

Your container receives the Lambda event JSON as the request body. Reply with the function's response as JSON (or a Lambda Proxy response shape `{"statusCode", "headers", "body"}` if it sits behind API Gateway). MiniStack passes these context headers on every forward:

| Header | Value |
|---|---|
| `X-Amzn-Lambda-Function-Name` | `phpapi` |
| `X-Amzn-Lambda-Function-Version` | `$LATEST` |
| `X-Amzn-Lambda-Function-Arn` | `arn:aws:lambda:us-east-1:000000000000:function:phpapi` |
| `X-Amzn-Lambda-Request-Id` | per-invocation UUID |
| `X-Amzn-Lambda-Deadline-Ms` | epoch-ms when the function timeout expires |

Errors map to Lambda's standard error shape so async-invoke retry, DLQ, destinations, and CloudWatch error metrics behave the same as for any other executor:

| What happened | `errorType` | `errorMessage` |
|---|---|---|
| Container unreachable | `Runtime.HandlerError` | `Proxy target … unreachable: …` |
| Took longer than the function `Timeout` | `Sandbox.Timedout` | `Task timed out after N.00 seconds` |
| Container returned non-2xx | `Runtime.HandlerError` | `Proxy target returned HTTP <code>: <body…>` |

**What this is not:** AWS Lambda doesn't have a feature called "register an externally-running container as my function." If you want byte-for-byte parity in production, ship the same container as a Lambda container image (`PackageType=Image`) and use the AWS Lambda Runtime Interface inside it. Proxy mode is a local-dev shortcut for the inner-loop case; what your production code sees from MiniStack at the SDK boundary is identical to AWS, but the handler signature inside your container differs from a real Lambda runtime contract.

---

## Architecture

```
                    ┌──────────────────────────────────────────┐
 AWS CLI / boto3    │         ASGI Gateway  :4566              │
 Terraform / CDK ──►│  ┌────────────────────────────────────┐  │
 Any AWS SDK        │  │          Request Router            │  │
                    │  │  1. X-Amz-Target header            │  │
                    │  │  2. Authorization credential scope │  │
                    │  │  3. Action query param             │  │
                    │  │  4. URL path pattern               │  │
                    │  │  5. Host header                    │  │
                    │  │  6. Default → S3                   │  │
                    │  └────────────────┬───────────────────┘  │
                    │                 │                        │
                    │  ┌────────────────────────────────────┐  │
                    │  │   Service Handlers (lazy-loaded)   │  │
                    │  │                                    │  │
                    │  │  S3      SQS     SNS    DynamoDB   │  │
                    │  │  Lambda  IAM     STS    Secrets    │  │
                    │  │  SSM     Events  Kinesis    CW     │  │
                    │  │  CW Logs  SES    SESv2     ACM     │  │
                    │  │  Step Functions   API GW  v1/v2    │  │
                    │  │  ECS    RDS   ElastiCache  Glue    │  │
                    │  │  Athena   Firehose    Route53      │  │
                    │  │  Cognito  EC2    EMR   EBS  EFS    │  │
                    │  │  ALB/ELBv2   WAF v2   KMS  ECR     │  │
                    │  │  CloudFormation    CloudFront      │  │
                    │  │  AppSync  Cloud Map   CodeBuild    │  │
                    │  │  AutoScaling  AppConfig     EKS    │  │
                    │  │  RDS Data  S3 Files  Scheduler     │  │
                    │  │  Transfer Family   IoT Core        │  │
                    │  └────────────────────────────────────┘  │
                    │                                          │
                    │  In-Memory Storage + Optional Docker     │
                    └──────────────────────────────────────────┘
                                        │
                         ┌──────────────┼──────────────┐
                         ▼              ▼              ▼
                    Redis:6379    Postgres:15432+  MySQL:15433+
                    (ElastiCache)    (RDS)           (RDS)
```

---

## Running Tests

```bash
# Install test dependencies
pip install boto3 pytest duckdb docker cbor2

# Start MiniStack
docker compose up -d

# Run the full test suite (2,500+ tests across all services)
pytest tests/ -v
```

Expected output:

```
tests/test_s3.py::test_s3_create_bucket PASSED
...
tests/test_lambda.py::test_lambda_invoke PASSED

2100+ passed in ~120s
```

---

## Terraform / CDK / Pulumi

### Terraform

Works with both Terraform AWS Provider v5 and v6.

```hcl
provider "aws" {
  region                      = "us-east-1"
  access_key                  = "test"
  secret_key                  = "test"
  s3_use_path_style           = true
  skip_credentials_validation = true
  skip_metadata_api_check     = true
  skip_requesting_account_id  = true

  endpoints {
    acm             = "http://localhost:4566"
    apigateway      = "http://localhost:4566"
    appsync         = "http://localhost:4566"
    athena          = "http://localhost:4566"
    cloudformation  = "http://localhost:4566"
    cloudfront      = "http://localhost:4566"
    cloudwatch      = "http://localhost:4566"
    codebuild       = "http://localhost:4566"
    cognitoidentity = "http://localhost:4566"
    cognitoidp      = "http://localhost:4566"
    dynamodb        = "http://localhost:4566"
    ec2             = "http://localhost:4566"
    ecr             = "http://localhost:4566"
    ecs             = "http://localhost:4566"
    efs             = "http://localhost:4566"
    elasticache     = "http://localhost:4566"
    elbv2           = "http://localhost:4566"
    emr             = "http://localhost:4566"
    events          = "http://localhost:4566"
    firehose        = "http://localhost:4566"
    glue            = "http://localhost:4566"
    iam             = "http://localhost:4566"
    kinesis         = "http://localhost:4566"
    kms             = "http://localhost:4566"
    lambda          = "http://localhost:4566"
    logs            = "http://localhost:4566"
    rds             = "http://localhost:4566"
    route53         = "http://localhost:4566"
    s3              = "http://localhost:4566"
    s3control       = "http://localhost:4566"
    secretsmanager  = "http://localhost:4566"
    ses             = "http://localhost:4566"
    sesv2           = "http://localhost:4566"
    sns             = "http://localhost:4566"
    sqs             = "http://localhost:4566"
    ssm             = "http://localhost:4566"
    stepfunctions   = "http://localhost:4566"
    sts             = "http://localhost:4566"
    wafv2           = "http://localhost:4566"
  }
}
```

**Terraform VPC module** — fully supported (v6.6.0):

```hcl
module "vpc" {
  source  = "terraform-aws-modules/vpc/aws"
  version = "6.6.0"

  name = "my-vpc"
  cidr = "10.0.0.0/16"

  azs             = ["us-east-1a", "us-east-1b", "us-east-1c"]
  private_subnets = ["10.0.0.0/20", "10.0.16.0/20", "10.0.32.0/20"]
  public_subnets  = ["10.0.64.0/20", "10.0.80.0/20", "10.0.96.0/20"]

  enable_nat_gateway = true
  single_nat_gateway = true
}
```

Creates VPC with per-VPC default network ACL, security group, and main route table. All 23 resources (subnets, IGW, NAT, route tables, associations, routes, default resources) supported.

#### Predictable API Gateway IDs (`ms-custom-id`)

Pin the generated `apiId` / REST API id to a caller-supplied value so URLs stay stable across `terraform apply` runs. Works on `aws_apigatewayv2_api` (HTTP + WebSocket) and `aws_apigateway_rest_api`.

```hcl
resource "aws_apigatewayv2_api" "example" {
  name          = "example"
  protocol_type = "HTTP"
  tags = {
    ms-custom-id = "example"
  }
}
# → invoke URL stays "example.execute-api.localhost:4566" every apply
```

Duplicates in the same account fail with `ConflictException`. The LocalStack `ls-custom-id` tag is not recognised — use `ms-custom-id` only (callers hitting the old name get a clear `BadRequestException`).

### AWS CDK

Set `AWS_ENDPOINT_URL` to route all CDK requests to MiniStack:

```bash
export AWS_ENDPOINT_URL=http://localhost:4566
export AWS_ACCESS_KEY_ID=test
export AWS_SECRET_ACCESS_KEY=test
export AWS_DEFAULT_REGION=us-east-1

cdk bootstrap aws://000000000000/us-east-1
cdk deploy --require-approval never
```

> **Important:** Running `cdk deploy` without `AWS_ENDPOINT_URL` will send requests to **real AWS**, not MiniStack. If you see "The security token included in the request is invalid", your requests are hitting AWS — set the endpoint.

To reset the bootstrap stack or delete all state:

```bash
# Delete a specific stack
aws --endpoint-url=http://localhost:4566 cloudformation delete-stack --stack-name CDKToolkit

# Or reset all MiniStack state
curl -X POST http://localhost:4566/_ministack/reset
```

### Pulumi

```yaml
# Pulumi.dev.yaml
config:
  aws:endpoints:
    - s3: http://localhost:4566
      dynamodb: http://localhost:4566
      # ... etc
```

### Amplify / CDK

MiniStack supports Amplify Gen 2 and CDK deployments. The underlying services are fully emulated:

- **Auth** — Cognito User Pools with JWKS/OIDC endpoints (`/.well-known/jwks.json`) for real JWT validation
- **Data** — AppSync GraphQL queries/mutations execute against DynamoDB resolvers (create/get/list/update/delete)
- **Storage** — S3
- **Functions** — Lambda (Python + Node.js)

```bash
export AWS_ENDPOINT_URL=http://localhost:4566
npx ampx sandbox
```

> **Note:** AppSync supports Amplify-style CRUD operations. Advanced GraphQL features (fragments, unions, subscriptions) are not supported.

### Testcontainers (Java / Go / Python)

See [`Testcontainers/java-testcontainers`](Testcontainers/java-testcontainers), [`Testcontainers/go-testcontainers`](Testcontainers/go-testcontainers), and [`Testcontainers/python-testcontainers`](Testcontainers/python-testcontainers) for ready-to-run integration tests using Testcontainers with the AWS SDK v2.

---

## Comparison

| Feature | MiniStack | LocalStack Free | LocalStack Pro |
|---------|-----------|-----------------|----------------|
| S3, SQS, SNS, DynamoDB | ✅ | ✅ | ✅ |
| **DynamoDB Streams** | ✅ | ✅ | ✅ |
| Lambda (Python + Node.js execution) | ✅ | ✅ | ✅ |
| IAM, STS, SecretsManager | ✅ | ✅ | ✅ |
| CloudWatch Logs | ✅ | ✅ | ✅ |
| SSM Parameter Store | ✅ | ✅ | ✅ |
| EventBridge | ✅ | ✅ | ✅ |
| Kinesis | ✅ | ✅ | ✅ |
| SES | ✅ | ✅ | ✅ |
| Step Functions | ✅ | ✅ | ✅ |
| **RDS (real DB containers)** | ✅ | ❌ | ✅ |
| **ElastiCache (real Redis)** | ✅ | ❌ | ✅ |
| **ECS (real Docker containers)** | ✅ | ❌ | ✅ |
| **Athena (real SQL via DuckDB)** | ✅ | ❌ | ✅ |
| **Glue Data Catalog + Jobs** | ✅ | ❌ | ✅ |
| **API Gateway v2 (HTTP API)** | ✅ | ✅ | ✅ |
| **API Gateway v2 (WebSocket API)** | ✅ | ❌ | ✅ |
| **API Gateway v1 (REST API)** | ✅ | ✅ | ✅ |
| **Firehose** | ✅ | ✅ | ✅ |
| **Route53** | ✅ | ✅ | ✅ |
| **Cognito** | ✅ | ✅ | ✅ |
| **EC2** | ✅ | ✅ | ✅ |
| **EMR** | ✅ | Paid | ✅ |
| **ELBv2 / ALB** | ✅ | ✅ | ✅ |
| **EBS** | ✅ | Paid | ✅ |
| **EFS** | ✅ | Paid | ✅ |
| **ACM** | ✅ | ✅ | ✅ |
| **SES v2** | ✅ | ✅ | ✅ |
| **WAF v2** | ✅ | Paid | ✅ |
| **CloudFormation** | **partial** | partial | ✅ Free |
| **KMS** | ✅ | Paid | ✅ Free |
| **ECR** | ✅ | ✅ | ✅ |
| **CloudFront** | ✅ | Paid | ✅ |
| **AppSync** | ✅ | NO | ✅ |
| **Cloud Map** | ✅ | ❌ | ✅ |
| **CodeBuild** | ✅ | ✅ | ✅ |
| **Transfer Family** | ✅ | ❌ | ❌ |
| **Inspector2** | ✅ | ❌ | ❌ |
| **IoT Core** | ✅ (control + WS data plane) | ❌ | ✅ (paid tier) |
| **S3 Files** | ✅ | ❌ | ❌ |
| Cost | **Free forever** | Was free, now paid | $35+/mo |
| Docker image size | ~250MB | ~1GB | ~1GB |
| Memory at idle | ~40MB | ~500MB | ~500MB |
| Startup time | <1s | ~15-30s | ~15-30s |
| License | MIT | BSL (restricted) | Proprietary |

---

## Community Integrations

| Project | Description |
|---------|-------------|
| [**StackPort**](https://github.com/DaviReisVieira/stackport) | **Web UI** — visual dashboard to browse and inspect AWS resources in MiniStack. Available on [PyPI](https://pypi.org/project/stackport/) and [Docker Hub](https://hub.docker.com/r/davireis/stackport). |
| [**McDoit.Aspire.Hosting.Ministack**](https://github.com/McDoit/aspire-hosting-ministack) | .NET Aspire hosting integration for MiniStack. |

---

## Contributing

PRs welcome. The codebase is intentionally simple — each service is a single self-contained Python file in `ministack/services/`. Adding a new service means:

1. Create `ministack/services/myservice.py` with an `async def handle_request(...)` function and a `reset()` function
2. Add it to `SERVICE_REGISTRY` in `ministack/app.py` so the handler, aliases, and service filter are generated automatically
3. Add detection patterns to `ministack/core/router.py`
4. Add a fixture to `tests/conftest.py` and tests to `tests/test_services.py`

See [CONTRIBUTING.md](CONTRIBUTING.md) for a full walkthrough.

---

## License

MIT — free to use, modify, and distribute. No restrictions.

```
Copyright (c) 2026 MiniStack Contributors

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
```
