AWS CDKをDenoで動かす2  experimentalモジュール編


https://qiita.com/rinfield/items/49b89005678da188c214

の続きで、DenoでAWS CDKを動かしていきます。

## 要約
- CDKモジュールを複数esm.shからimportする場合constructsのバージョン合わせが必要

## experimentalモジュールを使う

AWS CDK v2はパッケージが1つにまとまりましたが、
依然experimentalモジュールは別モジュールとして配布されています。

experimentalモジュールとして一番使いそうな[Amazon Kinesis Data Firehoseモジュール](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-kinesisfirehose-alpha-readme.html)を使ってみます。


### 素朴なインポート
```ts
// app.ts
import cdk from "https://esm.sh/aws-cdk-lib@2.26.0";
import logs from "https://esm.sh/aws-cdk-lib@2.26.0/aws-logs";
import s3 from "https://esm.sh/aws-cdk-lib@2.26.0/aws-s3";
import firehose from "https://esm.sh/@aws-cdk/aws-kinesisfirehose-alpha@2.26.0-alpha.0";
import firehoseDestinations from "https://esm.sh/@aws-cdk/aws-kinesisfirehose-destinations-alpha@2.26.0-alpha.0";

const app = new cdk.App();
export const stack = new cdk.Stack(app, "MyStack");
new logs.LogGroup(stack, "MyLogGroup");

const firehoseDestBucket = new s3.Bucket(stack, "FirehoseDestBucket", {});
new firehose.DeliveryStream(stack, "FirehoseStream", {
  destinations: [
    new firehoseDestinations.S3Bucket(firehoseDestBucket),
  ],
});

app.synth();
```

前回のコードに追加してこんな感じですかね。
Firehose Streamと宛先のS3 Bucketを作ります。


```console
$ cdk synth
Check file:///workspaces/cdk-deno/app.ts
error: TS2345 [ERROR]: Argument of type 'Stack' is not assignable to parameter of type 'Construct'.
  Types of property 'node' are incompatible.
    Type 'import("https://esm.sh/v83/constructs@10.1.22/lib/construct.d.ts").Node' is not assignable to type 'import("https://esm.sh/v84/constructs@10.1.23/lib/construct.d.ts").Node'.
      Types have separate declarations of a private property 'host'.
const firehoseDestBucket = new s3.Bucket(stack, "FirehoseDestBucket", {});
                                         ~~~~~
    at file:///workspaces/cdk-deno/app.ts:11:42

TS2345 [ERROR]: Argument of type 'Stack' is not assignable to parameter of type 'Construct'.
new firehose.DeliveryStream(stack, "FirehoseStream", {
                            ~~~~~
    at file:///workspaces/cdk-deno/app.ts:12:29

Found 2 errors.
```

ありゃ、エラーになってしまいました。
どうもaws-cdkが依存する[constructsモジュール](https://www.npmjs.com/package/constructs)の型が合わないようです。

> (esm.shの推移的依存の解決とかどうなってるんだ…！？)

と前回思っていましたが、
初回リクエスト時の最新の依存で解決されてしまい、
推移的依存であるconstructsのバージョンがズレてしまっているようです。


### esm.shでの依存関係の指定
https://esm.sh/ のドキュメントを読むと
以下のように`deps`パラメータで依存の指定ができるようです。良く出来てる。

> ```ts
> import React from "https://esm.sh/react@16.14.0"
> import useSWR from "https://esm.sh/swr?deps=react@16.14.0"
> ```

### constructsのバージョンを指定したインポート

#### CDKコード
先程のソースのインポート文を修正します。

```ts
// app.ts
import cdk from "https://esm.sh/aws-cdk-lib@2.26.0?deps=constructs@10.1.23";
import logs from "https://esm.sh/aws-cdk-lib@2.26.0/aws-logs?deps=constructs@10.1.23";
import s3 from "https://esm.sh/aws-cdk-lib@2.26.0/aws-s3?deps=constructs@10.1.23";
import firehose from "https://esm.sh/@aws-cdk/aws-kinesisfirehose-alpha@2.26.0-alpha.0?deps=constructs@10.1.23";
import firehoseDestinations from "https://esm.sh/@aws-cdk/aws-kinesisfirehose-destinations-alpha@2.26.0-alpha.0?deps=constructs@10.1.23";

const app = new cdk.App();
export const stack = new cdk.Stack(app, "MyStack");
new logs.LogGroup(stack, "MyLogGroup");

const firehoseDestBucket = new s3.Bucket(stack, "FirehoseDestBucket", {});
new firehose.DeliveryStream(stack, "FirehoseStream", {
  destinations: [
    new firehoseDestinations.S3Bucket(firehoseDestBucket),
  ],
});

app.synth();
```

```
$ cdk synth
Check file:///workspaces/cdk-deno/app.ts
Resources:
  MyLogGroup5C0DAD85:
    Type: AWS::Logs::LogGroup
    Properties:
      RetentionInDays: 731
    UpdateReplacePolicy: Retain
    DeletionPolicy: Retain
    Metadata:
      aws:cdk:path: MyStack/MyLogGroup/Resource
  FirehoseDestBucket5161CA67:
    Type: AWS::S3::Bucket
<...略...>
```

動きました!

#### テストコード

テストコードも修正します
```ts
// app.test.ts
import { assertSnapshot } from "https://deno.land/std@0.141.0/testing/snapshot.ts";
import assertions from "https://esm.sh/aws-cdk-lib@2.26.0/assertions?deps=constructs@10.1.23";
import { stack } from "./app.ts";

Deno.test("snapshot", async (t) => {
  await assertSnapshot(t, assertions.Template.fromStack(stack).toJSON());
});
```

そしてテスト実行します。

```console
$ deno test --allow-all
running 1 test from ./app.test.ts
snapshot ... FAILED (18ms)
```

以下の差分があると失敗しました。
Stackのリソースを追加したのでこれは想定通りの失敗です。
素晴らしい。

<details>

```console
 ERRORS 

snapshot => ./app.test.ts:5:6
error: AssertionError: Snapshot does not match:


    [Diff] Actual / Expected


    {
-     Mappings: {
-       awscdkawskinesisfirehoseCidrBlocks: {
-         "af-south-1": {
-           FirehoseCidrBlock: "13.244.121.224/27",
-         },
-         "ap-east-1": {
-           FirehoseCidrBlock: "18.162.221.32/27",
-         },
-         "ap-northeast-1": {
-           FirehoseCidrBlock: "13.113.196.224/27",
-         },
-         "ap-northeast-2": {
-           FirehoseCidrBlock: "13.209.1.64/27",
-         },
-         "ap-northeast-3": {
-           FirehoseCidrBlock: "13.208.177.192/27",
-         },
-         "ap-south-1": {
-           FirehoseCidrBlock: "13.232.67.32/27",
-         },
-         "ap-southeast-1": {
-           FirehoseCidrBlock: "13.228.64.192/27",
-         },
-         "ap-southeast-2": {
-           FirehoseCidrBlock: "13.210.67.224/27",
-         },
-         "ca-central-1": {
-           FirehoseCidrBlock: "35.183.92.128/27",
-         },
-         "cn-north-1": {
-           FirehoseCidrBlock: "52.81.151.32/27",
-         },
-         "cn-northwest-1": {
-           FirehoseCidrBlock: "161.189.23.64/27",
-         },
-         "eu-central-1": {
-           FirehoseCidrBlock: "35.158.127.160/27",
-         },
-         "eu-north-1": {
-           FirehoseCidrBlock: "13.53.63.224/27",
-         },
-         "eu-south-1": {
-           FirehoseCidrBlock: "15.161.135.128/27",
-         },
-         "eu-west-1": {
-           FirehoseCidrBlock: "52.19.239.192/27",
-         },
-         "eu-west-2": {
-           FirehoseCidrBlock: "18.130.1.96/27",
-         },
-         "eu-west-3": {
-           FirehoseCidrBlock: "35.180.1.96/27",
-         },
-         "me-south-1": {
-           FirehoseCidrBlock: "15.185.91.0/27",
-         },
-         "sa-east-1": {
-           FirehoseCidrBlock: "18.228.1.128/27",
-         },
-         "us-east-1": {
-           FirehoseCidrBlock: "52.70.63.192/27",
-         },
-         "us-east-2": {
-           FirehoseCidrBlock: "13.58.135.96/27",
-         },
-         "us-gov-east-1": {
-           FirehoseCidrBlock: "18.253.138.96/27",
-         },
-         "us-gov-west-1": {
-           FirehoseCidrBlock: "52.61.204.160/27",
-         },
-         "us-west-1": {
-           FirehoseCidrBlock: "13.57.135.192/27",
-         },
-         "us-west-2": {
-           FirehoseCidrBlock: "52.89.255.224/27",
-         },
-       },
-     },
      Parameters: {
        BootstrapVersion: {
          Default: "/cdk-bootstrap/hnb659fds/version",
          Description: "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]",
          Type: "AWS::SSM::Parameter::Value<String>",
        },
      },
      Resources: {
-       FirehoseDestBucket5161CA67: {
-         DeletionPolicy: "Retain",
-         Type: "AWS::S3::Bucket",
-         UpdateReplacePolicy: "Retain",
-       },
-       FirehoseStream85DFF016: {
-         DependsOn: [
-           "FirehoseStreamS3DestinationRoleDefaultPolicyD12DE613",
-         ],
-         Properties: {
-           DeliveryStreamType: "DirectPut",
-           ExtendedS3DestinationConfiguration: {
-             BucketARN: {
-               "Fn::GetAtt": [
-                 "FirehoseDestBucket5161CA67",
-                 "Arn",
-               ],
-             },
-             CloudWatchLoggingOptions: {
-               Enabled: true,
-               LogGroupName: {
-                 Ref: "FirehoseStreamLogGroup9325AD6F",
-               },
-               LogStreamName: {
-                 Ref: "FirehoseStreamLogGroupS3Destination159E9820",
-               },
-             },
-             RoleARN: {
-               "Fn::GetAtt": [
-                 "FirehoseStreamS3DestinationRole5FDC3E58",
-                 "Arn",
-               ],
-             },
-           },
-         },
-         Type: "AWS::KinesisFirehose::DeliveryStream",
-       },
-       FirehoseStreamLogGroup9325AD6F: {
-         DeletionPolicy: "Retain",
-         Properties: {
-           RetentionInDays: 731,
-         },
-         Type: "AWS::Logs::LogGroup",
-         UpdateReplacePolicy: "Retain",
-       },
-       FirehoseStreamLogGroupS3Destination159E9820: {
-         DeletionPolicy: "Retain",
-         Properties: {
-           LogGroupName: {
-             Ref: "FirehoseStreamLogGroup9325AD6F",
-           },
-         },
-         Type: "AWS::Logs::LogStream",
-         UpdateReplacePolicy: "Retain",
-       },
-       FirehoseStreamS3DestinationRole5FDC3E58: {
-         Properties: {
-           AssumeRolePolicyDocument: {
-             Statement: [
-               {
-                 Action: "sts:AssumeRole",
-                 Effect: "Allow",
-                 Principal: {
-                   Service: "firehose.amazonaws.com",
-                 },
-               },
-             ],
-             Version: "2012-10-17",
-           },
-         },
-         Type: "AWS::IAM::Role",
-       },
-       FirehoseStreamS3DestinationRoleDefaultPolicyD12DE613: {
-         Properties: {
-           PolicyDocument: {
-             Statement: [
-               {
-                 Action: [
-                   "s3:GetObject*",
-                   "s3:GetBucket*",
-                   "s3:List*",
-                   "s3:DeleteObject*",
-                   "s3:PutObject",
-                   "s3:PutObjectLegalHold",
-                   "s3:PutObjectRetention",
-                   "s3:PutObjectTagging",
-                   "s3:PutObjectVersionTagging",
-                   "s3:Abort*",
-                 ],
-                 Effect: "Allow",
-                 Resource: [
-                   {
-                     "Fn::GetAtt": [
-                       "FirehoseDestBucket5161CA67",
-                       "Arn",
-                     ],
-                   },
-                   {
-                     "Fn::Join": [
-                       "",
-                       [
-                         {
-                           "Fn::GetAtt": [
-                             "FirehoseDestBucket5161CA67",
-                             "Arn",
-                           ],
-                         },
-                         "/*",
-                       ],
-                     ],
-                   },
-                 ],
-               },
-               {
-                 Action: [
-                   "logs:CreateLogStream",
-                   "logs:PutLogEvents",
-                 ],
-                 Effect: "Allow",
-                 Resource: {
-                   "Fn::GetAtt": [
-                     "FirehoseStreamLogGroup9325AD6F",
-                     "Arn",
-                   ],
-                 },
-               },
-             ],
-             Version: "2012-10-17",
-           },
-           PolicyName: "FirehoseStreamS3DestinationRoleDefaultPolicyD12DE613",
-           Roles: [
-             {
-               Ref: "FirehoseStreamS3DestinationRole5FDC3E58",
-             },
-           ],
-         },
-         Type: "AWS::IAM::Policy",
-       },
-       FirehoseStreamServiceRole98797615: {
-         Properties: {
-           AssumeRolePolicyDocument: {
-             Statement: [
-               {
-                 Action: "sts:AssumeRole",
-                 Effect: "Allow",
-                 Principal: {
-                   Service: "firehose.amazonaws.com",
-                 },
-               },
-             ],
-             Version: "2012-10-17",
-           },
-         },
-         Type: "AWS::IAM::Role",
-       },
        MyLogGroup5C0DAD85: {
          DeletionPolicy: "Retain",
          Properties: {
            RetentionInDays: 731,
          },
          Type: "AWS::Logs::LogGroup",
          UpdateReplacePolicy: "Retain",
        },
      },
      Rules: {
        CheckBootstrapVersion: {
          Assertions: [
            {
              Assert: {
                "Fn::Not": [
                  {
                    "Fn::Contains": [
                      [
                        "1",
                        "2",
                        "3",
                        "4",
                        "5",
                      ],
                      {
                        Ref: "BootstrapVersion",
                      },
                    ],
                  },
                ],
              },
              AssertDescription: "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI.",
            },
          ],
        },
      },
    }

    throw new AssertionError(
          ^
    at assertSnapshot (https://deno.land/std@0.141.0/testing/snapshot.ts:407:11)
    at async file:///workspaces/cdk-deno/app.test.ts:6:3

 FAILURES 

snapshot => ./app.test.ts:5:6

test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out (2s)

error: Test failed
```

</details>

スナップショットを更新するオプション(`-- --update`)をつけて再実行します。

```console
$ deno test --allow-all -- --update
running 1 test from ./app.test.ts
snapshot ... ok (14ms)

------- output -------

 > 1 snapshots updated.

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (2s)
```

これでスナップショットが更新されました。

## インポートの整理

動くようにはなりましたがこれだとあちこちにCDKのバージョンや
constructsのバージョンが散らばって保守性が低い上に
インポート行が膨れて非常にノイズが大きいので非常に気持ち悪いです。

[Denoのドキュメント](https://deno.land/manual/examples/manage_dependencies)によるとこういう場合は
`deps.ts`というファイルでまとめて再エクスポートするのがお決まりのようです。

```ts
// deps.ts
import awsCdkLib from "https://esm.sh/aws-cdk-lib@2.26.0?deps=constructs@10.1.23";
export const cdk = awsCdkLib;
export const { aws_s3: s3, aws_logs: logs } = awsCdkLib;

export { default as firehose } from "https://esm.sh/@aws-cdk/aws-kinesisfirehose-alpha@2.26.0-alpha.0?deps=constructs@10.1.23";
export { default as firehoseTo } from "https://esm.sh/@aws-cdk/aws-kinesisfirehose-destinations-alpha@2.26.0-alpha.0?deps=constructs@10.1.23";

// test
export { assertSnapshot } from "https://deno.land/std@0.141.0/testing/snapshot.ts";
export const { Template } = awsCdkLib.assertions;
```

とりあえずこんな感じですかね。
CDKとconstructsのバージョンを文字列定数に切り出したかったのですが
`from`は文字列リテラルしか受け付けずエラーとなってしまいました。
まぁファイルが1つにまとまっただけマシか…

他に良い書き方があれば教えて下さい…


### 呼び出し元
```ts
// app.ts
import { cdk, firehose, firehoseTo, logs, s3 } from "./deps.ts";

const app = new cdk.App();
export const stack = new cdk.Stack(app, "MyStack");
new logs.LogGroup(stack, "MyLogGroup");

const firehoseDestBucket = new s3.Bucket(stack, "FirehoseDestBucket", {});
new firehose.DeliveryStream(stack, "FirehoseStream", {
  destinations: [
    new firehoseTo.S3Bucket(firehoseDestBucket),
  ],
});

app.synth();
```

```ts
// app.test.ts
import { assertSnapshot, Template } from "./deps.ts";
import { stack } from "./app.ts";

Deno.test("snapshot", async (t) => {
  await assertSnapshot(t, Template.fromStack(stack).toJSON());
});
```

利用側は非常にスッキリしましたね！これなら満足です:relaxed:

全体のソースはこちら

https://github.com/rinfield/cdk-deno/tree/041d855d23538e0e3eb403dae8f9a4495f9985ce


## 今回の感想
今のところいい感じに動いてる。CDK + Denoマジでなかなかいいんじゃないの？

## 続き
https://qiita.com/rinfield/items/284fae0e1e55b28789e0