Ephemeral AWS Lambda MicroVM shells — a Go library (microvm) and CLI (whim).
Spin up a throwaway root shell in a Firecracker MicroVM in ~2 seconds, run something, and let it disappear.
Recorded against a real AWS account — the account ID is masked in-frame via
WHIM_REDACT_ACCOUNT.whim init(cached), then the hero beat: an interactive root shell runninguname/os-release/whoamiat theλ $prompt,Ctrl-]to disconnect, andimage ls/ps/exec/gcto list, reach into, and reap a live VM.
whim init # one-time: build the default sandbox image (~3 min)
whim # drop into a throwaway root shell
whim run -- pytest # spawn → run → stream output → propagate exit code → terminate
Lambda MicroVMs run from a prebuilt image, so there's a one-time bootstrap:
whim initprovisions an S3 build bucket + IAM build role, then builds the default image (AL2023 +tar/gzip) and caches its ARN in~/.config/whim/config.json. This takes a couple of minutes.- After that, every
whim/whim runlaunches a fresh VM from that cached image in ~2s, attaches over a WebSocket, and terminates on exit.
Every VM is launched with a server-side TTL (default 25m, max 8h) as a cleanup backstop, so nothing is left running if a client dies.
- An AWS account enabled for Lambda MicroVMs, in a supported region (e.g.
us-east-1) - AWS credentials —
aws login,aws-vault, an assumed role, or the environment - Go 1.24+
go install github.com/udgover/whim/cmd/whim@latest # → $(go env GOPATH)/bin
aws login
whim preflight-check # verify creds + permissions
whim init # build the default imageFor development, build from a clone instead:
git clone https://github.com/udgover/whim && cd whim
go install ./cmd/whim| Command | What it does |
|---|---|
whim / whim shell |
Launch a VM and attach an interactive root shell. Ctrl-] disconnects (every other key, incl. Ctrl-C, goes to the remote pty). |
whim run -- <cmd…> |
Launch → run → stream combined output → propagate the exit code → terminate. |
whim exec <id> -- <cmd…> |
Run a command in an existing VM. |
whim put <id> <local> <remote-dir> / whim get <id> <remote> <local-dir> |
Copy files/dirs in or out (tar.gz, binary-safe, scp -r/docker cp semantics). |
whim ps [-q] [--json] |
List your whim VMs — running/pending/suspended, state-labeled (like docker ps). |
whim gc [--older-than <dur>] [--yes] |
Terminate your whim VMs (confirms unless --yes). |
whim suspend <id> / whim resume <id> |
Pause/restart a VM (disk + memory preserved). |
whim image ls [-q] [--json] / whim image rm <name…> |
Manage built images. |
whim build <source> --name <n> [--egress public|none] [--force] [--context-subdir <p>] [--json] |
Build a custom image from a local dir/Dockerfile, s3:///https:// archive, or github.com/org/repo@ref; caches the ARN under --name. |
whim init [--image-name <n>] [--force] / whim preflight-check / whim version |
Bootstrap, checks, version. |
Global flags: --region, --profile. shell/run also take --image <name|arn>
and --ttl <dur>. run/exec/put/get exit 125 for whim-level failures
(distinct from a remote command's own code).
whim init builds the default image; whim build builds custom images from
your own source and caches each one by --name (the name is the build/reuse
key) alongside the default in ~/.config/whim/config.json. It reuses the same
bootstrap (artifact bucket, build role, managed base image) as init.
whim build ./app --name whim-app # local build context (Dockerfile at its root)
whim build ./Dockerfile --name whim-min --egress none # a single local Dockerfile, airgapped image
whim build s3://my-bucket/app.zip --name whim-s3 --json
whim build https://example.com/app.zip --name whim-https
GITHUB_TOKEN=… whim build github.com/org/repo@<full-sha> --name whim-repo --forceSources: a local directory or Dockerfile, an s3://bucket/key, an
https://host/path (zip archive or raw Dockerfile), or GitHub shorthand
github.com/org/repo@ref / git+https://github.com/org/repo@ref. GitHub
shorthand is CLI-only — it lowers to an HTTPS archive download (no git needed);
the microvm library transports are local, s3://, and https:// only.
--nameis required and is the cache key: a secondwhim build --name Xreuses the existing imageXunless you pass--force(which deletes and rebuilds). Name-as-key means a changed source does not rebuild on its own.--egress public|nonefixes the image's outbound policy at build time (inherited by every VM launched from it).noneis airgapped.--context-subdir <p>descends into a subdirectory before locating theDockerfile(also strips a single wrapping top-level dir from forge archives).--jsonprints one redacted object{name, arn, source, cached, egress}and suppresses progress chatter.- GitHub auth: set
GITHUB_TOKENfor private repos — it is sent only as a request header, never placed in a URL, printed, logged, or written to config. A full commit SHA ref is an immutable source identity; branches/tags are moving.
Source safety & limits. Every source — local, S3, and HTTPS alike — is
validated and normalized before any image build: archives are re-rooted, and
absolute/../duplicate paths and root-escaping symlinks are rejected. Caps are
256 MiB compressed, 1 GiB uncompressed, and 10,000 files. A .dockerignore
at the context root is honored for a documented subset: blank lines, #
comments, ! negation, leading-/ anchoring, trailing-/ directory matches,
and */? single-segment globs. Unsupported constructs (** cross-segment
globs and […] character classes) are rejected rather than mis-applied, so a
wrong ignore rule can't silently ship excluded files.
Not in v0.1: private-ECR base images (the build role's IAM is intentionally
not widened until that path is validated) — whim build builds on the managed
base image, same as init.
A whim shell is root, but under a restricted Linux capability set — so mount,
network namespaces, unshare, and eBPF are all denied (EPERM) even as uid 0.
--privileged grants the image AWS's additionalOsCapabilities: ["ALL"], which
lifts the effective set to all capabilities. It is opt-in: default images
stay minimal-cap and privilege is never granted implicitly.
whim --privileged # throwaway shell that can mount, netns, run eBPF
whim run --privileged -- mount -t tmpfs none /mnt # one-shot privileged commandwhim --privileged launches the well-known whim-privileged image, building
it on first use (~2-3 min, like whim init) and caching its ARN alongside your
other images. It ships the tooling to use the caps — util-linux (mount,
unshare), iproute (ip netns), e2fsprogs — since capabilities unlock
syscalls, not binaries. --privileged and --image are mutually exclusive.
To bake privilege into a custom image instead, use whim build --privileged:
whim build ./app --name whim-app --privileged # your source + ALL capabilitiesCapabilities are fixed at build time and inherited by every VM launched from
the image. ALL is the only value AWS supports today. Elevated capabilities are
applied within the VM's isolation boundary — per AWS, they do not affect the
host or other MicroVMs.
The microvm package is the product; the CLI is a thin client. It is
credential-injection-only — it never resolves ambient credentials; you pass
an aws.Config you built (so it composes with IRSA, ECS task roles, aws-vault,
assumed roles, etc.).
import "github.com/udgover/whim/microvm"
cfg, _ := config.LoadDefaultConfig(ctx) // caller owns credential resolution
mgr := microvm.NewFromConfig(cfg)
sb, err := mgr.Launch(ctx, imageARN, microvm.WithTTL(25*time.Minute))
if err != nil { /* … */ }
defer sb.Terminate(ctx)
res, _ := sb.Exec(ctx, []string{"sh", "-c", "echo hi"}) // combined output + exit code
sb.Shell(ctx, microvm.ShellIO{In: os.Stdin, Out: os.Stdout})
sb.Put(ctx, "./src", "/opt"); sb.Get(ctx, "/var/log", "./logs")
sb.Suspend(ctx); sb.Resume(ctx)
mgr.List(ctx); mgr.GC(ctx, microvm.GCFilter{OlderThan: time.Hour})To build a custom image from source, the library takes explicit build inputs — there is no ambient discovery (the CLI owns that). The caller supplies the artifact bucket, base image, and build role; GitHub shorthand and default-bootstrap convenience live in the CLI, not here:
arn, err := mgr.BuildFromSource(ctx, "./app", microvm.BuildFromSourceOptions{
Name: "whim-app", // image name = reuse/cache key (required)
ArtifactBucket: "my-artifact-bucket", // caller-owned; staged context uploads here (required)
BaseImageARN: baseImageARN, // managed/base image to build on (required)
BuildRoleARN: buildRoleARN, // role the build assumes to read the staged artifact (required)
Egress: microvm.EgressNone, // outbound policy, fixed at build time
// Force, ContextSubdir, HTTPSHeaders, MaxCompressedBytes, MaxUncompressedBytes …
})source resolves to exactly one transport — a local path, s3://bucket/key, or
https://host/path; http://, credential-bearing URL userinfo, and unknown
schemes are rejected.
Errors are typed sentinels (match with errors.Is): ErrInvalidOption,
ErrInvalidSource, ErrSourceTooLarge, ErrImageNotFound,
ErrVMProvisionFailed, ErrImageBuildFailed, ErrConnClosed, ErrTimeout,
ErrTerminated. For tests, inject a mock via NewWithAPI.
The VM runs your code as root, reachable only through an authenticated WebSocket. whim's guarantees:
- Shell tokens are never logged (the
X-aws-proxy-authvalue lives only in the request header). - Injection-only credentials —
microvmnever sources ambient credentials. - Always a TTL — no VM is launched without one (≤ 8h).
- Privilege is opt-in — default images run with a restricted capability set;
--privileged(all OS capabilities) must be asked for explicitly, and its reach is confined to the VM's isolation boundary (host and other MicroVMs are unaffected). - Ownership-scoped cleanup —
ps/gconly ever touch VMs launched from your own account's microvm-images; AWS-managed base images and other accounts' VMs are never listed or reaped. (Microvms can't be tagged, so ownership is derived from the image ARN — see the caveat below.)gcconfirms unless--yes. - Path-traversal & symlink guards on
get— a compromised VM cannot write outside the destination via a crafted tar (absolute/../escaping-symlink entries are rejected; setuid/setgid bits are stripped). - Remote paths are shell-quoted (no command injection via filenames).
Ownership model: only the heuristic bulk commands ps/gc are scoped to
whim-owned VMs. The id-targeted commands (exec, put, get, suspend,
resume) act on any VM in your account that you explicitly name — like
ssh <host>, naming the target is the authorization. In a shared account, an
explicit whim suspend <foreign-id> could affect someone else's workload.
- Ownership is image-derived, not tagged. Lambda MicroVMs can't be tagged, so
ps/gctreat any VM launched from one of your account's microvm-images as whim-owned. In the rare case you run another tool that also launches from microvm-images, scopegccarefully. - Exec output is combined stdout+stderr (a pty merges them); separate streams would need a guest agent.
- Interactive shells last ~30 min (the shell-token lifetime);
--ttlabove that doesn't extend the session (no reconnect in v0.1). - Transfers are in-memory, capped at 256 MiB per
put/get. - Reconnect yields a new shell (disk persists, in-memory shell state does not).
- Terminal resize isn't forwarded yet (full-screen apps use the default size).
Unit tests are hermetic (no AWS, no network):
go test ./...Live AWS integration tests are build-tagged and opt-in — they provision and
build real images using your whim init artifact bucket and build role:
WHIM_INTEGRATION=1 go test -tags=integration ./...The local-directory and --egress none builds run as-is; set
WHIM_TEST_HTTPS_ZIP (an https:// zip whose Dockerfile is at the root) and
WHIM_TEST_GITHUB=org/repo@<full-sha> (plus GITHUB_TOKEN for private repos) to
exercise the remote-source paths. Override the derived inputs with
WHIM_TEST_ARTIFACT_BUCKET / WHIM_TEST_BUILD_ROLE / WHIM_TEST_BASE_IMAGE.
Apache 2.0
