@go-to-k/cdkd 0.31.2 → 0.33.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
+ - **VPC route DependsOn relaxation (on by default)**: 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). Pass `--no-aggressive-vpc-parallel` to opt out.
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
+ ## VPC route DependsOn relaxation (on by default)
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 the strict-CDK-ordering chain is serial:
422
+
423
+ ```text
424
+ NAT GW (~2-3 min) → DefaultRoute → Lambda → Lambda::Url → Distribution propagation (~3 min)
425
+ ```
426
+
427
+ cdkd drops the route DependsOn by default so Distribution + Lambda::Url
428
+ dispatch right after IAM Role / Subnet are ready and propagate in
429
+ parallel with NAT stabilization:
430
+
431
+ | Mode | Critical path | Total |
432
+ | --- | --- | --- |
433
+ | `--no-aggressive-vpc-parallel` (opt-out) | NAT → Lambda → CF (serial) | ~6 min |
434
+ | **default** | max(NAT, CF) | **~3 min** |
435
+
436
+ Measured **−54.6%** on `tests/integration/bench-cdk-sample` (398.59s
437
+ with `--no-aggressive-vpc-parallel` → 181.03s default).
438
+
439
+ To opt out (e.g. for a stack with a Custom Resource that synchronously
440
+ invokes a VPC Lambda outside cdkd's Lambda-ServiceToken Active wait):
441
+
442
+ ```bash
443
+ cdkd deploy --no-aggressive-vpc-parallel
444
+ ```
445
+
446
+ Deploy-only — the relaxation has no effect on destroy ordering (the
447
+ route DependsOn doesn't constrain delete-time correctness; Lambda
448
+ hyperplane ENI release is the actual destroy bottleneck and is handled
449
+ separately by `lambda-vpc-deps.ts`).
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
+ "--no-aggressive-vpc-parallel",
615
+ "Disable the default relaxation of CDK-injected VPC route DependsOn (on by default; opt out to keep the strict CDK ordering)"
616
+ );
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} (default; opt out with --no-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) (default; opt out with --no-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);
@@ -32957,7 +33032,9 @@ async function deployCommand(stacks, options) {
32957
33032
  bucket: stateBucket,
32958
33033
  prefix: options.statePrefix
32959
33034
  };
32960
- const dagBuilder = new DagBuilder();
33035
+ const dagBuilder = new DagBuilder({
33036
+ relaxCdkVpcDefensiveDeps: !!options.aggressiveVpcParallel
33037
+ });
32961
33038
  const diffCalculator = new DiffCalculator();
32962
33039
  const baseRegion = options.region || process.env["AWS_REGION"] || "us-east-1";
32963
33040
  const switchRegion = (region2) => {
@@ -36359,7 +36436,7 @@ function reorderArgs(argv) {
36359
36436
  }
36360
36437
  async function main() {
36361
36438
  const program = new Command13();
36362
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.31.2");
36439
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.33.0");
36363
36440
  program.addCommand(createBootstrapCommand());
36364
36441
  program.addCommand(createSynthCommand());
36365
36442
  program.addCommand(createListCommand());