cdk-local 0.5.1 → 0.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -22,7 +22,7 @@ cdk-local deliberately does NOT emulate AWS managed services. The bet is: keep d
22
22
  It also picks up where `sam local` leaves off:
23
23
 
24
24
  - **CDK-native** — point it at your CDK app's `cdk.json`. No SAM templates, no extra config files.
25
- - **Wider coverage** — Lambda (ZIP + container image), API Gateway REST v1 / HTTP v2 / Function URL / WebSocket API, ECS run-task, ECS service with Service Connect + Cloud Map.
25
+ - **Wider coverage** — Lambda (ZIP + container image, plus Function URL), API Gateway REST v1 / HTTP v2 / WebSocket API, ECS run-task, ECS service with Service Connect + Cloud Map.
26
26
  - **Real container images** — Lambda code runs in the real `public.ecr.aws/lambda/*` base image via Lambda Runtime Interface Emulator (RIE). ECS tasks run as real Docker containers. The only dependency is Docker.
27
27
 
28
28
  ## What runs locally, what doesn't
@@ -32,7 +32,7 @@ cdk-local runs your **application compute** locally in Docker, using your CDK ap
32
32
  **Runs locally (application compute):**
33
33
 
34
34
  - **Lambda functions** — your code in a real `public.ecr.aws/lambda/*` container via the Lambda Runtime Interface Emulator
35
- - **API Gateway** — REST v1 / HTTP v2 / Function URL / WebSocket served by a local HTTP server
35
+ - **HTTP APIs & Function URLs** — API Gateway REST v1 / HTTP v2 / WebSocket and Lambda Function URLs served by a local HTTP server
36
36
  - **ECS** — tasks and services as real Docker containers (awsvpc / Service Connect / Cloud Map registry)
37
37
  - **Authorizers** — Lambda authorizers, Cognito User Pool JWT verification, IAM SigV4 verification
38
38
 
@@ -66,14 +66,26 @@ cdkl invoke MyStack/MyFunction --event ./event.json
66
66
 
67
67
  ![cdkl invoke against a local sample CDK app — no AWS account, no deploy](assets/cdkl-invoke.gif)
68
68
 
69
- #### API Gateway — `start-api`
69
+ #### HTTP APIs & Function URLs — `start-api`
70
70
 
71
- Serve your API Gateway routes (REST v1 / HTTP v2 / Function URL / WebSocket) on a local HTTP server.
71
+ Serve your app's HTTP surface locally — API Gateway routes (REST v1 / HTTP v2 / WebSocket) and Lambda Function URLs on a local HTTP server.
72
72
 
73
73
  ```bash
74
+ # Single-stack app: serve every API in the stack (each on its own port)
75
+ cdkl start-api
76
+
77
+ # Or target one API
74
78
  cdkl start-api MyStack/MyApi
75
79
  ```
76
80
 
81
+ In a multi-stack app, a bare `cdkl start-api` errors out rather than serving every stack's API at once (so a side-stack's API is never booted by accident). Pick the synth stack with the first of these you supply:
82
+
83
+ - `--stack <name>` — the synth stack name, matched against your CDK app.
84
+ - `--from-cfn-stack <name>` — the deployed CloudFormation stack name; doubles as the stack selector (see [section 2](#2-bound-to-a-deployed-stack)).
85
+ - a stack-qualified target like `MyStack/MyApi` — the `MyStack` prefix selects the stack.
86
+
87
+ See [docs/cli-reference.md](docs/cli-reference.md) for the full precedence rules.
88
+
77
89
  #### ECS — `run-task` / `start-service`
78
90
 
79
91
  Run an ECS task definition once, or start a long-running service with Service Connect / Cloud Map registry.
@@ -91,7 +103,7 @@ Use this for fast iteration on Lambda code, API routing checks, and container ta
91
103
 
92
104
  Once your stack is deployed to AWS (via the AWS CDK CLI or any other tool), pass `--from-cfn-stack <StackName>` and cdk-local reads the deployed CloudFormation stack to inject real ARNs, Secrets values, and IAM credentials (resolved from your current AWS profile) into the local execution.
93
105
 
94
- #### API Gateway — `start-api` (the headline use case)
106
+ #### HTTP APIs & Function URLs — `start-api` (the headline use case)
95
107
 
96
108
  A local API talking to real AWS — point a frontend at it for end-to-end debugging, including real Cognito JWT verification.
97
109
 
package/dist/cli.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { i as createLocalInvokeCommand, n as createLocalRunTaskCommand, r as createLocalStartApiCommand, t as createLocalStartServiceCommand } from "./local-start-service-gYRarUgM.js";
2
+ import { i as createLocalInvokeCommand, n as createLocalRunTaskCommand, r as createLocalStartApiCommand, t as createLocalStartServiceCommand } from "./local-start-service-2tcrICJq.js";
3
3
  import { Command } from "commander";
4
4
 
5
5
  //#region src/cli/index.ts
6
6
  const program = new Command();
7
- program.name("cdkl").description("Run AWS CDK stacks locally with Docker.").version("0.5.1");
7
+ program.name("cdkl").description("Run AWS CDK stacks locally with Docker.").version("0.5.2");
8
8
  program.addCommand(createLocalInvokeCommand());
9
9
  program.addCommand(createLocalStartApiCommand());
10
10
  program.addCommand(createLocalRunTaskCommand());
package/dist/index.js CHANGED
@@ -1,3 +1,3 @@
1
- import { a as LocalStateSourceError, c as rejectExplicitCfnStackWithMultipleStacks, d as CfnLocalStateProvider, i as createLocalInvokeCommand, l as resolveCfnRegion, n as createLocalRunTaskCommand, o as createLocalStateProvider, r as createLocalStartApiCommand, s as isCfnFlagPresent, t as createLocalStartServiceCommand, u as resolveCfnStackName } from "./local-start-service-gYRarUgM.js";
1
+ import { a as LocalStateSourceError, c as rejectExplicitCfnStackWithMultipleStacks, d as CfnLocalStateProvider, i as createLocalInvokeCommand, l as resolveCfnRegion, n as createLocalRunTaskCommand, o as createLocalStateProvider, r as createLocalStartApiCommand, s as isCfnFlagPresent, t as createLocalStartServiceCommand, u as resolveCfnStackName } from "./local-start-service-2tcrICJq.js";
2
2
 
3
3
  export { CfnLocalStateProvider, LocalStateSourceError, createLocalInvokeCommand, createLocalRunTaskCommand, createLocalStartApiCommand, createLocalStartServiceCommand, createLocalStateProvider, isCfnFlagPresent, rejectExplicitCfnStackWithMultipleStacks, resolveCfnRegion, resolveCfnStackName };
@@ -3802,7 +3802,7 @@ async function runDetached(opts) {
3802
3802
  const ro = mount.readOnly ? ":ro" : "";
3803
3803
  args.push("-v", `${mount.hostPath}:${mount.containerPath}${ro}`);
3804
3804
  }
3805
- for (const [k, v] of Object.entries(opts.env)) args.push("-e", `${k}=${v}`);
3805
+ const passthroughEnv = appendEnvFlags(args, opts.env, SENSITIVE_ENV_KEYS);
3806
3806
  if (opts.tmpfs) args.push("--tmpfs", `${opts.tmpfs.target}:rw,size=${opts.tmpfs.sizeMb}m`);
3807
3807
  if (opts.workingDir) args.push("--workdir", opts.workingDir);
3808
3808
  let entryPointTail = [];
@@ -3813,7 +3813,10 @@ async function runDetached(opts) {
3813
3813
  args.push(opts.image, ...entryPointTail, ...opts.cmd);
3814
3814
  getLogger().child("docker").debug(`${getDockerCmd()} ${redactAwsCredentialsInArgs(args).join(" ")}`);
3815
3815
  try {
3816
- const { stdout } = await execFileAsync$3(getDockerCmd(), args, { maxBuffer: 10 * 1024 * 1024 });
3816
+ const { stdout } = await execFileAsync$3(getDockerCmd(), args, {
3817
+ maxBuffer: 10 * 1024 * 1024,
3818
+ ...execEnvForSecrets(passthroughEnv)
3819
+ });
3817
3820
  return stdout.trim();
3818
3821
  } catch (error) {
3819
3822
  const err = error;
@@ -3908,23 +3911,66 @@ function pickFreePort() {
3908
3911
  });
3909
3912
  }
3910
3913
  /**
3911
- * AWS credential keys whose values must NOT be written to the debug log.
3912
- * `forwardAwsEnv` / `assumeLambdaExecutionRole` (in `local-invoke.ts`)
3913
- * push these via `-e <KEY>=<value>` flags into `runDetached`'s args
3914
- * array, and `cdkl invoke --verbose` would otherwise leak them
3915
- * into stdout / log files. Only the matching `-e <KEY>=<value>` pair is
3916
- * redacted; non-credential `-e KEY=val` entries pass through unchanged.
3914
+ * Env keys whose VALUES are sensitive (AWS credentials). These are
3915
+ * routed through docker's value-from-process-env form by
3916
+ * {@link appendEnvFlags} so the value never appears in `docker run`'s
3917
+ * argv (`ps` / `/proc/<pid>/cmdline`), and they are also redacted by
3918
+ * {@link redactAwsCredentialsInArgs} as a defense for any path that
3919
+ * still emits the inline `-e <KEY>=<value>` form into the debug log.
3917
3920
  */
3918
- const REDACTED_ENV_KEYS = new Set([
3921
+ const SENSITIVE_ENV_KEYS = new Set([
3919
3922
  "AWS_ACCESS_KEY_ID",
3920
3923
  "AWS_SECRET_ACCESS_KEY",
3921
3924
  "AWS_SESSION_TOKEN"
3922
3925
  ]);
3923
3926
  /**
3927
+ * Append `-e` flags for `env` to `args`, routing keys in `sensitiveKeys`
3928
+ * through docker's value-from-process-env form (`-e KEY`, with NO
3929
+ * `=value`). Docker reads the value from its OWN environment at run time,
3930
+ * so the secret never lands in `docker run`'s argv — invisible to
3931
+ * `ps aux` / `/proc/<pid>/cmdline` (other users on a shared host) and to
3932
+ * shell history. Non-sensitive keys keep the inline `-e KEY=VALUE` form
3933
+ * (fine for non-secret config, and keeps `--debug` output readable).
3934
+ *
3935
+ * Returns the `{ KEY: value }` map of routed-through keys; the caller MUST
3936
+ * merge it into the spawned docker process's `env` (see
3937
+ * {@link execEnvForSecrets}) so docker can resolve each passed-through
3938
+ * key. Values still appear in `docker inspect` Config.Env — that is
3939
+ * inherent to container env and matches production behavior.
3940
+ *
3941
+ * Unlike `--env-file`, this handles multi-line values (e.g. PEM secrets)
3942
+ * and needs no temp file on disk.
3943
+ */
3944
+ function appendEnvFlags(args, env, sensitiveKeys) {
3945
+ const passthrough = {};
3946
+ for (const [k, v] of Object.entries(env)) if (sensitiveKeys.has(k)) {
3947
+ args.push("-e", k);
3948
+ passthrough[k] = v;
3949
+ } else args.push("-e", `${k}=${v}`);
3950
+ return passthrough;
3951
+ }
3952
+ /**
3953
+ * Build the `env` option for an `execFile` / `spawn` call that runs a
3954
+ * `docker run` whose args include passed-through sensitive keys (from
3955
+ * {@link appendEnvFlags}). Returns `{ env: {...process.env, ...passthrough} }`
3956
+ * so docker inherits the normal environment PLUS the sensitive values it
3957
+ * must resolve, or `{}` when there is nothing to pass through (preserving
3958
+ * the default inherited-environment behavior).
3959
+ */
3960
+ function execEnvForSecrets(passthrough) {
3961
+ if (Object.keys(passthrough).length === 0) return {};
3962
+ return { env: {
3963
+ ...process.env,
3964
+ ...passthrough
3965
+ } };
3966
+ }
3967
+ /**
3924
3968
  * Returns a copy of `args` with any `-e <KEY>=<value>` pair whose KEY is
3925
- * in {@link REDACTED_ENV_KEYS} replaced with `-e <KEY>=***`. The actual
3969
+ * in {@link SENSITIVE_ENV_KEYS} replaced with `-e <KEY>=***`. The actual
3926
3970
  * `args` passed to `spawn` are never mutated — this is for log output
3927
- * only.
3971
+ * only. With {@link appendEnvFlags} routing credentials through the
3972
+ * value-less `-e KEY` form this rarely fires, but it stays as defense for
3973
+ * any inline-form credential that slips into a logged command.
3928
3974
  */
3929
3975
  function redactAwsCredentialsInArgs(args) {
3930
3976
  const out = [];
@@ -3935,7 +3981,7 @@ function redactAwsCredentialsInArgs(args) {
3935
3981
  const eqIdx = next.indexOf("=");
3936
3982
  if (eqIdx > 0) {
3937
3983
  const key = next.substring(0, eqIdx);
3938
- if (REDACTED_ENV_KEYS.has(key)) {
3984
+ if (SENSITIVE_ENV_KEYS.has(key)) {
3939
3985
  out.push("-e", `${key}=***`);
3940
3986
  i++;
3941
3987
  continue;
@@ -15731,11 +15777,14 @@ async function createNetworkAndSidecar(args) {
15731
15777
  if (credentials.sessionToken) sidecarEnv["AWS_SESSION_TOKEN"] = credentials.sessionToken;
15732
15778
  }
15733
15779
  if (cluster) sidecarEnv["CLUSTER"] = cluster;
15734
- for (const [k, v] of Object.entries(sidecarEnv)) sidecarArgs.push("-e", `${k}=${v}`);
15780
+ const sidecarPassthroughEnv = appendEnvFlags(sidecarArgs, sidecarEnv, SENSITIVE_ENV_KEYS);
15735
15781
  sidecarArgs.push(METADATA_ENDPOINT_IMAGE);
15736
15782
  logger.info(`Starting ECS local-container-endpoints sidecar at ${sidecarIp}...`);
15737
15783
  try {
15738
- const { stdout } = await execFileAsync$2(getDockerCmd(), sidecarArgs, { maxBuffer: 10 * 1024 * 1024 });
15784
+ const { stdout } = await execFileAsync$2(getDockerCmd(), sidecarArgs, {
15785
+ maxBuffer: 10 * 1024 * 1024,
15786
+ ...execEnvForSecrets(sidecarPassthroughEnv)
15787
+ });
15739
15788
  return stdout.trim();
15740
15789
  } catch (err) {
15741
15790
  await destroyNetworkOnly(networkName);
@@ -16068,11 +16117,14 @@ async function runEcsTask(task, options, state) {
16068
16117
  for (const containerName of startOrder) {
16069
16118
  const container = task.containers.find((c) => c.name === containerName);
16070
16119
  await awaitDependencies(container, startedByName);
16071
- const args = dockerCmds.get(container.name);
16120
+ const { args, sensitiveEnv } = dockerCmds.get(container.name);
16072
16121
  logger.info(`Starting container '${container.name}' (image=${imagePlan.get(container.name)})`);
16073
16122
  let id;
16074
16123
  try {
16075
- const { stdout } = await execFileAsync$1(getDockerCmd(), args, { maxBuffer: 10 * 1024 * 1024 });
16124
+ const { stdout } = await execFileAsync$1(getDockerCmd(), args, {
16125
+ maxBuffer: 10 * 1024 * 1024,
16126
+ ...execEnvForSecrets(sensitiveEnv)
16127
+ });
16076
16128
  id = stdout.trim();
16077
16129
  } catch (err) {
16078
16130
  const e = err;
@@ -16426,7 +16478,9 @@ function buildDockerRunArgs(opts) {
16426
16478
  applyOverrideMap(finalEnv, overrides["Parameters"]);
16427
16479
  applyOverrideMap(finalEnv, overrides[container.name]);
16428
16480
  }
16429
- for (const [k, v] of Object.entries(finalEnv)) args.push("-e", `${k}=${v}`);
16481
+ const sensitiveEnvKeys = new Set(SENSITIVE_ENV_KEYS);
16482
+ for (const s of secrets) sensitiveEnvKeys.add(s.name);
16483
+ const sensitiveEnv = appendEnvFlags(args, finalEnv, sensitiveEnvKeys);
16430
16484
  if (container.user) args.push("--user", container.user);
16431
16485
  if (container.privileged) args.push("--privileged");
16432
16486
  if (container.readonlyRootFilesystem) args.push("--read-only");
@@ -16446,7 +16500,10 @@ function buildDockerRunArgs(opts) {
16446
16500
  entryPointTail = container.entryPoint.slice(1);
16447
16501
  }
16448
16502
  args.push(image, ...entryPointTail, ...container.command ?? []);
16449
- return args;
16503
+ return {
16504
+ args,
16505
+ sensitiveEnv
16506
+ };
16450
16507
  }
16451
16508
  function applyOverrideMap(acc, map) {
16452
16509
  if (!map) return;
@@ -18142,4 +18199,4 @@ function createLocalStartServiceCommand(opts = {}) {
18142
18199
 
18143
18200
  //#endregion
18144
18201
  export { LocalStateSourceError as a, rejectExplicitCfnStackWithMultipleStacks as c, CfnLocalStateProvider as d, createLocalInvokeCommand as i, resolveCfnRegion as l, createLocalRunTaskCommand as n, createLocalStateProvider as o, createLocalStartApiCommand as r, isCfnFlagPresent as s, createLocalStartServiceCommand as t, resolveCfnStackName as u };
18145
- //# sourceMappingURL=local-start-service-gYRarUgM.js.map
18202
+ //# sourceMappingURL=local-start-service-2tcrICJq.js.map