Skip to content
cdkd

local invoke (run Lambda functions locally)

cdkd local invoke <target> runs a Lambda function from a CDK app on the developer's machine, inside a Docker container that bundles the AWS Lambda Runtime Interface Emulator (RIE). Modeled on sam local invoke but reusing cdkd's synthesis / asset / construct-path plumbing.

Requires Docker. The first invocation pulls the Lambda base image (public.ecr.aws/lambda/nodejs:<version>, public.ecr.aws/lambda/python:<version>, public.ecr.aws/lambda/ruby:<version>, public.ecr.aws/lambda/java:<version>, public.ecr.aws/lambda/dotnet:<version>, or public.ecr.aws/lambda/provided:<al2|al2023> — ~600MB for the language-specific images, ~50MB for the OS-only provided.*); subsequent invocations reuse the cached image. Pass --no-pull to skip the docker pull round-trip altogether. Supported runtimes: nodejs18.x / nodejs20.x / nodejs22.x / nodejs24.x / python3.11 / python3.12 / python3.13 / python3.14 / ruby3.2 / ruby3.3 / java8.al2 / java11 / java17 / java21 / dotnet6 / dotnet8 / provided.al2 / provided.al2023. The deprecated go1.x runtime is rejected with a migration pointer to provided.al2023. Java, .NET, and provided.* are asset-backed only — inline Code.ZipFile is rejected with a routing message ("use lambda.Code.fromAsset(...)") because the Handler shape names a compiled artifact (package.Class::method for Java's JVM class; Assembly::Namespace.Class::Method for .NET's CLR assembly; an arbitrary bootstrap binary for provided.*).

A ZIP Lambda's Architectures: [x86_64] (default) / [arm64] is pinned to --platform linux/amd64 / linux/arm64 on the container's docker run (matching the container-image path). On an arch-mismatched host Docker emulates the function's declared arch, so a provided.* bootstrap compiled for the other architecture runs instead of failing with fork/exec /var/runtime/bootstrap: exec format error / Runtime.InvalidEntrypoint. The same pinning applies to cdkd local start-api's warm-container pool.

Container Lambdaslambda.DockerImageFunction(...) / Code.ImageUri is supported in addition to ZIP Lambdas. cdkd reads the function's local Dockerfile from cdk.out (via the asset manifest keyed off the :<hash> suffix on Code.ImageUri) and runs docker build locally, then docker run against the resulting image. When no asset matches (typically: invoking a stack deployed elsewhere), cdkd falls back to docker pull from ECR. Cross-account / cross-region pull is supported: cdkd auto-detects cross-account from sts:GetCallerIdentity, builds the ECR client for the URI's region, and (when --ecr-role-arn <arn> is passed) issues sts:AssumeRole to pick up permissions in the target account. Without --ecr-role-arn, cdkd falls through to the caller's credentials — works when the target ECR repository's resource policy grants the caller directly (AWS surfaces AccessDenied if missing, with a hint at the flag). Architectures: [x86_64] (default) and [arm64] are honored via --platform linux/amd64 / linux/arm64 on both the build and the run.

Target resolution

The positional <target> accepts two forms:

  • CDK display pathMyStack/MyApi/Handler. Matches the same prefix-rule cdkd uses for cdkd orphan: an L2 path resolves to the synthesized L1 child (MyStack/MyApi/Handler/Resource).
  • Stack-qualified logical IDMyStack:MyApiHandler1234ABCD. The colon is unambiguous because logical IDs cannot contain / or :.

Single-stack apps may omit the stack prefix entirely: cdkd local invoke MyHandler is valid when the app contains exactly one stack (mirrors cdkd deploy / cdkd destroy auto-detect).

When the target does not match anything, the error lists every Lambda in the resolved stack so the user can copy/paste a valid one.

Options

Option Default Description
-e, --event <file> {} JSON event payload file.
--event-stdin off Read event JSON from stdin (mutually exclusive with --event).
--env-vars <file> JSON env-var overrides, SAM-compatible shape: {"LogicalId":{"KEY":"VALUE"}} plus an optional top-level "Parameters" block applied to every invoke. null clears a key. The function-specific key may also be a CDK display path (MyStack/MyHandler — same form cdkd local invoke <target> accepts). Both forms coexist; later JSON entry wins on conflict (SAM apply-in-order).
--no-pull off Skip docker pull. Semantics differ by code path: ZIP Lambdas — skip pulling the public Lambda base image. Container Lambdas, local-build path — no-op (docker build's default does not refresh the FROM cache). Container Lambdas, ECR-pull fallback — skip docker pull AND error if the image is not in the local cache (re-run without --no-pull or pre-pull manually).
--no-build off Skip docker build on the Container Lambdas, local-build path (Code.ImageUri). Requires the deterministic cdkd-local-invoke-<hash> tag to already be in the local docker registry from a prior cdkd local invoke (or manual docker build); errors clearly when missing. No-op for ZIP Lambdas (no docker build runs there) AND for the Container Lambdas, ECR-pull fallback (use --no-pull to control that path). Compatible with --no-pull.
--ecr-role-arn <arn> Role ARN to assume before authenticating against ECR on the Container Lambdas, ECR-pull fallback path. Issues sts:AssumeRole via the default credential chain and uses the resulting temp creds for ecr:GetAuthorizationToken + docker pull. Required for cross-account pulls when the caller's identity does not already have direct cross-account access. Same-account / same-region pulls do not need this flag; cross-account without the flag falls back to the caller's credentials (succeeds when an IAM resource policy on the ECR repo grants the caller directly, else AWS surfaces AccessDenied). No-op when --no-pull is set.
--layer-role-arn <arn> Role to sts:AssumeRole before calling lambda:GetLayerVersion on every literal-ARN entry in Properties.Layers. Use only when the developer's own credentials cannot read the layer — typically a cross-account layer. AWS-published public layers (e.g. Lambda Powertools) are readable from every account and need no role. No-op for stacks whose layers are all same-stack AWS::Lambda::LayerVersion references.
--debug-port <port> off Set NODE_OPTIONS=--inspect-brk=0.0.0.0:<port> and publish the port; attach a Node debugger to step through the handler.
--container-host <host> 127.0.0.1 Host to bind the RIE port to.
--assume-role [arn] off STS-assume the deployed function's execution role and forward the resulting temp credentials to the container, so the handler runs under the deployed role's narrow permissions instead of the developer's typically-admin shell credentials. Three forms: (1) --assume-role <arn> assumes the explicit ARN (precedence wins); (2) --assume-role (bare) auto-resolves the function's Properties.Role from cdkd state (requires --from-state); (3) --no-assume-role explicitly opts out (forces dev creds even with --from-state). Off by default — when omitted, AWS_ACCESS_KEY_ID / AWS_SECRET_ACCESS_KEY / AWS_SESSION_TOKEN / AWS_REGION are passed through unchanged (SAM-compatible default). STS failures degrade to a warn + dev-creds fallback.
-a, --app <cmd-or-dir> CDK app command or pre-synthesized cdk.out directory. Default: synth every time (Q2 recommendation C). Pass -a cdk.out to skip synthesis when iterating.
--output <dir> cdk.out Output directory for synthesis.
--from-state off Read cdkd's S3 state for the target stack and substitute Ref / Fn::GetAtt / Fn::Sub / Fn::Join placeholders + AWS pseudo parameters (${AWS::AccountId} / ${AWS::Region} / ${AWS::Partition} / ${AWS::URLSuffix}) in env vars with the deployed physical IDs / attributes. Off by default — keeps PR 1's literal-only / warn-and-drop behavior. See State-driven env recovery (--from-state) below.
--from-cfn-stack [cfn-stack-name] off Read a deployed CloudFormation stack via DescribeStackResources and substitute Ref / Fn::ImportValue placeholders in env vars with the deployed physical IDs / exports. Use for CDK apps deployed via the upstream CDK CLI (cdk deploy). Bare form uses the cdkd stack name; pass an explicit value when the CFn stack name differs. Mutually exclusive with --from-state — pick one source. Fn::GetAtt in a consumer Lambda's own env vars is recovered from the deployed function config (lambda:GetFunctionConfiguration, via cdk-local@0.10.0); Fn::GetAtt at other sites still warn-and-drops, except a same-stack ECR repository's Arn / RepositoryUri in a container image URI (synthesized from the recovered physical name + pseudo parameters). See CloudFormation-driven env recovery (--from-cfn-stack) below.
--state-bucket <bucket> auto S3 bucket containing cdkd state. Falls back to CDKD_STATE_BUCKET env or cdk.json context.cdkd.stateBucket, then the default cdkd-state-{accountId}. Only used with --from-state.
--state-prefix <prefix> cdkd S3 key prefix for state files. Only used with --from-state.
--stack-region <region> auto Region of the state record to read. Required for --from-state when the same stack name has state in multiple regions. Also drives the CFn client region for --from-cfn-stack (cdkd does not have a separate --cfn-stack-region flag).

Environment variables

Template Properties.Environment.Variables entries:

  • Literal values (string / number / boolean) are passed through as-is.
  • Intrinsic-valued entries (Ref / Fn::GetAtt / Fn::Sub / Fn::Join, plus the ${AWS::AccountId} / ${AWS::Region} / ${AWS::Partition} / ${AWS::URLSuffix} pseudo parameters) need state (and a single sts:GetCallerIdentity for ${AWS::AccountId}) to resolve. Without --from-state v1 emits a warning naming the variable and drops it (rather than silently substituting garbage); pass --from-state (see below) to recover deployed values from cdkd's S3 state, or override intrinsics via --env-vars.

Standard Lambda runtime env vars are always set: AWS_LAMBDA_FUNCTION_NAME, AWS_LAMBDA_FUNCTION_MEMORY_SIZE, AWS_LAMBDA_FUNCTION_TIMEOUT, AWS_LAMBDA_FUNCTION_VERSION, AWS_LAMBDA_LOG_GROUP_NAME, AWS_LAMBDA_LOG_STREAM_NAME. The handler's context.* fields look real.

State-driven env recovery (--from-state)

When the target stack has been deployed with cdkd deploy, the function's intrinsic-valued env vars (Ref / Fn::GetAtt / Fn::Sub) reference resources whose physical IDs only exist in AWS. PR 1's behavior is to drop those entries with a warn — correct when there's no source of truth, but unhelpful when cdkd already knows them. --from-state opts in to reading cdkd's S3 state and substituting the deployed values before the env block reaches the container.

Resolution priority (highest priority wins):

  1. --env-vars file function-specific entry ({LogicalId: {KEY: VALUE}}).
  2. --env-vars file global Parameters block.
  3. --from-state substituted intrinsic (when the flag is set AND the template entry was a supported intrinsic AND substitution succeeded).
  4. Template literal value.

Supported intrinsics: Ref (→ state.resources[id].physicalId), Fn::GetAtt (→ state.resources[id].attributes[attr], JSON-stringified when the cached value is an object/array), Fn::Sub (single-string and two-arg forms; ${LogicalId} / ${LogicalId.attr} / ${AWS::*} placeholders are substituted in place — the two-arg form's bindings map can also carry intrinsic values, recursively resolved), Fn::Join (every element recursively resolved, then joined), and Ref: AWS::* pseudo parameters (AccountId / Region / Partition / URLSuffix) resolved against STS GetCallerIdentity + the configured region.

Failure mode: per-key best-effort. When a substitution can't be produced (state missing for the referenced resource, attribute not captured at deploy time, unsupported intrinsic in Fn::Sub), the key is reported via warn and dropped — same UX as PR 1. State-load failures (no state record, multi-region ambiguity without --stack-region, bucket-resolution error) degrade to warn-and-fall-back rather than aborting the whole invoke.

Auto-assume execution role: when --from-state is paired with bare --assume-role (no ARN argument), cdkd reads the function's Properties.Role from cdkd state, resolves Fn::GetAtt: [<RoleId>, 'Arn'] shapes against the sibling IAM Role resource's recorded Arn attribute, and STS-assumes that role automatically — no manual ARN lookup required. When --from-state is set WITHOUT --assume-role, the legacy hint path fires instead: cdkd logs the deployed role ARN once so users can re-run with --assume-role. Pass --no-assume-role to explicitly opt out even with --from-state; pass --assume-role <arn> to override the resolved ARN with an explicit one. STS failures (insufficient permissions / trust-policy mismatch) degrade to a warn + dev-creds fallback — this is a developer-loop tool, not a security boundary.

Pseudo parameters: when the function's template env contains any intrinsic value, cdkd local invoke --from-state issues a single sts:GetCallerIdentity (for ${AWS::AccountId}) and derives partition / urlSuffix from the resolved region (--region > AWS_REGION > AWS_DEFAULT_REGION > the synth-derived stack region). STS failures degrade to warn — substitution still runs for non-AWS::* refs; affected ${AWS::*} placeholders fall back to warn + drop. Literal-only env maps skip the STS hop.

Out of scope (deferred): cross-stack Fn::ImportValue / Fn::GetStackOutput, other intrinsics (Fn::Select, Fn::Split, Fn::If, etc.). Anything beyond the listed supported intrinsics is treated as unresolved (warn + drop).

# Single-region stack: --from-state alone is enough
cdkd deploy MyStack
cdkd local invoke MyStack/MyApi/Handler --from-state

# Multi-region: disambiguate the state record
cdkd local invoke MyStack/MyApi/Handler --from-state --stack-region us-west-2

# Combine with --env-vars to override a single key (override wins)
cdkd local invoke MyStack/MyApi/Handler --from-state \
  --env-vars '{"Parameters":{"DEBUG":"1"}}'

CloudFormation-driven env recovery (--from-cfn-stack)

--from-state only works when the target stack was deployed via cdkd deploy — cdkd reads its own S3 state and that state only exists for cdkd-deployed stacks. For CDK apps deployed via the upstream CDK CLI (cdk deploy → CloudFormation), use --from-cfn-stack instead: cdkd calls cloudformation:DescribeStackResources against the named CFn stack to populate the same per-logical-id physical-id map that --from-state would have built from cdkd state, then runs the existing substitution engine against it.

# Bare flag — uses the cdkd stack name as the CFn stack name
# (typical for CDK apps where they match).
cdk deploy MyStack
cdkd local invoke MyStack/MyApi/Handler --from-cfn-stack

# Explicit CFn stack name — use when the deployed CFn stack name
# differs from the cdkd / CDK display name (e.g. when CDK's `stackName`
# prop was overridden).
cdkd local invoke MyStack/MyApi/Handler --from-cfn-stack MyExplicitCfnStackName

# Cross-region CFn stack — --stack-region drives the CFn client region.
cdkd local invoke MyStack/MyApi/Handler --from-cfn-stack --stack-region eu-west-1

What's resolved: Ref: <LogicalId> against DescribeStackResources (one CFn API call per stack) and Fn::ImportValue: <ExportName> against cloudformation:ListExports (paginated, memoized for one substitution pass).

Fn::GetAtt is recovered for a consumer Lambda's OWN env vars; other sites warn-and-drop. CFn's DescribeStackResources does NOT return per-attribute values — it only exposes (LogicalResourceId, PhysicalResourceId, ResourceType) triplets. But CloudFormation already resolved every intrinsic at deploy time, so a consumer Lambda's Environment.Variables already carries the concrete value. As of cdk-local@0.10.0 (which cdkd consumes through the --from-cfn-stack shim), env keys whose template value is an Fn::GetAtt the static substituter could not resolve are filled at runtime by reading the deployed function's config (lambda:GetFunctionConfiguration) — this covers Fn::GetAtt / Fn::Sub / Fn::ImportValue / cross-stack Ref in Lambda env vars uniformly, without provider-specific describe calls. Fn::GetAtt at NON-Lambda-env sites (e.g. ECS container env) is still warn-and-dropped; override the affected entry via --env-vars if the value is critical.

Fn::GetStackOutput is rejected with a clear warn naming the cdkd- vs-CFn gap: it's a cdkd-specific intrinsic with no CloudFormation equivalent (CFn cross-stack vocabulary is Fn::ImportValue against an explicit Outputs.<name>.Export block). Use Fn::ImportValue or pass --from-state instead.

Mutually exclusive with --from-state — the CLI rejects the combination at parse time. The two flags target different state sources (cdkd's S3 state vs CloudFormation); asking for both is ambiguous about which wins.

Region handling: the CFn client is region-bound at construction time using the precedence --stack-region > --region > AWS_REGION

AWS_DEFAULT_REGION > the synth-derived stack region. There is intentionally no separate --cfn-stack-region flag — --stack-region does double duty. When NONE of these signals is set the CLI throws with a remediation message (distinct from --from-state's silent us-east-1 fallback; CFn DescribeStackResources queries a specific region and silently picking us-east-1 would query the wrong stack environment).

Multi-stack guard: local start-api / local start-service route multiple stacks in one invocation. Bare --from-cfn-stack works there because each routed stack uses its own cdkd stack name as the CFn stack name. Explicit --from-cfn-stack <name> is rejected when more than one stack is routed (the explicit name would apply to every routed stack and silently mismap Ref lookups whose logical IDs happen to collide between siblings). Use bare --from-cfn-stack for multi-stack apps, or run one cdkd invocation per stack.

Failure modes: DescribeStackResources failures (stack not found, access denied, throttling) degrade to a per-key warn + drop, same UX as the --from-state warn-and-fall-back path. ListExports failures only affect Fn::ImportValue resolution; same-stack Ref substitutions still succeed because they only need the DescribeStackResources result.

Asset resolution

ZIP Lambdas: cdkd uses the CDK-blessed Metadata['aws:asset:path'] hint on each Lambda's CFn resource (the same source SAM uses) to find the local unzipped asset directory under cdk.out, and bind-mounts it at /var/task read-only. Code.ZipFile (inline) functions are materialized to a tmpdir using the file path implied by the function's Handler property (index.handlertmpdir/index.js).

Lambda Layers

Same-stack AWS::Lambda::LayerVersion references in Properties.Layers are resolved automatically and bind-mounted at /opt (read-only) inside the container. The flow:

  1. cdkd local invoke walks Properties.Layers left-to-right.
  2. Each entry must be {Ref: '<LayerLogicalId>'} or {Fn::GetAtt: ['<LayerLogicalId>', 'Ref']} pointing at an AWS::Lambda::LayerVersion resource in the same stack. The layer's Metadata['aws:asset:path'] is read the same way Lambda code is located — the layer asset is unzipped under cdk.out/asset.<hash>/ ready to bind-mount.
  3. cdkd produces a single bind mount at /opt:
    • Single layer: the layer's asset dir is bind-mounted directly (no copy).
    • Multiple layers: each layer's contents are copied into a freshly-allocated tmpdir IN ORDER (later layers overwrite earlier files via cpSync({force: true})); the merged tmpdir is then bind-mounted at /opt and removed in the cleanup path.
    • The merge mirrors AWS Lambda's actual runtime behavior: AWS extracts every layer ZIP into /opt in template order so later layers shadow earlier files ("last layer wins on file collision"). cdkd cannot rely on multiple -v ...:/opt:ro entries — Docker rejects duplicate bind mounts at the same target path with Error response from daemon: Duplicate mount point: /opt.
  4. The layer's directory layout (/opt/python/..., /opt/nodejs/..., /opt/lib/..., etc.) is the user's responsibility — cdkd does NOT inspect the contents.

Literal-ARN layer entries: a Layers entry that is the string arn:<partition>:lambda:<region>:<account>:layer:<name>:<version> is resolved by downloading that layer version's ZIP and unzipping it into a host tmpdir, which then joins the same /opt merge as same-stack layers — so "last layer wins" holds across both kinds. This covers AWS-published public layers (Lambda Powertools, the Datadog extension) and cross-account / cross-region shared layers. Pass --layer-role-arn <arn> to sts:AssumeRole before lambda:GetLayerVersion when the developer's own credentials cannot read the layer — typically a cross-account one; AWS-published public layers are readable from every account and need no role.

The ARN's partition is derived from its region rather than matched against a hardcoded list, so the ARN in any of the eight partitions — commercial, aws-cn, aws-us-gov, aws-iso, aws-iso-b, aws-iso-e, aws-iso-f, aws-eusc — now PARSES; previously only three did, and the other five were refused outright at resolution. The two segments must also AGREE: arn:aws-cn:lambda:us-east-1:... is refused, naming the disagreement, because us-east-1 does not belong to aws-cn. A region cdkd's partition table does not recognise resolves to the commercial partition, so a brand-new commercial region keeps working with arn:aws:.

Parsing is not the whole path, and the rest is still commercial-only. The download itself runs through cdk-local, which rebuilds the ARN with a hardcoded aws partition before calling lambda:GetLayerVersion (node_modules/cdk-local/dist/local-studio-BBtUAVNy.js:15214`arn:aws:lambda:${layer.region}:${layer.accountId}:layer:${layer.name}`). So a layer ARN in any of the seven non-commercial partitions gets past cdkd's parse and then fails at the AWS call instead. aws-cn and aws-us-gov already parsed before this change and gain nothing from it; for the five that did not (aws-iso, aws-iso-b, aws-iso-e, aws-iso-f, aws-eusc) what changed is WHICH failure you get — an AWS-side error naming the real blocker, rather than cdkd refusing to read the ARN at all. End-to-end support for those partitions needs an upstream cdk-local fix.

Out of scope (v1) — hard-errors with a clear pointer at the offending entry:

  • Layer entries that are neither a same-stack reference nor a well-formed layer-version ARN (a malformed ARN, a function ARN, an unversioned layer ARN, or a partition that disagrees with the region).
  • Same-stack refs that don't point at an AWS::Lambda::LayerVersion (typo'd logical ID).
  • Same-stack refs to a LayerVersion whose Metadata['aws:asset:path'] is missing.

Container Lambdas (Code.ImageUri): the Layers property is silently ignored — matches AWS behavior, since container images bake their layers at build time and AWS rejects Layers on container Lambdas at deploy time.

Container Lambdas (Code.ImageUri): cdkd extracts the asset hash from the :<hash> tail of the image URI (CDK synthesizes the URI as a Fn::Sub whose body ends in the asset hash) and looks the matching entry up in the stack's asset manifest (cdk.out/<stack>.assets.json, dockerImages[<hash>]). When the lookup hits, cdkd local invoke calls docker build against the recorded build context. When the lookup misses AND the manifest contains exactly one Docker asset, that single asset is used (single-asset fallback — covers digest-pinned URIs). When both miss, cdkd falls back to ECR pull with cross-account / cross-region support: cdkd builds the ECR client for the URI's region and (when --ecr-role-arn <arn> is passed) issues sts:AssumeRole to gain credentials in the target account before authenticating to ECR and pulling. Without --ecr-role-arn, cdkd uses the caller's credentials directly (works when the ECR repo's resource policy grants the caller, else AWS surfaces AccessDenied with a hint at the flag). ImageConfig.Command becomes the docker run CMD; ImageConfig.EntryPoint (when set) becomes --entrypoint <first> plus the rest as positional args; ImageConfig.WorkingDirectory becomes --workdir. When EntryPoint is unset (the common case), the image's default entrypoint stays in charge — for AWS Lambda base images that's /lambda-entrypoint.sh, which routes to RIE on port 8080.

Ephemeral storage (/tmp cap)

When a Lambda's template declares Properties.EphemeralStorage.Size (typical CDK shape: new lambda.Function(this, 'X', { ephemeralStorageSize: cdk.Size.gibibytes(2) })), cdkd local invoke adds --tmpfs /tmp:rw,size=<N>m to the docker run command so the container's /tmp is a memory-backed filesystem capped at the templated value (N MiB; cdk.Size.gibibytes(2) serializes to 2048). Handlers that exceed the deployed cap fail locally with ENOSPC the way they would on AWS, and handlers that detect free space via statvfs / df see the configured cap rather than the host's overlay-fs.

Applies to both ZIP and IMAGE (container) Lambdas — --tmpfs overlays mount-time inside any container regardless of base image. Container Lambdas get an [info] log line at startup so users notice the /tmp override on top of whatever their Dockerfile placed there.

When EphemeralStorage is absent, no --tmpfs is emitted and the container's /tmp is whatever the base image provides (AWS Lambda base images don't mount a sized tmpfs themselves, so the existing behavior is preserved). Templates over the AWS 10240 MiB (10 GiB) ceiling hard-error at resolve time with an actionable message rather than hanging on a docker run that AWS would have refused anyway. Intrinsic-valued Size entries (the {Ref: 'SomeParam'} shape) drop silently to no---tmpfs since local invoke cannot resolve them without the Parameters context the deploy engine has.

The same cap applies to cdkd local start-api's warm container pool — each cold-started container for a Lambda with EphemeralStorage gets the same sized /tmp.

Reaching a server on the host (host.docker.internal)

The Lambda container can reach a server bound on the host loopback — an AWS_ENDPOINT_URL_* local endpoint (e.g. a local DynamoDB / S3 mock), or a tunneled VPC resource — via the host.docker.internal hostname. Docker Desktop (macOS / Windows) resolves it natively; on Linux native dockerd cdkd injects the --add-host host.docker.internal:host-gateway mapping automatically (Docker 20.10+). On an older / unavailable daemon the mapping is silently skipped (never an error). The same applies to cdkd local run-task container runs, and — inherited from cdk-local's ECS service emulator engine — to cdkd local start-service / cdkd local start-alb.

local invoke / local invoke-agentcore output streams

Everything cdkd's own logger prints goes to stderr. Both commands reserve stdout unconditionally, so every cdkd status line -- Synthesizing CDK app..., Target: ..., Starting container ..., layer resolution notices, and cdkd's --verbose debug output -- goes to stderr, the way sam local invoke has always done it. The lines are moved, not suppressed: a terminal shows what it always did, and 2>&1 restores the old single-stream view.

But stdout is not yet payload-only, so pipe through tail -1. Two things still reach it, neither routed by cdkd's logger:

  1. The container's own stdout, piped through by streamLogs. The Lambda runtime emulator puts START / END / REPORT and every handler log line on the container's stdout -- console.error included, which is measured rather than assumed -- so any handler that prints lands ahead of the response.
  2. cdk-local's own logger. The container-image build path is reused from cdk-local, which has a separate logger with no reservation concept, so Building container image (platform=...) and Skipping docker build ... print on stdout for a container-image Lambda.

A third used to be listed here and is now closed: docker pull progress reached stdout because runDockerForeground passes stdio: 'inherit', and cdkd runs it unconditionally for an ECR image, so it needed no flag at all. While a command holds the reservation that child's fd 1 is now redirected to fd 2. The remaining gap is streamLogs alone.

# Safe today: the response payload is always the LAST line on stdout.
cdkd local invoke MyStack/Handler --event event.json | tail -1 | jq .body
cdkd local invoke-agentcore MyStack/Agent 2> progress.log | tail -1

A ZIP-code Lambda whose handler prints nothing hits neither, so cdkd local invoke MyStack/Handler | jq does work there -- it is just not a guarantee cdkd can make yet for every target.

local start-api, local run-task and local start-service are unaffected -- their stdout is a human surface (route table, prefixed container logs), not a payload.

local invoke exit codes

  • 0 — RIE answered, regardless of whether the handler returned a success payload OR an error payload. Lambda-style: a thrown handler produces a 200 with an error structure on AWS, and we mirror that.
  • 1 — cdkd-side errors before/after the handler ran: Docker not installed, image pull failed, target not found, RIE port unreachable after the readiness window, container exited before responding.

v1 scope (out of scope, deferred)

Out of scope Deferred to
Java / Go / Ruby / .NET runtimes Future PRs
Cross-account / cross-region / pre-existing-ARN Lambda Layers Shipped — same-stack AWS::Lambda::LayerVersion refs and literal layer-version ARNs are both resolved, the latter by downloading the layer version; see the "Lambda Layers" section above
Cross-stack Fn::ImportValue / Fn::GetStackOutput in --from-state Future PR
Fn::Select / Fn::Split / Fn::If etc. in --from-state Future PR (warn + drop today)
SQS / S3 event source emulation Future PR
VPC simulation Never (local can't replicate VPC)
Custom Resources (Custom::*) Never — these are invoked by the deploy framework, not by users. cdkd surfaces a clear error pointing at the underlying ServiceToken Lambda.

Last updated: