AWS CDKをDenoで動かす

## 要約
- シンプルなケースではDenoで`cdk synth`動いた
- コード: https://github.com/rinfield/cdk-deno

## CDK + Nodeのビミョーなところ
[AWS CDK](https://github.com/aws/aws-cdk)いいですよね:relaxed:
今となっては素のCloudFormationテンプレートのYAML手書きは考えられません！

しかしながらCDKの登場によって格段にインフラのコード化がしやすくなった反面、
現状Nodeエコシステムのカオスさ(？)を受け入れる必要があります…
(Node + TypeScriptを使うことを前提としていますが)

ということでCDKを使う場合、TypeScriptトランスパイラ、
Linter、フォーマッター、テストフレームワークetc...
のメンテナンスを定期的に行う必要があります。

YAML手書きよりはマシなものの、これはこれで面倒くさい…


## CDKがDenoで使えたならば…
そんな中CDKのGitHub　Issueを眺めていたら以下のIssueがありました。

https://github.com/aws/aws-cdk/issues/17386

なるほど:bulb:詳細な説明は他の記事に譲りますが、
DenoはNodeと違いTypeScriptトランスパイラ、Linter、
フォーマッター、テストフレームワークなどの機能が同梱されています。
DenoでCDKが動くなら面倒くさいメンテナンス作業を減らすことができそうです。

ただ、肝心のCDK開発チームの反応は

> ts-nodeつかえばいいじゃん(意訳)

> 他の言語はもっと:+1:ついてるし優先度下げるわ。
> 今言語の追加は検討していないし、追加したとしてもこれ選ぶかわからんよ。(意訳)

と、冷めた反応で当面Denoの公式サポートはなさそうです…
Denoサポートは「言語」というよりは「ランタイム」という気もしますし、
もうちょっと考えてくれてもいいんじゃないかな…

とはいえ、動いたらなかなか魅力的なので
現時点(2022/05)のDenoでCDKが動かせるか試してみたいと思います。


## まずはCDK + Node
[このシンプルなCDKコード(GitHub)](https://github.com/rinfield/cdk-deno/tree/92c25eded498c377ee6db24b8c36f6d32df5b1c5)をDenoに移植してみます。

### コード
#### `app.ts`
```ts
import * as cdk from "aws-cdk-lib";
import * as logs from "aws-cdk-lib/aws-logs";

const app = new cdk.App();
export const stack = new cdk.Stack(app, "MyStack");
new logs.LogGroup(stack, "MyLogGroup");
```
LogGroupを一つだけ作るスタックです。

#### `app.test.ts`
```ts
import { stack } from "./app";
import { Template } from "aws-cdk-lib/assertions";

test("snapshot", () => {
  expect(Template.fromStack(stack).toJSON()).toMatchSnapshot();
});
```
テンプレートをスナップショットテストします。
スナップショットテスト大好きっ子なのでこの機能は外せません！


### コマンド
#### cdk synth
```console
$ cdk synth
Resources:
  MyLogGroup5C0DAD85:
    Type: AWS::Logs::LogGroup
    Properties:
<...略...>
```

#### ユニットテスト
```console
$ npm test

> new-cdk-proj@0.1.0 test
> jest

 PASS  ./app.test.ts (6.809 s)
  ✓ snapshot (32 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   1 passed, 1 total
Time:        6.893 s
Ran all test suites.
```

動いていますね。これをDenoで動かせないか試します。


## CDK + Denoにしてみる

(Deno歴１日の初心者のため不正確な情報はご容赦ください)

[ローカルの`node_module`を読み込む方法](https://deno.land/manual@v1.12.2/npm_nodejs/std_node#loading-commonjs-modules)は`any`型になってしまうようで、
これでは使い物にならないので動いたはずですが却下。

[esm.shサービスを使った方法](https://deno.land/manual@v1.12.2/npm_nodejs/cdns#esmsh)で進めます。

### コード
[変更後の全体のコードはこちら(GitHub)](https://github.com/rinfield/cdk-deno)

#### `app.ts`
```diff
--- a/app.ts
+++ b/app.ts
@@ -1,6 +1,8 @@
-import * as cdk from "aws-cdk-lib";
-import * as logs from "aws-cdk-lib/aws-logs";
+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";
 
 const app = new cdk.App();
 export const stack = new cdk.Stack(app, "MyStack");
 new logs.LogGroup(stack, "MyLogGroup");
+
+app.synth();
```

`aws-cdk-lib`をesm.shからのインポートに変更します。
型情報もちゃんとついたままなのでIDEでの補完も効きますね。

Nodeの場合明示的な`app.synth()`は任意だったと思いますが(？)、
Denoにした場合これがないと`cdk.out`にファイルが出力されませんでした…謎…


#### `app.test.ts`
```diff
--- a/app.test.ts
+++ b/app.test.ts
@@ -1,6 +1,7 @@
-import { stack } from "./app";
-import { Template } from "aws-cdk-lib/assertions";
+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";
+import { stack } from "./app.ts";
 
-test("snapshot", () => {
-  expect(Template.fromStack(stack).toJSON()).toMatchSnapshot();
-});
+Deno.test("snapshot", async (t) => {
+  await assertSnapshot(t, assertions.Template.fromStack(stack).toJSON());
+});
```

テストはフレームワークが違うのでインポート文だけとはいきませんがしょうがないですね。
Denoには標準でテストフレームワークもスナップショットテストもあるなんて素晴らしいですね！

#### `cdk.json`
```diff
--- a/cdk.json
+++ b/cdk.json
@@ -1,5 +1,5 @@
 {
-  "app": "npx ts-node --prefer-ts-exts app.ts",
+  "app": "deno run --allow-all app.ts",
```

CDK CLIが実行するコマンドをDenoに変更します。
CDKはファイルシステムや環境変数にアクセスするのでDenoでは明示的な許可が必要です。
面倒くさいのですべてを許可する`--allow-all`を指定します。

### コマンド
念の為`node_modules`と`cdk.out`を削除しておきます。
また、スナップショットのフォーマットがJestと異なる反面ファイルパスが一緒で実行時にエラーになってしまうので、
一旦古いJestのスナップショットを削除してから再生成しました。

#### cdk synth
```console
$ cdk synth
Resources:
  MyLogGroup5C0DAD85:
    Type: AWS::Logs::LogGroup
    Properties:
<...略...>
```

動きました！:tada:

#### ユニットテスト
```console
$ deno test --allow-all
running 1 test from ./app.test.ts
snapshot ... ok (11ms)

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

リソース論理名を変えて失敗させてみます。

<details><summary>クリックして展開</summary>

```console
$ deno test --allow-all
Check file:///workspaces/cdk-deno/app.test.ts
running 1 test from ./app.test.ts
snapshot ... FAILED (13ms)

 ERRORS 

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


    [Diff] Actual / Expected


    {
      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: {
-       MyLogGroupXCCBD7FC5: {
+       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 (1s)

error: Test failed
```

</details>

ちゃんと失敗しました！

## 感想
とりあえずCDK synthとテストは動きました:tada:
CDKのコードはインポート文の変更だけですんなり動いて驚きです！
(esm.shの推移的依存の解決とかどうなってるんだ…！？)
とはいえ正式サポートされていないのでいつ壊れても泣かない覚悟が必要ですね…

これでTypeScriptトランスパイラ、
パッケージマネージャの巨大なロックファイル&`node_modules`、
テストフレームワークのメンテを個別にしなくて良くなりました！

かなり環境として見込みがあると感じています:sparkles:
今後CDKチームはDenoサポート本気出してほしいところです…

あとNodeのままでオールインワンツールの[Rome](https://rome.tools/)も見込みあるから頑張れ！

## 試してみたいこと
- synth以外はちゃんと動いてんのか？(`cdk.out`が出てたらあとはCLIの範囲っぽいし動くか)
  - ⇒ [その3](https://qiita.com/rinfield/items/284fae0e1e55b28789e0)
- DenoのフォーマッターやリンターはCDKでは？
- [aws-lambda-nodejs](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda_nodejs-readme.html)モジュールは動かせる？ 
- CDK CLIはDenoで動かせない？
  - ⇒[その4](https://qiita.com/rinfield/items/02479ba04a727a9819de)
- npmスクリプトみたいなもんはないのかな

## 続き
https://qiita.com/rinfield/items/5496e7d1884a9d4593da

https://qiita.com/rinfield/items/284fae0e1e55b28789e0

https://qiita.com/rinfield/items/02479ba04a727a9819de