kumo puts 82 AWS service emulators behind one unauthenticated port
A lightweight AWS service emulator written in Go. Works as both a CI/CD testing tool and a local development server with optional data persistence.
At a glance
- What is it?
- sivchari/kumo is a single Go binary that answers AWS SDK v2 calls on localhost:4566 for CI runs and local development. The service catalog, the pinned dependency list and the two different compose files in the repository do not agree with each other, and that is where the interesting parts are.
- Who is it for?
- Reach for kumo when a pipeline needs an AWS shaped endpoint on localhost and nothing beyond that. Skip it if you need credential checks, tenant isolation, or coverage outside the service modules its go.mod pins.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on November 13, 2026, and from our analysis. They are not legal advice.
Star history
- sivchari/kumo1,508 ★ · +73
| Repository | 2026-07-28 | 2026-11-13 |
|---|---|---|
| sivchari/kumo | 1.4k | 1.5k |
73 Stars in the last 108 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
The 82 service rows are generated from each service's own Meta()
The service catalog on the overview page is not maintained by hand. An HTML comment opens the table and names the mechanism:
<!-- BEGIN SERVICES: auto-generated from each service's Meta(); run `make readme` to update. Do not edit by hand. -->The `readme` target in the Makefile is one line, `go run ./cmd/readme-gen`, and the comment above it says it regenerates the catalog from each service's Meta(). A service therefore contributes its own display name and its own one line description, and the thirteen category headings (Storage, Compute, Container, Database, Messaging & Integration, Security & Identity, Monitoring & Logging, Networking & Content Delivery, Application Integration, Management & Configuration, Analytics & ML, Developer Tools, Other Services) come out of that same generator.
Counting the rows gives 82, which matches the number in the heading. What the generator cannot tell you is how deep any single implementation goes. Meta() is metadata, so a service that returns a name and a sentence earns a table row identical in weight to one that covers a large part of its API. Anyone picking an emulator by scanning this list is reading declared intent, not a coverage report.
The dependency list pins about thirty services, not eighty two
go.mod is module github.com/sivchari/kumo, declaring go 1.25.0 with toolchain go1.26.8. The require block pins aws-sdk-go-v2 v1.43.4, config v1.32.17 and credentials v1.19.16, then one module per service that has a typed client: acm v1.38.3, amplify v1.38.16, apigateway v1.39.3, apigatewayv2 v1.35.2, appmesh v1.35.14, appsync v1.53.7, athena v1.57.6, backup v1.55.2, cloudformation v1.76.1, cloudfront v1.67.4, cloudwatch v1.66.3, cloudwatchlogs v1.81.1, dynamodb v1.57.3, dynamodbstreams v1.36.4, ec2 v1.319.1, ecr v1.60.4, ecs v1.90.0, eventbridge v1.45.25, firehose v1.46.4, iam v1.58.1, kinesis v1.43.7, kms v1.51.1, lambda v1.101.2, route53 v1.65.6, s3 v1.100.1, secretsmanager v1.44.4, sesv2 v1.66.4, sfn v1.45.4 and sns v1.42.4.
The require block visible in that file stops partway, after the sns entry and partway into the next module path, so this list is a floor rather than a complete count. Even as a floor the distance to 82 is worth weighing. Services outside the pinned set are reached either through one of these modules or through shared dependencies that sit below the cut.
The pins do show the upgrade path clearly. Because the emulator works from the real SDK's request and response types, following an AWS API change is largely a matter of bumping the matching service module.
Port 4566 is fixed in every example, and only one compose file sets the bind address
Every worked example in the quick start uses the same port:
docker run -p 4566:4566 ghcr.io/sivchari/kumo:latestThe compose snippet on the page maps 4566 to 4566 as well, and both Go examples point `BaseEndpoint` at http://localhost:4566. The knobs themselves appear in the repository's own docker-compose.yml:
environment:
- KUMO_HOST=0.0.0.0
- KUMO_PORT=4566
- KUMO_LOG_LEVEL=debugTwo details there matter. KUMO_HOST is 0.0.0.0, so the shipped compose file listens on every interface, while the compose snippet in the docs sets no host variable at all and leaves the default unstated anywhere on the page. KUMO_LOG_LEVEL=debug appears only in the repository file; nothing in the quick start mentions logging configuration at all.
The two compose files also disagree about where the binary comes from. The one at the repository root builds from source with `context: .` and `dockerfile: docker/Dockerfile`. The one in the docs pulls `ghcr.io/sivchari/kumo:latest` and never mentions the Dockerfile. A healthcheck exists only in the root file, probing the port every five seconds:
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost:4566/health"]
interval: 5s
timeout: 3s
retries: 5No authentication required, and the emulated IAM does not change that
The features list opens with No authentication required. Both usage examples comply with that, since an emulator that rejected calls would defeat the point. Each passes `credentials.NewStaticCredentialsProvider("test", "test", "")` and a fixed region of us-east-1.
The same catalog lists IAM, KMS, Secrets Manager, STS, Cognito, ACM, Macie and Security Lake under Security & Identity. Those are emulated APIs that your code calls. They are not a protection standing in front of the emulator: nothing on the overview page asks for a token before answering, and no example switches on a credential check.
For a CI runner or a laptop this is the right default. No secret to provision, nothing to expire, no AWS account to clean up after a test run. On a shared network it inverts, because the absence of an inbound check is the feature. Bind to loopback or keep the port inside an isolated container network, and do not treat the presence of an IAM implementation as a reason to leave the port reachable.
The two usage examples stop mid-statement
The S3 block on the overview page sets the SDK v2 client up the way the emulator expects and then ends:
client := s3.NewFromConfig(cfg, func(o *s3.Options) {
o.BaseEndpoint = aws.String("http://localhost:4566")
o.UsePathStyle = truThree things are visible there. The last assignment reads `tru` rather than `true`. `main()` is never closed, and the import list carries "strings" without using it. As printed, the snippet is a fragment rather than a program that builds.
The SQS block underneath is shorter. It creates the client with the same two settings, a BaseEndpoint and static credentials, then stops after a `// Create queue` comment with a blank line and nothing after it.
The transferable part of both is a three line contract: a region, a static credential pair, and a BaseEndpoint pointing at localhost:4566. UsePathStyle shows up only in the S3 example, which is the line that governs bucket addressing. Neither example shows an emulator specific option beyond that, which tells you kumo is reached by moving the endpoint rather than by configuring the client.
Persistence is one environment variable, and the root compose file omits it
Surviving a restart depends on a single variable, KUMO_DATA_DIR. Three forms appear across the quick start:
# Build
make build
# Run
./bin/kumo
# Run with data persistence
KUMO_DATA_DIR=./data ./bin/kumodocker run -p 4566:4566 \
-e KUMO_DATA_DIR=/data \
-v kumo-data:/data \
ghcr.io/sivchari/kumo:latestIn the container form the variable points at /data inside the container and a named volume kumo-data is mounted there, so whatever a run wrote is still there for the next one. The compose variant of the same idea declares that volume twice, once under the service and once at the top level under `volumes:`.
The repository's own docker-compose.yml declares neither the variable nor a volume. Start it from a clone and the emulator comes up with no persistence wired in, while the compose snippet in the docs, which pulls the published image instead of building, does have it. If you want durable state from the local file, set the variable yourself, because nothing in the shipped configuration sets it for you.
The version string reaches the binary through grep and cut
The Makefile does not import a version package. It shells out and slices a constant out of version.go:
VERSION?=$(shell grep 'const Version' version.go | cut -d'"' -f2)That value is compiled into the binary with ldflags aimed at the main package:
build:
go build -ldflags "-X main.version=$(VERSION)" -o $(BUILD_DIR)/$(BINARY_NAME) ./cmd/kumoThe `?=` lets an environment variable of the same name win over the grep, which is a useful override. If `const Version` is renamed, reformatted or moved out of version.go, the grep returns nothing, ldflags receives an empty string, and the build still succeeds with an empty version. No step in the Makefile checks that the grep found anything.
Toolchain pinning is more careful. go.mod declares go 1.25.0 and toolchain go1.26.8, the Makefile exports GOTOOLCHAIN=go1.26.8 for every recipe, and the fuzz target overrides it with FUZZ_GOTOOLCHAIN=go1.27.1. The comment attached to that override gives the reason: before go1.27, fuzzing intermittently reports "context deadline exceeded" as a failure when the fuzztime budget expires, tracked as golang/go#75804. Fuzzing therefore runs on a newer toolchain than everything else, with a note to drop the override once GOTOOLCHAIN moves up.
A Helm chart, and a coverage target the phony list forgot
The root entry list includes a charts/ directory, and the Makefile has a target that drives it end to end:
test-helm-e2e:
bash test/e2e/helm-e2e.shSo there is a Kubernetes packaging path with its own end to end script, alongside a `test-terraform` target that runs the integration suite with `-parallel 4`. Neither appears in the quick start, which covers only docker run, make build and compose.
The .PHONY line has a gap in it. It declares build, run, test, test-fuzz, test-integration, test-terraform, test-helm-e2e, clean, docker, lint, lint-fix, fmt, fmt-diff, readme, cli-gen and cli-coverage. `test-cover` has a recipe sitting right below the `test` target:
test-cover:
go test -v -race -coverprofile=coverage.out -coverpkg=./... ./...
go tool cover -html=coverage.out -o coverage.htmlIt is the one target in the file with a recipe and no .PHONY entry. In practice the target still runs, and only misbehaves if something named coverage.out already exists in the tree.
Lint stays out of the main dependency graph by living in its own module, `go tool -modfile tools/go.mod golangci-lint`. lefthook.yml sits next to the Makefile, and .golangci.yaml, .goreleaser.yml, .pinact.yaml and .tagpr account for the rest of the tool configuration at the root.
Editorial conclusion
Reach for kumo when a pipeline needs an AWS shaped endpoint on localhost and nothing beyond that. Skip it if you need credential checks, tenant isolation, or coverage outside the service modules its go.mod pins. Before putting the port on any shared network, set the bind address yourself, because there is no authentication standing in front of it and the shipped compose file already listens on every interface.
Frequently asked questions
Does kumo require AWS credentials to run?
No inbound credentials. The features list states No authentication required, and both usage examples pass the static pair "test" and "test" with region us-east-1, because your code still needs something in those SDK fields while the emulator answers without checking them.
How many AWS services does kumo emulate?
The catalog heading says 82 and the tables hold 82 rows across thirteen categories. The list is auto-generated from each service's Meta() function by `go run ./cmd/readme-gen`, which is the readme target in the Makefile, so the row count follows the code rather than a hand written list.
How do I keep kumo data between restarts?
Set KUMO_DATA_DIR. The binary example uses `KUMO_DATA_DIR=./data ./bin/kumo`, and the container example sets `KUMO_DATA_DIR=/data` together with a named volume kumo-data mounted at /data. The docker-compose.yml at the repository root sets neither.
Which port does kumo listen on, and can it be changed?
All examples use 4566: the docker run command maps 4566:4566, both compose snippets publish the same port, and the Go examples set BaseEndpoint to http://localhost:4566. The port itself is configurable, since the repository compose file sets KUMO_PORT=4566 explicitly along with KUMO_HOST.
Which Go version does kumo build with?
go.mod declares go 1.25.0 with toolchain go1.26.8, and the Makefile exports GOTOOLCHAIN=go1.26.8. The fuzz target is the exception: it uses FUZZ_GOTOOLCHAIN=go1.27.1 to avoid a pre go1.27 issue where an expired fuzztime budget is reported as a failure.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/sivchari-kumo)Star history chart for your README
[](https://hysenlabs.com/projects/sivchari-kumo)Was this useful?