【CDK】 色んな言語でCRUD API作る【TypeScript】

# この記事について
[AWS CDKv2](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-construct-library.html)でCRUD操作ができるAPIを作る
今回はTypeScriptを使う

# アプリの構成図
![infra (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2533663/06475e56-a024-719f-f594-47bcfae07c71.png)

API Gateway + Lambda + DynamoDBでTODOオブジェクトのCRUD操作ができるAPIを作る

# プロジェクト初期化
## CLIのインストール
まずはCDK CLIをインストールする

```bash
npm install -g aws-cdk
cdk --version
```

バージョンが表示されればOK

## CDK Toolkitのデプロイ
初回のみCDKを利用する際に必要となるファイル群のデプロイが必要

```bash
cdk bootstrap aws://${ACCOUNT}/${REGION}
```

以下のように表示されればOK

```bash
✅  Environment aws://${ACCOUNT}/${REGION} bootstrapped.
```

※異なるアカウント、リージョンでCDKを利用する場合はまたこの作業が必要になる

## プロジェクト初期化
適当なディレクトリで↓を実行

```bash
cdk init app --language typescript
```

こんな感じでプロジェクトが初期化される
![スクリーンショット 2022-07-01 153603.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2533663/e3f6d9e1-041a-e047-d184-e4d2b35b7451.jpeg)

# 実装
## CDKのコードを書く
### スタックの実装
`/lib`以下のファイルにスタック(CDKでデプロイするリソース群の単位)を実装する

```/lib/todoapp-ts-stack.ts
import { RemovalPolicy, Stack, StackProps } from "aws-cdk-lib";
import { Construct } from "constructs";
import * as iam from "aws-cdk-lib/aws-iam";
import * as lambda from "aws-cdk-lib/aws-lambda-nodejs";
import * as dynamodb from "aws-cdk-lib/aws-dynamodb";
import * as logs from "aws-cdk-lib/aws-logs";
import * as apigateway from "aws-cdk-lib/aws-apigateway";

export class TodoappTsStack extends Stack {
  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);

    // DynamoDB
    const todosTable = new dynamodb.Table(this, "TodosTable", {
      partitionKey: { name: "id", type: dynamodb.AttributeType.STRING },
      tableName: "todoapp-ts-todos-table",
      removalPolicy: RemovalPolicy.DESTROY,
    });

    // Lambda
    const todoFunction = new lambda.NodejsFunction(this, "TodoFunction", {
      entry: "src/main.ts", // Lambdaのソースコードのエントリポイント
      handler: "handler", // "entry"で指定したファイルのexportしている関数名
      environment: {
        TODOS_TABLE_NAME: todosTable.tableName,
      },
      functionName: "todoapp-ts-function",
    });
    // LambdaにDynamoDBのCRUD操作権限を付与
    todoFunction.addToRolePolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        actions: [
          "dynamodb:Scan",
          "dynamodb:PutItem",
          "dynamodb:UpdateItem",
          "dynamodb:DeleteItem",
        ],
        resources: [todosTable.tableArn],
      })
    );

    // LogGroup
    new logs.LogGroup(this, "TodoFunctionLogs", {
      logGroupName: "/aws/lambda/" + todoFunction.functionName,
      removalPolicy: RemovalPolicy.DESTROY,
    });

    // API Gateway
    const api = new apigateway.RestApi(this, "TodoApi", {
      restApiName: "todoapp-ts-todo-api",
      defaultCorsPreflightOptions: {
        allowOrigins: apigateway.Cors.ALL_ORIGINS,
        allowMethods: apigateway.Cors.ALL_METHODS,
        allowHeaders: apigateway.Cors.DEFAULT_HEADERS,
        statusCode: 200,
      },
    });
    const integration = new apigateway.LambdaIntegration(todoFunction);

    // /todos
    const todosResource = api.root.addResource("todos");
    todosResource.addMethod("GET", integration);
    todosResource.addMethod("POST", integration);

    // /todos/{id}
    const todoIdResource = todosResource.addResource("{id}");
    todoIdResource.addMethod("PUT", integration);
    todoIdResource.addMethod("DELETE", integration);
  }
}
```
#### Lambdaについて
Lambdaの作成には**aws-lambda-nodejs**を使っている

https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda_nodejs-readme.html

このモジュールを使うとTypeScriptのコードをjavascriptに変換する作業(トランスパイル)をCDKのデプロイ時によしなにやってくれる

ちなみに[Python](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-lambda-python-alpha-readme.html)と[Go](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-lambda-go-alpha-readme.html)にも同様のモジュールが用意されているもののまだアルファ版
javaと.NETにはないっぽい

## Lambdaのコード実装
[こちら](https://github.com/tetsu0000/todoapp-ts)のサンプルコードを参照
今回は[express](https://expressjs.com/ja/)でルーティングを、[serverless-express](https://github.com/vendia/serverless-express)でAPI Gatewayのイベントとの結合を実装してみた

# デプロイ

↓を実行
```bash
cdk deploy TodoappTsStack
```

ちなみに、スタック名などは`/bin/todoapp-ts.ts`で指定している

```/bin/todoapp-ts.ts
#!/usr/bin/env node
import "source-map-support/register";
import * as cdk from "aws-cdk-lib";
import { TodoappTsStack } from "../lib/todoapp-ts-stack";

const app = new cdk.App();
new TodoappTsStack(app, "TodoappTsStack"); // ここの"TodoappTsStack"を指定する
```

`/lib`にスタックを追加した場合はこのファイル内でインスタンス生成してあげればOK

# 動作確認

デプロイが完了したら以下のスクリプトを実行

```bash
#!/bin/bash

ENDPOINT="API Gatewayのエンドポイント"

RESPONSE=$(curl "${ENDPOINT}/todos" \
  -X POST \
  -H "Content-Type: application/json" \
  -d '{"message":"post todo"}' \
  -w"\n")
echo ${RESPONSE}
TODO_ID=$(echo ${RESPONSE} | jq .data.todo.id -r)

curl "${ENDPOINT}/todos" \
  -w"\n"

curl "${ENDPOINT}/todos/${TODO_ID}" \
  -X PUT \
  -H "Content-Type: application/json" \
  -d '{"message":"put todo"}' \
  -w"\n"

curl "${ENDPOINT}/todos/${TODO_ID}" \
  -X DELETE \
  -w"\n"
```

こんな感じで出力されていれば成功

```bash
{"message":"Post todo succeed.","data":{"todo":{"id":"39593ece-2bc7-4456-b52f-3265494b8331","message":"post todo"}}}
{"message":"Get todos succeed.","data":{"todos":[{"id":"39593ece-2bc7-4456-b52f-3265494b8331","message":"post todo"}]}}
{"message":"Put todo succeed.","data":{"todo":{"id":"39593ece-2bc7-4456-b52f-3265494b8331","message":"put todo"}}}
{"message":"Delete todo succeed."}
```

# 感想
TypeScriptの静的型付けによってCloudFormationをプログラミング言語で書けるCDKの恩恵を最大限に受けられた
また、Webアプリの場合はクライアントもTypeScriptで書くことが多いので言語を統一できるのもGood
LambdaのコードをTypeScriptで書いた場合のトランスパイルについても[それ用のモジュール](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda_nodejs-readme.html)があるので全く問題ない
CDKのサンプルコードや記事なども他の言語に比べて多いのもうれしい

総じて使い勝手は良好なのでWebアプリのプロジェクトならTypeScriptが丸いと思う

# 参考リンク

サンプルコード
https://github.com/tetsu0000/todoapp-ts

AWS公式ドキュメント
https://docs.aws.amazon.com/ja_jp/cdk/v2/guide/home.html
https://docs.aws.amazon.com/cdk/api/v2/docs/aws-construct-library.html

aws-sdk-v3の公式ドキュメント
https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/index.html

serverless-express
https://github.com/vendia/serverless-express