cdk-local
v0.147.0
Published
Run your CDK-built app locally — standalone, or against the real AWS resources and data it depends on. A CDK-native alternative to sam local.
Downloads
32,122
Maintainers
Readme
cdk-local (cdkl)
Run your CDK-built app locally, no deploy needed — standalone, or kept local while it reaches the real AWS resources and data it depends on, with no .env or local copies to maintain.
A CDK-native alternative to sam local, covering Lambda, API Gateway, ECS, ALB, CloudFront, and more of your CDK app's compute.

Or drive it all from a browser with cdkl studio — pick a target, invoke or serve it, and watch every request, response, and log line land on one live timeline:

Quick start
Requires Docker (running) and Node.js 20+.
npm install -g cdk-local # installs the `cdkl` command
cd your-cdk-app # the directory holding cdk.json
cdkl invoke # pick a Lambda from the list, then run it locallyAdd --from-cfn-stack to bind to a deployed stack — your handler still runs locally in Docker, but reads and writes against the real AWS the deployed app uses: DynamoDB, S3, Secrets, Cognito, and more (see Why cdk-local below).
cdkl start-api --from-cfn-stack # local API on real AWS data; JWT verified against the real Cognito User PoolPrefer a browser? cdkl studio opens a local web console over the same targets — pick one, invoke or serve it, and watch every request, response, and log line on a live timeline.
cdkl studio # open the web console (no target needed)Why cdk-local
- Zero-friction local execution — run standalone with just Docker and your CDK app, no AWS account or deploy needed. Verify the parts of your app that don't touch AWS in seconds — handy as a zero-setup first run, or in CI where no credentials are available:
- pure handler logic — validation, transforms, branching
- Lambda authorizers, running in real local containers
- API Gateway routing and request shaping
- Iterate against your real deployed stack — including its data.
--from-cfn-stackreads the deployed CloudFormation stack and injects its real ARNs and Secret values into the container — no.envfile to maintain, no manual ARN copy-paste — so you stay on the real DynamoDB rows, S3 objects, Cognito users, and Secret values your IAM credentials reach. An offline emulator can fake the API surface, but you'd still own the cost of seeding it:- dumping real data into a local DB
- mirroring Secret values into a local secret store
- re-anonymizing that test data every time the schema changes
- scripting realistic Cognito test users
What runs locally
cdk-local runs your application compute in Docker, using your CDK app as the source of truth.
It deliberately does NOT emulate AWS managed services: your code reaches DynamoDB / S3 / Secrets Manager / Cognito / SNS / SQS / etc. as real AWS through your IAM credentials (or pass --assume-role <arn> to assume a different role). Add --from-cfn-stack to also bind env vars to a deployed stack's real ARNs and Secret values.
The locally executable resources are listed under Supported resources.
Commands
Run every cdkl command from your CDK project root (the directory containing cdk.json).
Every command takes its target two ways:
- Leave it off and cdk-local lists every matching target for you to pick with the arrow keys — no need to know or type the CDK path (
invoke/invoke-agentcore/start-agentcore/run-task/start-cloudfrontpick one;start-service/start-alb/start-apimulti-select). - Or name it — pass the CDK display path (e.g.
cdkl invoke MyStack/Fn) or a stack-qualified logical ID (MyStack:Fn1234ABCD, the SAM-compatible form); single-stack apps may drop the stack prefix.

invoke runs one Lambda in a real RIE container; the options you reach for most:
cdkl invoke --event ./event.json # run with a JSON event payload
cdkl invoke --env-vars ./env.json # overlay env vars (SAM-shape file)
cdkl invoke --from-cfn-stack # bind env to the deployed stack's real values
cdkl invoke --from-cfn-stack --assume-role # ...and run as its deployed execution rolePer-command notes — full capabilities are in Supported resources:
start-apiserves one HTTP server per API; a barestart-apiin a multi-stack app needs--all-stacksor--stack <name>.run-taskruns one ECS task to completion; declared container ports publish on the host (a privileged port like 80 auto-remaps to a free high port with a WARN, or--host-port <container>=<host>pins one).start-serviceruns the service's replicas with no load balancer; a single-replica run publishes host ports the same way. Bothstart-serviceandstart-albprint each host URL in aService endpoints:banner after boot.start-albstands up the ECS service(s) behind an ALB with full listener-rule routing, auth, and WebSocket proxying (details).start-cloudfrontserves theviewer-request-> origin ->viewer-responsepipeline over S3 / Lambda Function URL origins, running CloudFront Functions and Lambda@Edge locally (details).invoke-agentcoreruns a Bedrock AgentCore Runtime agent once (details).start-agentcoreserves an AgentCore Runtime warm — one container you hit repeatedly until^C(details).studioopens a local web console over the same targets, no target needed: Web console —cdkl studio.- Non-TTY (CI / pipes): every command except a bare
start-apineeds an explicit target.
The full command list:
cdkl invoke # Lambda (ZIP or container image)
cdkl run-task # ECS task, run once
cdkl start-service # ECS service replicas (no load balancer)
cdkl start-alb # ECS behind an ALB (front-door per listener)
cdkl start-api # API Gateway REST v1 / HTTP v2 / WebSocket + Function URLs
cdkl start-cloudfront # CloudFront: S3 / Lambda Function URL origins + Functions (static site / SPA / SSR)
cdkl invoke-agentcore # Bedrock AgentCore Runtime (HTTP / MCP / A2A / AGUI)
cdkl start-agentcore # serve an AgentCore Runtime warm: HTTP (POST /invocations + /ws) / MCP (POST /mcp) / A2A (POST /), repeatable
cdkl list # every runnable target, grouped by command (alias: ls)
cdkl studio # interactive web console over every targetFull flags, precedence, and --from-cfn-stack resolution: docs/cli-reference.md and docs/local-emulation.md.
Web console — cdkl studio
cdkl studio is a point-and-click front over the same runners — it takes no target and lists them all. Beyond running a target, it gives you what the CLI can't:
- a live timeline where every invocation and captured serve request lands, each with its container logs bound;
- replay — re-open any past row with an edited payload and re-invoke, or re-send a captured serve request.
A served WebSocket endpoint — an API Gateway WebSocket API, or an HTTP / AGUI AgentCore runtime's /ws from start-agentcore — also gets an interactive WebSocket console (connect / send / receive frames).
cdkl studio # open the console (launches your browser)
cdkl studio --no-open # don't launch a browser; just print the URL
cdkl studio --studio-port 8200 # pin the port (default: auto-assigned)
cdkl studio --from-cfn-stack # bind the whole session to the deployed stack
cdkl studio --from-cfn-stack --assume-role # ...and run every target as its deployed role
cdkl studio --watch # serves started from the UI hot-reload on source changes
cdkl studio --stack 'dev/*' # scope the displayed target list (multi-stack apps)--from-cfn-stack / --assume-role / --watch are session-global and also editable live from the Session bar — they apply to every invoke / serve you start from the UI. The standard synth flags (--app / --profile / --region / -c) work here too.
Each target's composer surfaces its per-run options as controls:
- curated controls per kind — a Lambda's
--env-varsas KEY/VALUE or JSON, ALB--tls/--lb-port, ECS--max-tasks/--host-port, an AgentCore runtime's--ws/--sigv4/--bearer-token; - an All options panel that renders an editable control (checkbox / input / dropdown) for every other flag the underlying command accepts, with a raw extra-args input as the final escape hatch;
- a Dockerfile picker for an ECS service pinned to a deployed registry (where local edits otherwise don't take effect), rebuilding it from local source.
Deployed stack binding — --from-cfn-stack
--from-cfn-stack binds to the deployed CloudFormation stack whose name matches your CDK stack. The bare form resolves the stack name from the target; pass an explicit name only when the deployed CFn stack name differs (e.g. CDK's stackName prop was overridden):
cdkl invoke --from-cfn-stack # bare: uses resolved stack name
cdkl invoke --from-cfn-stack MyExplicitCfnName # explicit when names differ
cdkl invoke --from-cfn-stack --stack-region eu-west-1 # cross-region CFn client
cdkl invoke --from-cfn-stack --assume-role # auto-assume deployed execution roleSubstitutes Ref / Fn::ImportValue / Fn::GetStackOutput in env vars with the deployed physical IDs / exports, decrypts AWS::SSM::Parameter::Value entries (kept off the docker run argv), and resolves same-stack ECR ContainerUri to the deployed image. Fn::GetAtt in the Lambda's own env is recovered from the deployed function's resolved Environment.Variables via lambda:GetFunctionConfiguration. Full resolution rules: docs/cli-reference.md#cloudformation-driven-env-recovery---from-cfn-stack.
Environment variables — --env-vars
Every command except start-cloudfront (whose CloudFront Functions and Lambda@Edge have no env vars) accepts --env-vars <file>, a SAM-shape JSON file that overlays the container's environment — point a Lambda function or ECS container at a different backend for a local run, or supply a value the synthesized template only knows as an intrinsic:
cdkl invoke --env-vars ./env.json
cdkl start-service --env-vars ./env.json
cdkl start-alb --env-vars ./env.json{
"Parameters": { "LOG_LEVEL": "debug" },
"MyStack/Fn": { "TABLE_ENDPOINT": "http://localhost:8000" },
"MyStack/Worker": { "WEBHOOK_URL": null },
"AppContainer": { "DB_HOST": "host.docker.internal", "DB_PORT": "13306" }
}Each top-level JSON key picks which target to overlay:
| Target | Key shape | Notes |
| --- | --- | --- |
| Every target | Parameters | Reserved literal; applied first to every container |
| Lambda / AgentCore Runtime | CDK construct path (e.g. MyStack/Fn) | From Metadata['aws:cdk:path'] of the resource; prefix-matched (MyStack/Fn also catches MyStack/Fn/Resource) |
| Lambda / AgentCore Runtime | CloudFormation logical ID (e.g. MyStackFn1A2B3C) | Top-level resource key in the synthesized template; exact match |
| ECS container | Container Name (e.g. AppContainer) | ContainerDefinitions[].Name in the synthesized TaskDefinition — explicitly set via the containerName option of taskDef.addContainer(id, { containerName, ... }), or defaults to the construct id (first arg of addContainer) when omitted. The TaskDefinition's CDK path / logical ID is NOT accepted as a key — it would identify the TaskDef but not which container's env block to overlay |
--env-vars overlays the env block after the template's literals and any resolved ECS Secrets[] have been applied. A per-target key (from the table above) wins over Parameters. A null value clears the key — use the JSON literal null, not the string "null".
--env-vars can be combined with --from-cfn-stack: the latter resolves intrinsics (Ref / Fn::ImportValue / Fn::GetStackOutput / Fn::GetAtt) against the deployed stack first, then --env-vars overlays your overrides on top. Running standalone (no --from-cfn-stack), env vars whose template value is an intrinsic can't be resolved and are dropped with a warning — --env-vars is how you supply a concrete value for them.
When pointing a container at a tunneled VPC resource (e.g. an Aurora cluster reached via a local port forward), use host.docker.internal instead of 127.0.0.1 — 127.0.0.1 inside the container is the container itself, not the host where the tunnel listens.
Selective local backends
The same overlay can redirect the AWS SDK itself. Set the SDK's standard per-service endpoint variables to point individual services at a local AWS-compatible endpoint you run yourself, while every other service stays on real AWS:
{
"Parameters": {
"AWS_ENDPOINT_URL_DYNAMODB": "http://host.docker.internal:8000",
"AWS_ENDPOINT_URL_S3": "http://host.docker.internal:9000"
}
}DynamoDB and S3 calls resolve to the local endpoint; Secrets Manager, SSM Parameter Store, and everything else have no override, so they hit real AWS through your --assume-role / --from-cfn-stack credentials. The suffix is the service name uppercased (DYNAMODB, S3, SECRETS_MANAGER, ...); the bare AWS_ENDPOINT_URL (no suffix) redirects every service at once, so the per-service form is what makes the split selective. cdk-local only injects these variables — it does not run the endpoint for you; start the local server separately and point the URL at it.
host.docker.internal resolves to the host from inside the container — Docker Desktop (macOS / Windows) provides it natively, and on Linux native dockerd cdkl adds the host-gateway alias for you on the invoke / run-task / start-service / start-alb container runs (Docker 20.10+). On an older daemon the alias is skipped; use the Docker bridge address (e.g. http://172.17.0.1:8000) or put the local server on the same Docker network.
Hot reload — --watch
cdkl start-api --watch # reload API routes on save
cdkl start-service --watch # roll ECS replicas on save
cdkl start-alb --watch # roll ALB-fronted ECS replicas on save
cdkl start-cloudfront --watch # reload CloudFront Functions + origins on save
cdkl invoke-agentcore --ws --watch # reload an open /ws agent sessionEdit a handler and the next request hits the new code — no server restart. ECS reloads roll replicas one at a time so the service stays available across the reload (an external request stream against the ALB listener port sees zero connection refusals, even on multi-replica services). Synth failures keep the previous replica(s) serving. start-cloudfront --watch swaps its CloudFront Functions and origins in place; its Lambda@Edge / Function URL warm containers are boot-time only, so restart to pick up changes to their code. Honors cdk.json's watch.include / watch.exclude globs, so no separate cdk watch process is needed.
Reload classifier (interpreted-language fast path vs Dockerfile rebuild), shadow-replica TCP-probe timeout (--shadow-ready-timeout), and per-runtime caveats: docs/local-emulation.md#hot-reload---watch.
Local build override — --image-override
When a service's image is pinned to a deployed registry — ContainerImage.fromEcrRepository(...), typical under --from-cfn-stack — cdkl start-service / cdkl start-alb run those deployed image bytes locally, so your source edits never take effect, even with --watch. --image-override rebuilds the image locally with docker build instead, so iteration works while real DynamoDB / Secrets / SSM stay wired in.
Boot in a TTY and the command walks each detected pinned target with an interactive Dockerfile picker:
cdkl start-alb --from-cfn-stack # interactive boot prompt for each pinned targetOr name them up-front (CI / scripted setups), with build inputs:
cdkl start-alb --from-cfn-stack \
--image-override AppService=./services/app/Dockerfile \
--image-build-arg NODE_ENV=production \
--image-build-secret npmrc=./.npmrc \
--image-target builderPer-service build inputs (<svc>:KEY=VAL for build-arg / build-secret, <svc>=stage for target), monorepo recipes, private-registry npmrc threading, --no-interactive-overrides / --strict-overrides, and the --watch rebuild loop: docs/local-emulation.md#local-build-override---image-override.
start-service vs start-alb — which one?
Most CDK ECS apps boot multiple replicas behind an ALB. cdk-local exposes each layer separately so you can target the slice you care about:
| Goal | Command | How to reach |
|---|---|---|
| App logic / DB / response shape — hit the handler directly | cdkl start-service --max-tasks 1 --host-port 80=8080 | curl http://127.0.0.1:8080/... |
| ALB routing — listener rules, host-header / path / method, default actions, redirects, fixed-response, weighted forwards, authenticate-cognito / authenticate-oidc | cdkl start-alb --lb-port 443=8443 --tls | curl -H 'Host: api.example.com' https://127.0.0.1:8443/... |
| Multi-replica rolling-reload + Cloud Map service discovery | cdkl start-service (multi-replica default) | Sibling container on the cdkl-svc- network |
Why the extra flags on the simple case? The template's DesiredCount (typically 3 in production) is honored locally by default, but N replicas can't all bind the same host port — so start-service skips host publishing for multi-replica runs and the app is reachable only from inside the cdkl-svc- docker network. To get the simple curl http://127.0.0.1:... access path:
--max-tasks 1clamps the local replica count to 1 without touching your CDK code.- A privileged declared host port (
< 1024, e.g. 80) is auto-remapped to a free high host port — with a WARN naming the remap — because macOS Docker Desktop refuses to publish privileged ports and a< 1024host port needs root.--host-port <containerPort>=<hostPort>pins a specific host port instead.
start-alb uses the symmetric --lb-port <listenerPort>=<hostPort> for privileged listener ports like 80 / 443, and --tls (or --tls-cert / --tls-key) to terminate TLS locally instead of serving the HTTPS listener over plain HTTP (the default). Full resolution model: docs/cli-reference.md.
Supported resources
| Resource | Local execution |
|----------|-----------------|
| Lambda functions (ZIP, container image, Function URLs) | invoke — every current Lambda runtime |
| API Gateway (REST v1, HTTP v2, WebSocket) + Lambda Function URLs | start-api |
| ECS task definitions | run-task |
| ECS services | start-service |
| Cloud Map / Service Connect registry | start-service / start-alb — service discovery between local replicas |
| ALB-fronted ECS / Lambda services | start-alb — HTTP / HTTPS listeners, all six listener-rule conditions, weighted forwards, redirect / fixed-response, mixed ECS + Lambda targets, authenticate-cognito / authenticate-oidc (local Bearer-JWT enforcement), WebSocket Upgrade |
| CloudFront distributions (S3 + Lambda Function URL origins + CloudFront Functions + Lambda@Edge) | start-cloudfront — CloudFront Functions (viewer-request / viewer-response) and Lambda@Edge (real RIE container, all four event types) over an S3 origin served from local files — the BucketDeployment source or a --origin <id>=<dir> dir — or from real S3 on demand (--from-cfn-stack); plus Lambda Function URL origins, KeyValueStore reads, ResponseHeadersPolicy CORS, --tls, --watch (details) |
| Bedrock AgentCore Runtime agents | invoke-agentcore — single-shot (boot, invoke, tear down); start-agentcore — long-running warm serve, one container kept warm to hit repeatedly until ^C |
Lambda runs on every current AWS Lambda runtime — Node.js (18/20/22/24), Python (3.11–3.14), Ruby (3.2/3.3), Java (8.al2/11/17/21), .NET (6/8), and the OS-only provided.al2 / provided.al2023. The retired go1.x runtime is rejected with a pointer to migrate to provided.al2023.
Programmatic use
cdk-local also exports its commands as Commander factories so a host project can embed it into its own CLI, register custom state sources alongside the built-in --from-cfn-stack, and rebrand the embedded commands. See docs/library-mode.md for the API and an example.
License
Apache-2.0
