@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 +46 -0
- package/dist/cli.js +92 -9
- package/dist/cli.js.map +3 -3
- package/dist/go-to-k-cdkd-0.32.0.tgz +0 -0
- package/dist/index.js +70 -0
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
- package/dist/go-to-k-cdkd-0.31.1.tgz +0 -0
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.* (
|
|
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(
|
|
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.
|
|
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());
|