@go-to-k/cdkd 0.31.1 → 0.32.0

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
@@ -25,6 +25,7 @@
25
25
  - **S3-based state management**: No DynamoDB required, uses S3 conditional writes for locking
26
26
  - **DAG-based parallelization**: Analyze `Ref`/`Fn::GetAtt` dependencies and execute in parallel
27
27
  - **`--no-wait` for async resources**: Skip the multi-minute wait on CloudFront / RDS / ElastiCache / NAT Gateway and return as soon as the create call returns (CloudFormation always blocks)
28
+ - **`--aggressive-vpc-parallel`**: Drop CDK-injected defensive `DependsOn` edges from VPC Lambdas onto private-subnet routes so `CloudFront::Distribution` and `Lambda::Url` start their ~3-min propagation in parallel with NAT Gateway stabilization (~50% faster on VPC + Lambda + CloudFront stacks)
28
29
 
29
30
  > **Note**: Resource types not covered by either SDK Providers or Cloud Control API cannot be deployed with cdkd. If you encounter an unsupported resource type, deployment will fail with a clear error message.
30
31
 
@@ -405,6 +406,51 @@ ElastiCache) don't apply to destroy either — their providers are
405
406
  already non-blocking on delete because they're leaves in the destroy
406
407
  DAG.
407
408
 
409
+ ## `--aggressive-vpc-parallel`: relax CDK-defensive VPC route DependsOn
410
+
411
+ CDK synth eagerly injects `DependsOn` from VPC Lambdas (and adjacent
412
+ IAM Role / Policy / Lambda::Url / EventSourceMapping resources) onto
413
+ the private subnet's `DefaultRoute` / `RouteTableAssociation` so that
414
+ nothing tries to invoke the Lambda before its egress path to the
415
+ internet is up. The dependency is real at *runtime* (a Lambda code
416
+ call to a third-party API can't reach the internet without a NAT
417
+ route), but it is NOT required at *deploy time* — `CreateFunction` /
418
+ `CreateFunctionUrlConfig` / `AddPermission` /
419
+ `CreateEventSourceMapping` all accept a function in `Pending` state.
420
+
421
+ For VPC + Lambda + CloudFront stacks this turns into a serial
422
+ critical path:
423
+
424
+ ```text
425
+ NAT GW (~2-3 min) → DefaultRoute → Lambda → Lambda::Url → Distribution propagation (~3 min)
426
+ ```
427
+
428
+ Pass `--aggressive-vpc-parallel` to drop the route DependsOn so
429
+ Distribution + Lambda::Url dispatch right after IAM Role / Subnet are
430
+ ready and propagate in parallel with NAT stabilization:
431
+
432
+ ```bash
433
+ cdkd deploy --aggressive-vpc-parallel
434
+ ```
435
+
436
+ | Mode | Critical path | Total |
437
+ | --- | --- | --- |
438
+ | Default | NAT → Lambda → CF (serial) | ~6 min |
439
+ | `--aggressive-vpc-parallel` | max(NAT, CF) | ~3 min |
440
+
441
+ Measured **−45.6%** on `tests/integration/bench-cdk-sample` (387s
442
+ baseline → 211s relaxed).
443
+
444
+ Off by default for v1: opt-in is the conservative play because
445
+ CloudFront `Create` / `Delete` are each ~5 min, so a Lambda-side
446
+ async failure incurs a high rollback cost. Deploy-only —
447
+ `cdkd destroy` doesn't accept it (the route DependsOn doesn't
448
+ constrain delete-time correctness; Lambda hyperplane ENI release
449
+ is the actual destroy bottleneck).
450
+
451
+ See [docs/cli-reference.md](docs/cli-reference.md) for the full
452
+ type-pair allowlist, implementation pointers, and trade-off notes.
453
+
408
454
  ## Other CLI flags
409
455
 
410
456
  For concurrency knobs (`--concurrency`, `--stack-concurrency`,
package/dist/cli.js CHANGED
@@ -610,6 +610,10 @@ var noWaitOption = new Option(
610
610
  "--no-wait",
611
611
  "Skip waiting for async resources to stabilize (CloudFront, RDS, ElastiCache, NAT Gateway)"
612
612
  );
613
+ var aggressiveVpcParallelOption = new Option(
614
+ "--aggressive-vpc-parallel",
615
+ "Relax CDK-injected VPC route DependsOn to let CloudFront/Lambda::Url create in parallel with NAT GW stabilization"
616
+ ).default(false);
613
617
  var deployOptions = [
614
618
  new Option("--concurrency <number>", "Maximum concurrent resource operations").default(10).argParser((value) => parseInt(value, 10)),
615
619
  new Option("--stack-concurrency <number>", "Maximum concurrent stack deployments").default(4).argParser((value) => parseInt(value, 10)),
@@ -622,6 +626,7 @@ var deployOptions = [
622
626
  new Option("--skip-assets", "Skip asset publishing").default(false),
623
627
  new Option("--no-rollback", "Skip rollback on deployment failure"),
624
628
  noWaitOption,
629
+ aggressiveVpcParallelOption,
625
630
  new Option(
626
631
  "-e, --exclusively",
627
632
  "Only deploy requested stacks, do not include dependencies"
@@ -4699,6 +4704,58 @@ function collectRefIds(value, out) {
4699
4704
  }
4700
4705
  }
4701
4706
 
4707
+ // src/analyzer/cdk-defensive-deps.ts
4708
+ var DEFENSIVE_DEPENDS_ON_TYPE_PAIRS = [
4709
+ // VPC Lambda's execution Role (and its inline Policy) get DependsOn'd onto
4710
+ // the route only because CDK assumes the Lambda will run before the route
4711
+ // is up. The Role/Policy create call itself is VPC-agnostic.
4712
+ { fromType: "AWS::IAM::Role", toType: "AWS::EC2::Route" },
4713
+ { fromType: "AWS::IAM::Role", toType: "AWS::EC2::SubnetRouteTableAssociation" },
4714
+ { fromType: "AWS::IAM::Policy", toType: "AWS::EC2::Route" },
4715
+ { fromType: "AWS::IAM::Policy", toType: "AWS::EC2::SubnetRouteTableAssociation" },
4716
+ // VPC Lambda itself: CreateFunction returns synchronously while the
4717
+ // function is still in Pending; the route only matters once the function
4718
+ // is invoked at runtime.
4719
+ { fromType: "AWS::Lambda::Function", toType: "AWS::EC2::Route" },
4720
+ { fromType: "AWS::Lambda::Function", toType: "AWS::EC2::SubnetRouteTableAssociation" },
4721
+ // Lambda::Url is just a deterministic URL derivation off the function; it
4722
+ // doesn't need the function's runtime egress to exist.
4723
+ { fromType: "AWS::Lambda::Url", toType: "AWS::EC2::Route" },
4724
+ { fromType: "AWS::Lambda::Url", toType: "AWS::EC2::SubnetRouteTableAssociation" },
4725
+ // EventSourceMapping just registers the wire-up; AWS handles delivery
4726
+ // async and will retry once the function reaches Active.
4727
+ { fromType: "AWS::Lambda::EventSourceMapping", toType: "AWS::EC2::Route" },
4728
+ {
4729
+ fromType: "AWS::Lambda::EventSourceMapping",
4730
+ toType: "AWS::EC2::SubnetRouteTableAssociation"
4731
+ }
4732
+ ];
4733
+ function defensiveDependsOnToSkip(resource, template) {
4734
+ const skip = /* @__PURE__ */ new Set();
4735
+ if (!resource.DependsOn) {
4736
+ return skip;
4737
+ }
4738
+ const dependsOn = Array.isArray(resource.DependsOn) ? resource.DependsOn : [resource.DependsOn];
4739
+ for (const dep of dependsOn) {
4740
+ if (typeof dep !== "string")
4741
+ continue;
4742
+ const target = template.Resources[dep];
4743
+ if (!target)
4744
+ continue;
4745
+ const fromType = resource.Type;
4746
+ const toType = target.Type;
4747
+ if (!fromType || !toType)
4748
+ continue;
4749
+ const matched = DEFENSIVE_DEPENDS_ON_TYPE_PAIRS.some(
4750
+ (pair) => pair.fromType === fromType && pair.toType === toType
4751
+ );
4752
+ if (matched) {
4753
+ skip.add(dep);
4754
+ }
4755
+ }
4756
+ return skip;
4757
+ }
4758
+
4702
4759
  // src/analyzer/dag-builder.ts
4703
4760
  var { Graph, alg } = graphlib;
4704
4761
  var IAM_ROLE_POLICY_TYPES = /* @__PURE__ */ new Set([
@@ -4709,6 +4766,10 @@ var IAM_ROLE_POLICY_TYPES = /* @__PURE__ */ new Set([
4709
4766
  var DagBuilder = class {
4710
4767
  logger = getLogger().child("DagBuilder");
4711
4768
  parser = new TemplateParser();
4769
+ options;
4770
+ constructor(options = {}) {
4771
+ this.options = options;
4772
+ }
4712
4773
  /**
4713
4774
  * Build dependency graph from CloudFormation template
4714
4775
  *
@@ -4727,13 +4788,22 @@ var DagBuilder = class {
4727
4788
  });
4728
4789
  this.logger.debug(`Total nodes: ${resourceIds.length}`);
4729
4790
  let edgeCount = 0;
4791
+ let relaxedEdgeCount = 0;
4730
4792
  for (const logicalId of resourceIds) {
4731
4793
  const resource = this.parser.getResource(template, logicalId);
4732
4794
  if (!resource) {
4733
4795
  continue;
4734
4796
  }
4735
4797
  const dependencies = this.parser.extractDependencies(resource);
4798
+ const skip = this.options.relaxCdkVpcDefensiveDeps ? defensiveDependsOnToSkip(resource, template) : null;
4736
4799
  for (const depId of dependencies) {
4800
+ if (skip?.has(depId)) {
4801
+ relaxedEdgeCount++;
4802
+ this.logger.debug(
4803
+ `Skipped CDK-defensive DependsOn edge: ${depId} -> ${logicalId} (--aggressive-vpc-parallel)`
4804
+ );
4805
+ continue;
4806
+ }
4737
4807
  if (graph.hasNode(depId)) {
4738
4808
  graph.setEdge(depId, logicalId);
4739
4809
  edgeCount++;
@@ -4745,6 +4815,11 @@ var DagBuilder = class {
4745
4815
  }
4746
4816
  }
4747
4817
  }
4818
+ if (relaxedEdgeCount > 0) {
4819
+ this.logger.info(
4820
+ `[DagBuilder] Relaxed ${relaxedEdgeCount} CDK-defensive DependsOn edge(s) (--aggressive-vpc-parallel)`
4821
+ );
4822
+ }
4748
4823
  this.logger.debug(`Dependency graph built: ${resourceIds.length} nodes, ${edgeCount} edges`);
4749
4824
  edgeCount += this.addCustomResourcePolicyEdges(graph, template);
4750
4825
  edgeCount += this.addLambdaVpcEdges(graph, template);
@@ -13036,6 +13111,11 @@ import {
13036
13111
  DeleteNetworkInterfaceCommand
13037
13112
  } from "@aws-sdk/client-ec2";
13038
13113
  init_aws_clients();
13114
+ function inlineCodeFileNameForRuntime(runtime) {
13115
+ if (runtime?.startsWith("python"))
13116
+ return "index.py";
13117
+ return "index.js";
13118
+ }
13039
13119
  var LambdaFunctionProvider = class {
13040
13120
  lambdaClient;
13041
13121
  ec2Client;
@@ -13138,7 +13218,7 @@ var LambdaFunctionProvider = class {
13138
13218
  const createParams = {
13139
13219
  FunctionName: functionName,
13140
13220
  Role: role,
13141
- Code: this.buildCode(code),
13221
+ Code: this.buildCode(code, properties["Runtime"]),
13142
13222
  Handler: properties["Handler"],
13143
13223
  Runtime: properties["Runtime"],
13144
13224
  Timeout: properties["Timeout"],
@@ -13224,7 +13304,7 @@ var LambdaFunctionProvider = class {
13224
13304
  const newCode = properties["Code"];
13225
13305
  const oldCode = previousProperties["Code"];
13226
13306
  if (newCode && JSON.stringify(newCode) !== JSON.stringify(oldCode)) {
13227
- const builtCode = this.buildCode(newCode);
13307
+ const builtCode = this.buildCode(newCode, properties["Runtime"]);
13228
13308
  const codeParams = {
13229
13309
  FunctionName: physicalId,
13230
13310
  S3Bucket: builtCode.S3Bucket,
@@ -13592,7 +13672,7 @@ var LambdaFunctionProvider = class {
13592
13672
  /**
13593
13673
  * Build Lambda Code parameter from CDK properties
13594
13674
  */
13595
- buildCode(code) {
13675
+ buildCode(code, runtime) {
13596
13676
  const result = {};
13597
13677
  if (code["S3Bucket"]) {
13598
13678
  result.S3Bucket = code["S3Bucket"];
@@ -13604,7 +13684,7 @@ var LambdaFunctionProvider = class {
13604
13684
  result.S3ObjectVersion = code["S3ObjectVersion"];
13605
13685
  }
13606
13686
  if (code["ZipFile"]) {
13607
- result.ZipFile = this.createZipFromInlineCode(code["ZipFile"]);
13687
+ result.ZipFile = this.createZipFromInlineCode(code["ZipFile"], runtime);
13608
13688
  }
13609
13689
  if (code["ImageUri"]) {
13610
13690
  result.ImageUri = code["ImageUri"];
@@ -13616,13 +13696,14 @@ var LambdaFunctionProvider = class {
13616
13696
  *
13617
13697
  * CloudFormation's ZipFile property automatically wraps inline code in a zip,
13618
13698
  * but the Lambda SDK expects actual zip binary. This creates a minimal zip
13619
- * containing the code as index.* (matching the Handler).
13699
+ * containing the code as index.* (extension derived from runtime — nodejs
13700
+ * runtimes use index.js, python runtimes use index.py; see CFn ZipFile docs).
13620
13701
  */
13621
- createZipFromInlineCode(code) {
13702
+ createZipFromInlineCode(code, runtime) {
13622
13703
  const fileData = Buffer.from(code, "utf-8");
13623
13704
  const crc32 = this.crc32(fileData);
13624
13705
  const compressedData = zlib.deflateRawSync(fileData);
13625
- const fileName = Buffer.from("index.py");
13706
+ const fileName = Buffer.from(inlineCodeFileNameForRuntime(runtime));
13626
13707
  const now = /* @__PURE__ */ new Date();
13627
13708
  const modTime = (now.getHours() << 11 | now.getMinutes() << 5 | now.getSeconds() >> 1) & 65535;
13628
13709
  const modDate = (now.getFullYear() - 1980 << 9 | now.getMonth() + 1 << 5 | now.getDate()) & 65535;
@@ -32951,7 +33032,9 @@ async function deployCommand(stacks, options) {
32951
33032
  bucket: stateBucket,
32952
33033
  prefix: options.statePrefix
32953
33034
  };
32954
- const dagBuilder = new DagBuilder();
33035
+ const dagBuilder = new DagBuilder({
33036
+ relaxCdkVpcDefensiveDeps: !!options.aggressiveVpcParallel
33037
+ });
32955
33038
  const diffCalculator = new DiffCalculator();
32956
33039
  const baseRegion = options.region || process.env["AWS_REGION"] || "us-east-1";
32957
33040
  const switchRegion = (region2) => {
@@ -36353,7 +36436,7 @@ function reorderArgs(argv) {
36353
36436
  }
36354
36437
  async function main() {
36355
36438
  const program = new Command13();
36356
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.31.1");
36439
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.32.0");
36357
36440
  program.addCommand(createBootstrapCommand());
36358
36441
  program.addCommand(createSynthCommand());
36359
36442
  program.addCommand(createListCommand());