@go-to-k/cdkd 0.30.2 → 0.31.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
@@ -24,7 +24,7 @@
24
24
  - **Diff calculation**: Self-implemented resource/property-level diff between desired template and current state
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
- - **`--no-wait` for async resources**: Skip the multi-minute wait on CloudFront / RDS / ElastiCache and return as soon as the create call returns (CloudFormation always blocks)
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
28
 
29
29
  > **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
30
 
@@ -372,11 +372,11 @@ cdkd state destroy MyStack --region us-east-1
372
372
 
373
373
  ## `--no-wait`: skip async-resource waits
374
374
 
375
- CloudFront Distributions, RDS Clusters/Instances, and ElastiCache
376
- typically take 3–15 minutes for AWS to fully propagate. By default
377
- cdkd waits for them to reach a ready state — the same behavior as
378
- CloudFormation. Pass `--no-wait` to return as soon as the create call
379
- returns:
375
+ CloudFront Distributions, RDS Clusters/Instances, ElastiCache, and
376
+ NAT Gateways typically take 1–15 minutes for AWS to fully provision.
377
+ By default cdkd waits for them to reach a ready state — the same
378
+ behavior as CloudFormation. Pass `--no-wait` to return as soon as the
379
+ create call returns:
380
380
 
381
381
  ```bash
382
382
  cdkd deploy --no-wait
@@ -386,6 +386,25 @@ The resource is fully functional once AWS finishes the async
386
386
  deployment in the background. CloudFormation has no equivalent — once
387
387
  you submit a stack, you wait for everything.
388
388
 
389
+ NAT Gateway is included as of v0.31. Provisioning typically takes
390
+ 1–2 minutes and is the dominant cost in many VPC stacks; with
391
+ `cdkd deploy --no-wait`, `CreateNatGateway` returns the `NatGatewayId`
392
+ immediately and dependent Routes that only reference the ID can
393
+ proceed against a still-`pending` gateway. AWS continues NAT
394
+ provisioning asynchronously after the deploy returns. Use this only
395
+ when nothing in the deploy flow needs actual NAT-routed egress (e.g.
396
+ no Lambda invoked during deploy that hits the internet).
397
+
398
+ `--no-wait` is **deploy-only**. `cdkd destroy` always waits for NAT
399
+ Gateway to reach `deleted` state — while the gateway is in
400
+ `deleting` AWS keeps the ENI / EIP / route-table associations
401
+ attached, so any concurrent `DeleteSubnet` / `DeleteInternetGateway`
402
+ / `DeleteVpc` returns `DependencyViolation` and the destroy enters a
403
+ retry storm. Other `--no-wait` resources (CloudFront / RDS /
404
+ ElastiCache) don't apply to destroy either — their providers are
405
+ already non-blocking on delete because they're leaves in the destroy
406
+ DAG.
407
+
389
408
  ## Other CLI flags
390
409
 
391
410
  For concurrency knobs (`--concurrency`, `--stack-concurrency`,
package/dist/cli.js CHANGED
@@ -606,6 +606,10 @@ function validateResourceTimeouts(opts) {
606
606
  }
607
607
  }
608
608
  }
609
+ var noWaitOption = new Option(
610
+ "--no-wait",
611
+ "Skip waiting for async resources to stabilize (CloudFront, RDS, ElastiCache, NAT Gateway)"
612
+ );
609
613
  var deployOptions = [
610
614
  new Option("--concurrency <number>", "Maximum concurrent resource operations").default(10).argParser((value) => parseInt(value, 10)),
611
615
  new Option("--stack-concurrency <number>", "Maximum concurrent stack deployments").default(4).argParser((value) => parseInt(value, 10)),
@@ -617,7 +621,7 @@ var deployOptions = [
617
621
  new Option("--dry-run", "Show changes without applying").default(false),
618
622
  new Option("--skip-assets", "Skip asset publishing").default(false),
619
623
  new Option("--no-rollback", "Skip rollback on deployment failure"),
620
- new Option("--no-wait", "Skip waiting for async resources (CloudFront, RDS, etc.)"),
624
+ noWaitOption,
621
625
  new Option(
622
626
  "-e, --exclusively",
623
627
  "Only deploy requested stacks, do not include dependencies"
@@ -16790,6 +16794,11 @@ import {
16790
16794
  DeleteInternetGatewayCommand,
16791
16795
  AttachInternetGatewayCommand,
16792
16796
  DetachInternetGatewayCommand,
16797
+ CreateNatGatewayCommand,
16798
+ DeleteNatGatewayCommand,
16799
+ DescribeNatGatewaysCommand,
16800
+ waitUntilNatGatewayAvailable,
16801
+ waitUntilNatGatewayDeleted,
16793
16802
  CreateRouteTableCommand,
16794
16803
  DeleteRouteTableCommand,
16795
16804
  CreateRouteCommand,
@@ -16835,6 +16844,20 @@ var EC2Provider = class {
16835
16844
  ],
16836
16845
  ["AWS::EC2::InternetGateway", /* @__PURE__ */ new Set(["Tags"])],
16837
16846
  ["AWS::EC2::VPCGatewayAttachment", /* @__PURE__ */ new Set(["VpcId", "InternetGatewayId"])],
16847
+ [
16848
+ "AWS::EC2::NatGateway",
16849
+ /* @__PURE__ */ new Set([
16850
+ "AllocationId",
16851
+ "SubnetId",
16852
+ "ConnectivityType",
16853
+ "PrivateIpAddress",
16854
+ "SecondaryAllocationIds",
16855
+ "SecondaryPrivateIpAddresses",
16856
+ "SecondaryPrivateIpAddressCount",
16857
+ "MaxDrainDurationSeconds",
16858
+ "Tags"
16859
+ ])
16860
+ ],
16838
16861
  ["AWS::EC2::RouteTable", /* @__PURE__ */ new Set(["VpcId", "Tags"])],
16839
16862
  [
16840
16863
  "AWS::EC2::Route",
@@ -16922,6 +16945,8 @@ var EC2Provider = class {
16922
16945
  return this.createInternetGateway(logicalId, resourceType, properties);
16923
16946
  case "AWS::EC2::VPCGatewayAttachment":
16924
16947
  return this.createVpcGatewayAttachment(logicalId, resourceType, properties);
16948
+ case "AWS::EC2::NatGateway":
16949
+ return this.createNatGateway(logicalId, resourceType, properties);
16925
16950
  case "AWS::EC2::RouteTable":
16926
16951
  return this.createRouteTable(logicalId, resourceType, properties);
16927
16952
  case "AWS::EC2::Route":
@@ -16958,6 +16983,8 @@ var EC2Provider = class {
16958
16983
  return this.updateInternetGateway(logicalId, physicalId);
16959
16984
  case "AWS::EC2::VPCGatewayAttachment":
16960
16985
  return this.updateVpcGatewayAttachment(logicalId, physicalId);
16986
+ case "AWS::EC2::NatGateway":
16987
+ return this.updateNatGateway(logicalId, physicalId);
16961
16988
  case "AWS::EC2::RouteTable":
16962
16989
  return this.updateRouteTable(logicalId, physicalId);
16963
16990
  case "AWS::EC2::Route":
@@ -17011,6 +17038,8 @@ var EC2Provider = class {
17011
17038
  return this.deleteInternetGateway(logicalId, physicalId, resourceType, context);
17012
17039
  case "AWS::EC2::VPCGatewayAttachment":
17013
17040
  return this.deleteVpcGatewayAttachment(logicalId, physicalId, resourceType, context);
17041
+ case "AWS::EC2::NatGateway":
17042
+ return this.deleteNatGateway(logicalId, physicalId, resourceType, context);
17014
17043
  case "AWS::EC2::RouteTable":
17015
17044
  return this.deleteRouteTable(logicalId, physicalId, resourceType, context);
17016
17045
  case "AWS::EC2::Route":
@@ -17579,6 +17608,118 @@ var EC2Provider = class {
17579
17608
  );
17580
17609
  }
17581
17610
  }
17611
+ // ─── AWS::EC2::NatGateway ─────────────────────────────────────────
17612
+ //
17613
+ // CloudFormation parity: by default we wait for the new NAT gateway to
17614
+ // reach `available` state before marking the resource created. NAT
17615
+ // provisioning takes ~1–2 minutes (often the longest single step in a
17616
+ // VPC stack). Pass `--no-wait` to skip the wait — `CreateNatGateway`
17617
+ // returns the `NatGatewayId` immediately so dependent Routes /
17618
+ // Subnets that only need the ID can proceed against a still-`pending`
17619
+ // gateway. Anything that requires actual NAT-routed egress (e.g. a
17620
+ // Lambda invocation that hits the internet during deploy) must not
17621
+ // rely on the gateway being live; with `--no-wait`, AWS continues
17622
+ // provisioning asynchronously after the deploy returns.
17623
+ async createNatGateway(logicalId, resourceType, properties) {
17624
+ this.logger.debug(`Creating NatGateway ${logicalId}`);
17625
+ const subnetId = properties["SubnetId"];
17626
+ if (!subnetId) {
17627
+ throw new ProvisioningError(
17628
+ `SubnetId is required for NatGateway ${logicalId}`,
17629
+ resourceType,
17630
+ logicalId
17631
+ );
17632
+ }
17633
+ try {
17634
+ const response = await this.ec2Client.send(
17635
+ new CreateNatGatewayCommand({
17636
+ SubnetId: subnetId,
17637
+ AllocationId: properties["AllocationId"],
17638
+ ConnectivityType: properties["ConnectivityType"] ?? void 0,
17639
+ PrivateIpAddress: properties["PrivateIpAddress"],
17640
+ SecondaryAllocationIds: properties["SecondaryAllocationIds"],
17641
+ SecondaryPrivateIpAddresses: properties["SecondaryPrivateIpAddresses"],
17642
+ SecondaryPrivateIpAddressCount: properties["SecondaryPrivateIpAddressCount"]
17643
+ })
17644
+ );
17645
+ const natGatewayId = response.NatGateway.NatGatewayId;
17646
+ await this.applyTags(natGatewayId, properties, logicalId);
17647
+ if (process.env["CDKD_NO_WAIT"] !== "true") {
17648
+ this.logger.debug(`Waiting for NatGateway ${natGatewayId} to reach available state...`);
17649
+ await waitUntilNatGatewayAvailable(
17650
+ // 15-min cap matches AWS's documented worst case for NAT
17651
+ // provisioning. Per-resource `--resource-timeout` (default
17652
+ // 30 min) still bounds the outer call as a backstop.
17653
+ { client: this.ec2Client, maxWaitTime: 15 * 60 },
17654
+ { NatGatewayIds: [natGatewayId] }
17655
+ );
17656
+ this.logger.debug(`NatGateway ${natGatewayId} is available`);
17657
+ } else {
17658
+ this.logger.debug(
17659
+ `NatGateway ${natGatewayId} created (skipping available-state wait per --no-wait)`
17660
+ );
17661
+ }
17662
+ this.logger.debug(`Successfully created NatGateway ${logicalId}: ${natGatewayId}`);
17663
+ return {
17664
+ physicalId: natGatewayId,
17665
+ attributes: {
17666
+ NatGatewayId: natGatewayId
17667
+ }
17668
+ };
17669
+ } catch (error) {
17670
+ const cause = error instanceof Error ? error : void 0;
17671
+ throw new ProvisioningError(
17672
+ `Failed to create NatGateway ${logicalId}: ${error instanceof Error ? error.message : String(error)}`,
17673
+ resourceType,
17674
+ logicalId,
17675
+ void 0,
17676
+ cause
17677
+ );
17678
+ }
17679
+ }
17680
+ updateNatGateway(logicalId, physicalId) {
17681
+ this.logger.debug(`Updating NatGateway ${logicalId}: ${physicalId} (no-op)`);
17682
+ return Promise.resolve({ physicalId, wasReplaced: false });
17683
+ }
17684
+ async deleteNatGateway(logicalId, physicalId, resourceType, context) {
17685
+ this.logger.debug(`Deleting NatGateway ${logicalId}: ${physicalId}`);
17686
+ try {
17687
+ await this.ec2Client.send(new DeleteNatGatewayCommand({ NatGatewayId: physicalId }));
17688
+ } catch (error) {
17689
+ if (this.isNotFoundError(error)) {
17690
+ const clientRegion = await this.ec2Client.config.region();
17691
+ assertRegionMatch(
17692
+ clientRegion,
17693
+ context?.expectedRegion,
17694
+ resourceType,
17695
+ logicalId,
17696
+ physicalId
17697
+ );
17698
+ this.logger.debug(`NatGateway ${physicalId} does not exist, skipping deletion`);
17699
+ return;
17700
+ }
17701
+ const cause = error instanceof Error ? error : void 0;
17702
+ throw new ProvisioningError(
17703
+ `Failed to delete NatGateway ${logicalId}: ${error instanceof Error ? error.message : String(error)}`,
17704
+ resourceType,
17705
+ logicalId,
17706
+ physicalId,
17707
+ cause
17708
+ );
17709
+ }
17710
+ this.logger.debug(`Waiting for NatGateway ${physicalId} to reach deleted state...`);
17711
+ try {
17712
+ await waitUntilNatGatewayDeleted(
17713
+ { client: this.ec2Client, maxWaitTime: 15 * 60 },
17714
+ { NatGatewayIds: [physicalId] }
17715
+ );
17716
+ } catch (error) {
17717
+ this.logger.warn(
17718
+ `Wait for NatGateway ${physicalId} deletion did not complete cleanly: ${error instanceof Error ? error.message : String(error)} \u2014 proceeding with downstream delete steps`
17719
+ );
17720
+ }
17721
+ this.logger.debug(`Successfully deleted NatGateway ${logicalId}`);
17722
+ }
17582
17723
  // ─── AWS::EC2::RouteTable ─────────────────────────────────────────
17583
17724
  async createRouteTable(logicalId, resourceType, properties) {
17584
17725
  this.logger.debug(`Creating RouteTable ${logicalId}`);
@@ -18788,6 +18929,15 @@ var EC2Provider = class {
18788
18929
  const sg = resp.SecurityGroups?.[0];
18789
18930
  return sg?.GroupId ? { physicalId: sg.GroupId, attributes: {} } : null;
18790
18931
  }
18932
+ case "AWS::EC2::NatGateway": {
18933
+ const resp = await this.ec2Client.send(
18934
+ new DescribeNatGatewaysCommand({
18935
+ Filter: [{ Name: `tag:${CDK_PATH_TAG}`, Values: [input.cdkPath] }]
18936
+ })
18937
+ );
18938
+ const gw = resp.NatGateways?.find((g) => g.State !== "deleted" && g.State !== "deleting");
18939
+ return gw?.NatGatewayId ? { physicalId: gw.NatGatewayId, attributes: {} } : null;
18940
+ }
18791
18941
  default:
18792
18942
  return null;
18793
18943
  }
@@ -18816,6 +18966,13 @@ var EC2Provider = class {
18816
18966
  );
18817
18967
  return resp.SecurityGroups?.[0] ? { physicalId, attributes: {} } : null;
18818
18968
  }
18969
+ case "AWS::EC2::NatGateway": {
18970
+ const resp = await this.ec2Client.send(
18971
+ new DescribeNatGatewaysCommand({ NatGatewayIds: [physicalId] })
18972
+ );
18973
+ const gw = resp.NatGateways?.find((g) => g.State !== "deleted" && g.State !== "deleting");
18974
+ return gw ? { physicalId, attributes: {} } : null;
18975
+ }
18819
18976
  default:
18820
18977
  return null;
18821
18978
  }
@@ -31139,6 +31296,7 @@ function registerAllProviders(registry) {
31139
31296
  registry.register("AWS::EC2::Subnet", ec2Provider);
31140
31297
  registry.register("AWS::EC2::InternetGateway", ec2Provider);
31141
31298
  registry.register("AWS::EC2::VPCGatewayAttachment", ec2Provider);
31299
+ registry.register("AWS::EC2::NatGateway", ec2Provider);
31142
31300
  registry.register("AWS::EC2::RouteTable", ec2Provider);
31143
31301
  registry.register("AWS::EC2::Route", ec2Provider);
31144
31302
  registry.register("AWS::EC2::SubnetRouteTableAssociation", ec2Provider);
@@ -36172,7 +36330,7 @@ function reorderArgs(argv) {
36172
36330
  }
36173
36331
  async function main() {
36174
36332
  const program = new Command13();
36175
- program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.30.2");
36333
+ program.name("cdkd").description("CDK Direct - Deploy AWS CDK apps directly via SDK/Cloud Control API").version("0.31.0");
36176
36334
  program.addCommand(createBootstrapCommand());
36177
36335
  program.addCommand(createSynthCommand());
36178
36336
  program.addCommand(createListCommand());