CDK で Hello Lambda

# 小話

最近見つけた grape の「[缶詰ウォーマー](https://shop.grapee.jp/products/flo-002-set)」が気になって仕方がないこの頃です。何より小鍋も嵌るサイズ感なのが良い。ミニおでんでも仕立てて 日本酒をしっぽり飲みながら夜を過ごすのもいいじゃないですか。

めっちゃ使ってみたい反面、お試しとばかりに色々な缶詰をスタックしそうで…。うーん、、気分は商品棚の前を何往復もウロウロしてるような感じです。

さて今回は缶詰をスタック、、、ではなく、AWS CDK を使って CloudFormation のスタックで管理された Lambda を作ってみるチュートリアルをしたいと思います。

# 対象者と前提条件

こんな方を対象にしています。

* AWS を触っているけど構築を自動化したい
* EC2 以外のことにチャレンジしたい
* CloudFormation のテンプレート直書きや SAM を使ってるけど限界を感じてきた
* Infra as Code (IaC) に興味がある

前提として既に AWS アカウントがあり、Node.js の環境が必要です。

# CDK is 何？

公式の説明はこれです。

https://aws.amazon.com/jp/cdk/features/

・・・要約すると

* プログラム実行によって超ダイナミックな **CloudFormation のスタック定義ファイル (JSON)** を作成できる
    * VPC ID など既にある管理外リソースでも気軽に参照できて、関連付けできます
    * 開発モード・本番モードなどでの生成分け・名前変えも柔軟にできます
* Lambda の実行コードを自動コンパイル＆自動アップロード
* ファイルをまとめて S3 バケットに自動アップロード
    * SPA のような Web アプリ、単純な HTML
    * 動的に生成した JSON ファイルもできる - クライアントID、環境で異なる API のエンドポイント、機能スイッチとか
* コンテナのビルドをして Amazon ECR に自動登録

みたいな事をやってくれます。太字で書いたように CloudFormation のスタックが出来上がるのが最終ゴールになります。

# 準備

## AWS のアクセス設定

コマンドラインから AWS へアクセスするので、IAM ユーザーでアクセスキーやシークレットを発行してローカルに設定する必要があります。

* aws configure コマンドで設定する
* または AWS_ACCESS_KEY, AWS_ACCESS_SECRET 環境変数を設定する
* または saml2aws のようなツールでトークン認証する

設定方法は割愛します。

## AWS CDK ツールキット

```powershell
npm i -g aws-cdk
# または
yarn global add aws-cdk
# または
pnpm add -g aws-cdk
```

でインストールします。`cdk --version` と打ってバージョン番号が表示できれば OK です。

## AWS のリージョン準備

CDK のデプロイ時に自動で諸々のファイルをアップロードなり登録をしてくれますが、それらを格納するための枠組みをデプロイ予定のリージョンに作っておく必要があります。これは `cdk bootstrap` を実行します。

```powershell
cdk bootstrap aws://123456789012/ap-northeast-1
cdk bootstrap aws://123456789012/us-east-1
```

`123456789012` 部分はアカウント ID です。ブラウザーで AWS コンソールにサインイン後、右上のプロフィールをクリックしてドロップダウンすると確認できます。

ひとまず、東京リージョンと何かと入り用になるバージニア北部、合計 2 つのリージョンが準備できてれば十分です。

# プロジェクト作り

AWS のワークショップ記事が参考になります。同じプロジェクト構造を作っていきます。

[AWS CDK > TypeScript ワークショップ > プロジェクト構造](https://catalog.us-east-1.prod.workshops.aws/workshops/99731164-1d19-4d2e-9319-727a130e2d57/ja-JP/20-typescript/20-create-project/300-structure)

## 下ごしらえ

### テンプレから生成

開発用に空のディレクトリを用意して TypeScript 用のテンプレートを作ります。

```powershell
mkdir cdk-workshop
cd cdk-workshop

cdk init app --language=typescript --generate-only
npm i
```

NOTE: `--generate-only` を付けることで git リポジトリ化とパッケージの自動インストールをしなくなります。yarn や pnpm によるパッケージ管理をしたい場合におすすめです。

### 依存パッケージの整備

各パッケージのバージョンが古いので、適宜更新します。

```powershell
npm outdated

npm i typescript@latest jest@latest ts-jest@latest
npm i @types/node@latest @types/jest@latest @types/prettier@latest
```

CDK 関連の重要なライブラリ 2 点 `aws-cdk-lib` と `constructs` は最新が入るようですが、バージョン固定した方が無難です。

```powershell
npm i -E aws-cdk-lib@latest
npm i -E constructs@latest
```

### 追加パッケージ

Lambda を使うのに便利な定義と、ビルドツールを入れます。

```powershell
npm i -D @types/aws-lambda esbuild
```

NOTE: esbuild がローカルで見つからないと、自動で Docker を使たビルド環境内でビルドされます。

# ソース

[NodejsFunction](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_lambda_nodejs.NodejsFunction.html) を使って Lamda を作ります。ついでに [関数 URL](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-urls.html) を発行して、Web API としてアクセスできるようにしてみます。

## スタックの構築

既存の `cdk-workshop-stack.ts` ファイルを以下の内容で丸ごと書き換えます。

```typescript:lib/cdk-workshop-stack.ts
import { CfnOutput, Stack, StackProps } from 'aws-cdk-lib';
import { FunctionUrlAuthType, Runtime } from 'aws-cdk-lib/aws-lambda';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Construct } from 'constructs';
import { join } from 'path';

export class CdkWorkshopStack extends Stack {
  private helloUrl: string;

  constructor(scope: Construct, id: string, props?: StackProps) {
    super(scope, id, props);
    this.buildHelloLambda();
    new CfnOutput(this, "HelloUrl", {value: this.helloUrl});
  }

  private buildHelloLambda() {
    const lambda = new NodejsFunction(this, "HelloLambda", {
      runtime: Runtime.NODEJS_16_X,
      entry: join(__dirname, "hello-lambda.ts"), // 実行するプログラム
      handler: "handler", // Lambda が呼び出されたときに実行する関数名 (既定は handler)
      environment: { // 環境変数として渡すパラメーター (process.env.XXX で参照できる)
        SERVICE_NAME: "Foo Bar"
      }
    });

    const funcUrl = lambda.addFunctionUrl({
      authType: FunctionUrlAuthType.NONE
    });
    this.helloUrl = funcUrl.url;
  }
}
```

## Lambda の作成

NodejsFunction の entry で指定している `hello-lambda.ts` を新規作成します。
折角なので、環境変数の参照とリクエストで受け取った情報のうちソース IP アドレスを利用してみようと思います。

```typescript:lib/hello-lambda.ts
import { APIGatewayProxyEventV2, APIGatewayProxyHandlerV2 } from "aws-lambda";

const handler: APIGatewayProxyHandlerV2 = async (event: APIGatewayProxyEventV2) => {
    const serviceName = process.env.SERVICE_NAME;
    const sourceIp = event.requestContext.http.sourceIp
    return {
        statusCode: 200,
        body: JSON.stringify({
            message: `ようこそ ${serviceName} へ!`,
            sourceIp: `接続元IPアドレスは '${sourceIp}' です`,
        }),
    };
};

export { handler };
```

# デプロイ

```powershell
cdk synth --quiet
cdk deploy
```

途中でデプロイの確認が入るので `y` を押して続行します。

```
Do you wish to deploy these changes (y/n)? y
```

成功するとデプロイの最後にある `Output` の項目に作っておいた `HelloUrl` が表示されます。

```text
CdkWorkshopStack: deploying...
[0%] start: Publishing 1e5cdcd7531be9776328964c60cd8edfa8b49c660f1fba2b562af17751d4afb5:current_account-current_region
＜＜省略＞＞
CdkWorkshopStack | 6/6 | 10:06:12 | CREATE_COMPLETE      | AWS::CloudFormation::Stack | CdkWorkshopStack

 ✅  CdkWorkshopStack

✨  Deployment time: 80.55s

Outputs:  ＜＜ココ☟＞＞
CdkWorkshopStack.HelloUrl = https://bcfw6ckna3qoxaairv4bgsowx40pjefx.lambda-url.ap-northeast-1.on.aws/
Stack ARN:
arn:aws:cloudformation:ap-northeast-1:123456789012:stack/CdkWorkshopStack/c23ba720-5bdc-11ee-8ee0-0e3f14b4b7ef

✨  Total time: 88.65s
```

# レッツアクセス！

ブラウザー等で生成された URL `https://{ランダム}.lambda-url.ap-northeast-1.on.aws/` にアクセスします。
すると、ウェルカムメッセージとIPアドレスが返ってきます。

```powershell
curl https://bcfw6ckna3qoxaairv4bgsowx40pjefx.lambda-url.ap-northeast-1.on.aws/
{"message":"ようこそ Foo Bar へ!","sourceIp":"接続元IPアドレスは '1.2.3.4' です"}
```

# まとめ

## AWS に何が生成されたか？

* [CloudFormation スタック](https://ap-northeast-1.console.aws.amazon.com/cloudformation/home?region=ap-northeast-1#/stacks) にスタックが 2 個できてます
    * **CDKToolkit** - bootstrap で作られたもの
    * **CdkWorkshopStack** - 今回のサンプルで作ったスタック
        * リソースタブで Lambda 等の生成物を見れます
        * 出力タグで `HelloUrl` の出力値が見れます
* [Lambda アプリケーション](https://ap-northeast-1.console.aws.amazon.com/lambda/home?region=ap-northeast-1#/applications)
    * **CdkWorkshopStack** こちらでも見えます
* [Lambda 関数](https://ap-northeast-1.console.aws.amazon.com/lambda/home?region=ap-northeast-1#/functions)
    * **CdkWorkshopStack-HelloLambda{ランダム}-{ランダム}**
        * 作成した hello-lambda.ts が実行される Lambda です
        * 「設定＞環境変数」で定義した `SERVICE_NAME` があるのを確認できます
* [IAM ロール](https://us-east-1.console.aws.amazon.com/iamv2/home?region=ap-northeast-1#/roles)
    * **CdkWorkshopStack-HelloLambdaServiceRole{ランダム}-{ランダム}**
        * Lambda にアタッチされるロールです
        * Lambda 側の「設定＞アクセス権限」で確認できます

CDK でデプロイすることで以上のようなリソースが AWS に展開され、準備完了の状態まで一気にできました。便利ですね。

しかも、実際にはコードとして記述していない IAM ロールがあるように必要なものも適宜作ってくれます。
この辺りを AWS の膨大なリファレンス、時には長い英語のみの文章を読み込んで理解して正確に記述・連携するだけでも膨大な時間が掛かる作業です。それだけでも CDK を導入する価値があると私は思います。

スタック定義のソース内で、buildHelloLambda() のように生成するリソースのグループ・役割ごとに関数分けしておけるというのも便利です。もっと汎用化してライブラリ化・共通部品化する事も出来るわけです。夢が広がリングですね！

## 後始末

```powershell
cdk destroy
```

でデプロイしたリソースを削除できます。または CloudFormation スタックから直接削除することでもできます。

缶詰ウォーマーどうしよっかな…。