AWS CDKをDenoで動かす(完全版？)

# 要約
- Denoを使うことでNodeなしにAWS CDKを動作できる！
- これによりnpmツールチェーンのセットアップ、管理の手間から開放される！(…かも？)
- これは[Denoのnpmサポートが強化された](https://deno.land/manual@v1.28.3/node/npm_specifiers)ため可能になった


# まずは動かしてみましょう
1. [Denoをインストール](https://deno.land/manual@v1.28.3/getting_started/installation)します
    CDKアプリケーションコードの型チェックをする場合Deno v1.28.2以上である必要があります
2. 以下の2ファイルを用意します
    [Denoの設定ファイルである`deno.jsonc`](https://deno.land/manual@v1.28.3/getting_started/configuration_file)です。主にタスクランナーの設定がしてあります。
    ```jsonc
    // deno.jsonc
    {
      "tasks": {
        "cdk": "deno run --allow-all npm:aws-cdk --app 'deno run --allow-all main.ts'",
        "test": "deno test --allow-all",
        "ci": "deno fmt --check && deno lint && deno task test"
      },
      "lint": {
        "files": {
          "exclude": ["cdk.out"]
        }
      },
      "fmt": {
        "files": {
          "exclude": ["cdk.out"]
        }
      }
    }
    ```
    CDKアプリケーションコードです。シンプルなリソースであるロググループを作成しています。
    ```ts
    // main.ts
    import cdk from "npm:aws-cdk-lib";
    
    const app = new cdk.App();
    export const stack = new cdk.Stack(app, "DenoCdkStack");
    
    new cdk.aws_logs.LogGroup(stack, "DenoCdkLogGroup");
    ```
3. ターミナルから実行します
    [Denoのタスクランナー](https://deno.land/manual@v1.28.3/tools/task_runner)経由で実行するため`deno task cdk <CDKサブコマンド>`で呼び出します。
    ```console
    deno task cdk synth
    ```
    <details><summary>実行結果(クリックして展開)</summary>

    ```
    Task cdk deno run --allow-all npm:aws-cdk --app 'deno run --allow-all main.ts' "synth"
    Resources:
      DenoCdkLogGroup24F7F268:
        Type: AWS::Logs::LogGroup
        Properties:
          RetentionInDays: 731
        UpdateReplacePolicy: Retain
        DeletionPolicy: Retain
        Metadata:
          aws:cdk:path: DenoCdkStack/DenoCdkLogGroup/Resource
      CDKMetadata:
        Type: AWS::CDK::Metadata
        Properties:
          Analytics: v2:deflate64:H4sIAAAAAAAA/zPSMzXQM1BMLC/WTU7J1s3JTNKrDi5JTM7WAQrF5+SnF+tV++SnuxfllxboOKflwdi1IE5QanF+aVFyKojtnJ+XklmSmZ9Xq5OXn5Kql1WsX2ZopmdoDjQ9qzgzU7eoNK8kMzdVLwhCAwCBVnBAeQAAAA==
        Metadata:
          aws:cdk:path: DenoCdkStack/CDKMetadata/Default
        Condition: CDKMetadataAvailable
    Conditions:
      CDKMetadataAvailable:
        Fn::Or:
          - Fn::Or:
              - Fn::Equals:
                  - Ref: AWS::Region
                  - af-south-1
              - Fn::Equals:
                  - Ref: AWS::Region
                  - ap-east-1
              - Fn::Equals:
                  - Ref: AWS::Region
                  - ap-northeast-1
              - Fn::Equals:
                  - Ref: AWS::Region
                  - ap-northeast-2
              - Fn::Equals:
                  - Ref: AWS::Region
                  - ap-south-1
              - Fn::Equals:
                  - Ref: AWS::Region
                  - ap-southeast-1
              - Fn::Equals:
                  - Ref: AWS::Region
                  - ap-southeast-2
              - Fn::Equals:
                  - Ref: AWS::Region
                  - ca-central-1
              - Fn::Equals:
                  - Ref: AWS::Region
                  - cn-north-1
              - Fn::Equals:
                  - Ref: AWS::Region
                  - cn-northwest-1
          - Fn::Or:
              - Fn::Equals:
                  - Ref: AWS::Region
                  - eu-central-1
              - Fn::Equals:
                  - Ref: AWS::Region
                  - eu-north-1
              - Fn::Equals:
                  - Ref: AWS::Region
                  - eu-south-1
              - Fn::Equals:
                  - Ref: AWS::Region
                  - eu-west-1
              - Fn::Equals:
                  - Ref: AWS::Region
                  - eu-west-2
              - Fn::Equals:
                  - Ref: AWS::Region
                  - eu-west-3
              - Fn::Equals:
                  - Ref: AWS::Region
                  - me-south-1
              - Fn::Equals:
                  - Ref: AWS::Region
                  - sa-east-1
              - Fn::Equals:
                  - Ref: AWS::Region
                  - us-east-1
              - Fn::Equals:
                  - Ref: AWS::Region
                  - us-east-2
          - Fn::Or:
              - Fn::Equals:
                  - Ref: AWS::Region
                  - us-west-1
              - Fn::Equals:
                  - Ref: AWS::Region
                  - us-west-2
    Parameters:
      BootstrapVersion:
        Type: AWS::SSM::Parameter::Value<String>
        Default: /cdk-bootstrap/hnb659fds/version
        Description: Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]
    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.
    ```

    </details>

動きました！🎉


## テストも追加する場合
1. 以下のテストコードを配置します
    [スナップショットテスト](https://deno.land/manual@v1.28.3/basics/testing/snapshot_testing)機能を使ったCloudFormationテンプレート全体のチェックをします。
    ```ts
    // main.test.ts
    import { assertSnapshot } from "https://deno.land/std@0.167.0/testing/snapshot.ts";
    import { Template } from "npm:aws-cdk-lib/assertions";
    import { stack } from "./main.ts";
    
    Deno.test("Snapshot Test", async (t) => {
      await assertSnapshot(t, Template.fromStack(stack).toJSON());
    });
    ```
2. ターミナルから実行します
    初回でスナップショットが作成されていないので、まずは作成します。
    完了後に`__snapshots__/main.test.ts.snap`が生成されます。
    ```console
    deno task test -- --update
    ```
    スナップショットを更新しない場合(変更がないことを確信している)は`--update`オプションなしで実行します。
    Snapshotと差分がある場合はテスト失敗になります。
    ```console
    deno task test
    ```
3. CI環境でフォーマッター、リンター、テストのチェックを行う場合のために`ci`タスクも定義してあります
    ```
    deno task ci
    ```


# 何がうれしいのか？
一言でいうとNodeとnpmエコシステムの複雑さが一掃されるという点に付きます。
上記の手順の通り設定ファイルはDenoの`deno.jsonc`しか設定していません！
それなのに型チェックやフォーマッター、リンター、テストフレームワーク、必要なモノが全て揃った環境が手に入ります。

更にVSCode + Denoプラグインの環境を用意するだけで、コード補完や型チェックができる環境が出来上がります。

## Node + npmでリンター、フォーマッター、テスト環境を揃えた場合

Denoを使わない場合

- TypeScript, ts-node (TypeScript)
- npm or yarn or pnpm (パッケージマネージャ)
- Prettier (フォーマッター)
- ESLint (リンター)
- Jest (テストフレームワーク)

これらの設定がそれぞれ必要です。(IDEがある場合更にそれぞれのプラグインの設定も必要)
これらのバージョン、トレンド、ベストプラクティスも刻一刻と変化/進化しており追随するのは大変です。(特にTypeScript用の設定が難しくてよくわからずハマりがち)

特にこの記事を読んで頂いているCDKを扱うような方々はAWSインフラがメインであることがほとんどのはずです。
re:Inventの新サービスや機能[^1]を追っかけるだけでも大変なのに、
「npmとyarnどれがいい？yarnのメジャーバージョンアップするべき？」とか
「ESLintとPrettierの推奨組み合わせ方法が変わった」[^2]とか正直追っかけてらんないし、やりたくないですよね？

私はやりたくないです😊


# Denoの動向
これが可能になったのはごく最近Deno v1.28から[npm指定子](https://deno.land/manual@v1.28.3/node/npm_specifiers)をサポートするようになったためです。
これにより多くのnpmパッケージがDeno上で動作するようになりました。

GA当初のバージョンではCDKアプリケーションコードのタイプチェックがうまく行かない問題がありましたが、DenoリポジトリにIssueを報告して直してもらっています。

https://github.com/denoland/deno/issues/16695

## かつてのDenoでは

Denoは従来、npmパッケージを動かすにはCDN側でESMに変換しESMとして実行するという方式をとっていました。
過去の記事ではその方式で試していましたが、DenoのCDN側の不安定さやDenoのnode互換性の不足によりCDK CLIが動かなかったり、タイムアウトしたりで残念ながら実戦投入できるレベルではありませんでした。

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

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

これらの記事は本記事とは別の方法であるため、混同しないようご注意下さい。


# 注意点
もちろん現状使っているNode + CDKの環境をDenoにすぐ移行できるとは限りません。

おそらくNodeプロジェクト構成を前提としているCDKの[NodeJsFunctionクラス](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda_nodejs.NodejsFunction.html)などの実装は動かないでしょう。
また、ESLintは柔軟なプラグイン機構によりルールを追加することができますが、Denoのリンターはそういった機構がなさそうなため、移行する際はチェックできないルールがあるかもしれません。(個人的にはCDKアプリケーションコードにそこまで厳密なチェックが必要とも思わないですが…)

他にも単純には移行できない部分もあるかと思うので、きちんと検証の上自己責任でご利用ください。

また[Denoのパーミッション](https://deno.land/manual@v1.28.3/basics/permissions)が簡略化のため全許可になっておりますので気になる場合修正ください。


# 追記
`deno task cdk deploy`が動きませんでしたすみません…
まだ完全移行の道は長そうです…

# 追記2
以下のバージョンで`deno task cdk deploy`も動作することを確認しました:rocket:

||バージョン|
|-|-|
|Deno|1.37.2|
|AWS CDK|2.102.0|


[^1]: https://qiita.com/nasuvitz/items/5b7589b911cccf35ea30
[^2]: https://blog.ojisan.io/prettier-eslint-cli/