Hono + AWS CDK + サーバーレス（AWS Lambda + Amazon API Gateway）

# HonoをCDKでデプロイしてAWS Lambda（Amazon API Gateway）で動かす

- Honoを知ってから、試せていなかったので使ってみた。
    - これまでは、Serverless Framework（sls）を主体に利用していたがV4から有償になるので、CDKも試す。
    - API Gateway + AWS LambdaのWeb APIを作成してみる
    - そして、npm よりも Bunが速いようなので合わせて試す
    - 後日、ローカルのMySQLやPostgreSQLのコンテナ使い接続するのも試す予定

## 参考サイト
- https://dev.classmethod.jp/articles/cdk-hono-crud-api-lambda-api-gateway-dynamodb/ 
- こちらの参考サイトからDynamoDB関連を削除してAPIとしてのリクエストとレスポンス確認だけできるようにコード改変しています


## 0. 準備
- Dockerfile、CDK、Hono、 Lambdaのプログラムソースコードは、https://github.com/ssugimoto/hono-cdk-api-gw-lambda に置いています

- ローカル開発環境はDockerコンテナを利用します、docker composeを使いコンテナを起動し、VSCodeからRemote Explorerでの DEV Containersからコンテナ内を参照・操作しています。
### Dockerfile
```Dockerfile
FROM node:20
# ARG AWS_ACCESS_KEY_ID
# ARG AWS_SECRET_ACCESS_KEY

# update apt-get
RUN apt-get update -y && apt-get upgrade -y

RUN wget https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip \
  && unzip awscli-exe-linux-x86_64.zip \
  && ./aws/install \
  # Clean up
  && rm -f awscli-exe-linux-x86_64.zip

# Node.js 使うのでインストール
ENV NVM_DIR /root/.nvm
ENV NODE_VERSION 20.18.1
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.37.2/install.sh | bash \
  && . $HOME/.nvm/nvm.sh \
  && nvm install $NODE_VERSION \
  && nvm alias default $NODE_VERSION \
  && nvm use default \
  && node -v && npm -v && which npm

ENV NODE_PATH $NVM_DIR/v$NODE_VERSION/lib/node_modules
ENV PATH $NVM_DIR/versions/node/v$NODE_VERSION/bin:$PATH
RUN node -v
RUN npm -v
RUN npm install -g aws-cdk@2.172

# Bunのインストール
RUN curl -fsSL https://bun.sh/install | bash
ENV PATH $PATH:/root/.bun/bin/
RUN bun -v

# change work directory
RUN mkdir -p /app

# ホスト側との共有用で、ここにアプリケーションコードを置く
WORKDIR /app/app
```


#### node20以上、コンテナイメージの指定
Dockerfileに `FROM node:20`を指定

#### cdkのインストール
- コンテナの中に入ってインストールする場合、バージョンは最新がインストールされます
```
npm install -g aws-cdk
```
Dockerfileに記載する場合、CDKのバージョンを指定している場合
```
RUN npm install -g aws-cdk@2.171.1
```
Dockerfileに記載する場合、CDKのバージョンを指定してしない、最新がインストールされます
```
RUN npm install -g aws-cdk
```
#### aws cli
Dockerfileに記載してインストール
```
RUN wget https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip \
  && unzip awscli-exe-linux-x86_64.zip \
  && ./aws/install \
  # Clean up
  && rm -f awscli-exe-linux-x86_64.zip
```

#### Bunのインストール
Dockerfileに記載してインストール
```
RUN curl -fsSL https://bun.sh/install | bash
ENV PATH $PATH:/root/.bun/bin/
RUN bun -v
```

### 各種バージョン情報
```
root@8bb2f36d32b0:/app/app# npm -v
10.8.2

root@8bb2f36d32b0:/app/app# node -v
v20.18.1

root@8bb2f36d32b0:/app/app# cdk --version
2.172.0 (build 0f666c5)

root@8bb2f36d32b0:/app/app# bun -v
1.1.38
```


## 1. cdkプロジェクト作成

任意のディレクトリで
```
cdk init -l typescript
```

```sh
root@8bb2f36d32b0:/app/app/my-app4# cdk init -l typescript
Applying project template app for typescript
# Welcome to your CDK TypeScript project

This is a blank project for CDK development with TypeScript.

The `cdk.json` file tells the CDK Toolkit how to execute your app.

## Useful commands

* `npm run build`   compile typescript to js
* `npm run watch`   watch for changes and compile
* `npm run test`    perform the jest unit tests
* `npx cdk deploy`  deploy this stack to your default AWS account/region
* `npx cdk diff`    compare deployed stack with current state
* `npx cdk synth`   emits the synthesized CloudFormation template

Initializing a new git repository...
hint: Using 'master' as the name for the initial branch. This default branch name
hint: is subject to change. To configure the initial branch name to use in all
hint: of your new repositories, which will suppress this warning, call:
hint: 
hint:   git config --global init.defaultBranch <name>
hint: 
hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and
hint: 'development'. The just-created branch can be renamed via this command:
hint: 
hint:   git branch -m <name>
Executing npm install...
npm warn deprecated inflight@1.0.6: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
npm warn deprecated glob@7.2.3: Glob versions prior to v9 are no longer supported
✅ All done!
```


## 2. Honoのインストール、dotenvライブラリのインストール

cdk init したディレクトリと同じディレクトリで実行
```
bun install hono@4.6.13 
bun install dotenv@16.4.5
```


```
root@8bb2f36d32b0:/app/app/my-app4# bun install hono@4.6.13 
bun add v1.1.38 (bf2f153f)
[7.78ms] migrated lockfile from package-lock.json

installed hono@4.6.13

8 packages installed [3.92s]
root@8bb2f36d32b0:/app/app/my-app4# bun install dotenv@16.4.5
bun add v1.1.38 (bf2f153f)

installed dotenv@16.4.5

1 package installed [1049.00ms]
root@8bb2f36d32b0:/app/app/my-app4# 
````

## tree結果

```
├─.git
├─bin
├─lambda
│  ├─ index.ts
│  └─src
│      ├─ app.ts
│      └─api
│        └─ todos.ts
├─lib
├─node_modules
└─test
```

## 3.アプリケーションコード作成

- ディレクトリ作成
```
mkdir -p lambda/src/api
```

#### Web APIのリクエストを受ける、グループ化したルート
- todos.ts
```my-app4/lambda/src/api/todos.ts
import { Hono } from "hono";

// TODO のREST API
const todos = new Hono();
// 現在のUTC時刻を取得する関数
const getCurrentTimestamp = () => new Date().toISOString();

// Create: 新しいTodoを作成
todos.post("/", async (c) => {
  const now = getCurrentTimestamp();
  const params = {
    Item: {
      id: "dummyId" + now,
      createdAt: now,
      updatedAt: now,
    },
  };
  console.log("todo post.");
  return c.json(params);
});

// Read: 特定のユーザーの全てのTodoを取得
todos.get("/user/:userId", async (c) => {
  const userId = c.req.param("userId");
  const params = {
    ":userId": userId
  }
  return c.json(params);
});

// Read: 特定のTodoを取得
todos.get("/:id", async (c) => {
  const id = c.req.param("id");
  const params = {
    Key: { id }
  };
  return c.json(params);
});

// Update: Todoを更新
todos.put("/:id", async (c) => {
  const id = c.req.param("id");
  const params = {
    Key: { id }
  };
  return c.json({ "method": "put", params });
});

// Delete: Todoを削除
todos.delete("/:id", async (c) => {
  const id = c.req.param("id");
  const params = {
    Key: { id }
  };
  return c.json({ "method": "delete", params });
});

export { todos };

```

#### Honoのインスタンスを生成し、グループ化したルートをルーティング設定に追加
- app.ts
```my-app4/lambda/src/app.ts
import { Hono } from "hono";
import { logger } from "hono/logger";
import { basicAuth } from "hono/basic-auth";
import { todos } from "./api/todos";

// 純粋なHTTPサーバ、bun runで起動する
const app = new Hono();

// ログの設定
app.use("*", logger());
//Basic認証の設定
app.use(
  "*",
  basicAuth({
    username: process.env.BASIC_USERNAME ? process.env.BASIC_USERNAME : "",
    password: process.env.BASIC_PASSWORD ? process.env.BASIC_PASSWORD : "",
  })
);

app.route("/api/todos", todos);

export default app;
```

#### AWS Lambda向けのハンドラーを追加
- index.ts
```my-app4/lambda/index.ts
import { handle } from 'hono/aws-lambda'
import app from './src/app'

// index.ts で定義された純粋なHTTPサーバをAWS Lambda用のアダプタでラップしてハンドラとしてエクスポート
// AWS Lambda用にハンドラーをexport
export const handler = handle(app)
```

## 4. APIサーバーとして動かす

- package.jsonにrun用の定義を追加
```my-app4/package.json

  "scripts": {
    "build": "tsc",
    "watch": "tsc -w",
    "test": "jest",
    "cdk": "cdk",
    "dev": "ENV=development bun --hot run lambda/src/app.ts",
  },
```

- 環境特有の設定
アプリケーションコードは、コンテナの中ではなくホスト側に置いているためホットリードが効かないので、`--hot`を指定する

#### 起動
```
npm run dev
```

```
root@8bb2f36d32b0:/app/app/my-app4# npm run dev

> my-app4@0.1.0 dev
> ENV=development bun --hot run lambda/src/app.ts

Started development server: http://localhost:3000
```


## 5. ローカルでAPIを実行してみる

- Basic認証を設定しているのでリクエストヘッダーにつけるのを忘れずに

```
set username=""
set password=""
curl --basic -u %username%:%password% -X POST "http://localhost:3000/api/todos

curl --basic -u %username%:%password% -X POST "http://localhost:3000/api/todos

curl --basic -u %username%:%password% -X GET "http://localhost:3000/api/todos/user/1000

curl --basic -u %username%:%password% -X GET "http://localhost:3000/api/todos/1001

```

## 6. CDK用のコード作成

cdk init で作成された `my-app4/lib/my-app4-stack.ts` をAPI Gateway + AWS Lambda Node.js 20を使うようにする

## 7. cdk deploy

```
cdk deploy
```

### エラーが出る場合は、いくつかある。対処例です。

#### エラーa. bun用、npm用、lockファイルが複数あり、エラーになる

```
                                                           ^
Error: Multiple package lock files found: /app/app/my-app4/bun.lockb, /app/app/my-app4/package-lock.json. Please specify the desired one with `depsLockFilePath`.
    at findLockFile (/app/app/my-app4/node_modules/aws-cdk-lib/aws-lambda-nodejs/lib/function.js:1:3975)
    at new NodejsFunction (/app/app/my-app4/node_modules/aws-cdk-lib/aws-lambda-nodejs/lib/function.js:1:1977)
    at new MyApp4Stack (/app/app/my-app4/lib/my-app4-stack.ts:11:24)
    at Object.<anonymous> (/app/app/my-app4/bin/my-app4.ts:6:1)
    at Module._compile (node:internal/modules/cjs/loader:1469:14)
    at Module.m._compile (/app/app/my-app4/node_modules/ts-node/src/index.ts:1618:23)
    at Module._extensions..js (node:internal/modules/cjs/loader:1548:10)
    at Object.require.extensions.<computed> [as .ts] (/app/app/my-app4/node_modules/ts-node/src/index.ts:1621:12)
    at Module.load (node:internal/modules/cjs/loader:1288:32)
    at Function.Module._load (node:internal/modules/cjs/loader:1104:12)
Subprocess exited with error 1
root@8bb2f36d32b0:/app/app/my-app4# 
```

- 対処例
    - 対処例１、bun用のbun.lockbを一時的にリネームする。
    - 対処例２，CDKでのdepsLockFilePathでpackage-lock.jsonのパスを指定する
        - `../package-lock.json` のように相対パスでなぜか `Error: Lock file at ../package-lock.json doesn't exist` になる、stack.tsからのパスでなく、cdkコマンド実行するディレクトリからのパスが必要なので、 `depsLockFilePath: './package-lock.json',` にする

#### エラー b. Error: spawnSync docker ENOENT

- `Error: spawnSync docker ENOENT` は CDKが使うesbuildのインストールされていないので
```
root@8bb2f36d32b0:/app/app/my-app4# cdk deploy
Error: spawnSync docker ENOENT
    at Object.spawnSync (node:internal/child_process:1123:20)
    at spawnSync (node:child_process:877:24)
    at dockerExec (/app/app/my-app4/node_modules/aws-cdk-lib/core/lib/private/asset-staging.js:1:3596)
    at Function.fromBuild (/app/app/my-app4/node_modules/aws-cdk-lib/core/lib/bundling.js:1:4761)
    at new Bundling (/app/app/my-app4/node_modules/aws-cdk-lib/aws-lambda-nodejs/lib/bundling.js:1:4583)
    at Function.bundle (/app/app/my-app4/node_modules/aws-cdk-lib/aws-lambda-nodejs/lib/bundling.js:1:1066)
    at new NodejsFunction (/app/app/my-app4/node_modules/aws-cdk-lib/aws-lambda-nodejs/lib/function.js:1:2171)
    at new MyApp4Stack (/app/app/my-app4/lib/my-app4-stack.ts:11:24)
    at Object.<anonymous> (/app/app/my-app4/bin/my-app4.ts:6:1)
    at Module._compile (node:internal/modules/cjs/loader:1469:14) {
  errno: -2,
  code: 'ENOENT',
  syscall: 'spawnSync docker',
  path: 'docker',
  spawnargs: [
    'build',
    '-t',
    'cdk-681cc25a184e0e0d0cbc957d0169702f0f7d2a1159be4615241ebfd23087ede9',
    '--platform',
    'linux/amd64',
    '--build-arg',
    'IMAGE=public.ecr.aws/sam/build-nodejs20.x',
    '--build-arg',
    'ESBUILD_VERSION=0.21',
    '/app/app/my-app4/node_modules/aws-cdk-lib/aws-lambda-nodejs/lib'
  ]
}
Subprocess exited with error 1
root@8bb2f36d32b0:/app/app/my-app4# 
```


```
npm install --save-dev esbuild@0
```
を実行する

#### エラーc. cdkを実行するロール（権限）がない

```
Unable to resolve AWS account to use. It must be either configured when you define your CDK Stack, or through the environment

```

- ローカルのコンテナからAWSクラウドに対して操作する時のIAMロールがない（足らない）
- 手っ取り早いのは、aws configureでIAMユーザーのクレデンシャルを設定する

### cdk deploy
- エラー解消できたら、cdk deployが成功する
- 作成するスタック情報がコンソールに表示され、「`Do you wish to deploy these changes (y/n)?`」で 'y'を入力すれば、AWSクラウド上にAPI Gateway、AWS Lambdaのリソースが作成される。


## 8. API試す

作成された
```
MyApp4Stack.ApiEndpoint = https://XXXX.execute-api.us-east-1.amazonaws.com/prod/
MyApp4Stack.honomyapp4ApiEndpointXXXX = https://XXXX.execute-api.us-east-1.amazonaws.com/prod/
```
にAPIリクエストしてみる

```
set username=""
set password=""
curl --basic -u %username%:%password% -X GET https://xxxx.execute-api.us-east-1.amazonaws.com/prod/api/todos/1002

{"Key":{"id":"1002"}}
```


## ホットリロード
- dockerコンテナ外のファイルだとホットリードがきかない

#### 環境変数で対応する場合
https://qiita.com/SDTakeuchi/items/694fe7545396246da573

Dockerfile
```Dockerfile
frontend:
        build: ./frontend
        container_name: frontend_ultimate_timer
        hostname: frontend
        volumes:
            - ./frontend/ultimate_timer:/app
        tty: true
        environment:
            # - CHOKIDAR_USEPOLLING=true
            - WATCHPACK_POLLING=true
        ports:
            - "3000:3000"
```

#### npm run dev で対応する場合
- https://cacaca-blog.vercel.app/blogs/338fbcd0-cb9f-11ed-8c5e-c1499ac59172

```
package.json
"scripts": {
    "dev": "WATCHPACK_POLLING=true npm run dev",
  },

```


## その他
 
### Serverless Framework V4から有償になった

- https://serverless.co.jp/blog/xu024euy8kpy/
    ```
    Serverless Framework V4からはライセンス形態が変更されて、年間200万ドル以上の売上のある組織では有料でサブスクリプションを購入する必要があります。 それ以外の中小企業や個人の開発者は引き続き無料で使えます
    ```

    ```
     Serverless Framework V4では、HashiCorp Terraformとの統合が強化され、${terraform}変数を使ってTerraformの状態出力を簡単に取得できるようになりました。これにより、RDSやSQSなどの共有インフラをTerraformで管理し、アプリ固有のリソースはServerless Frameworkが担当するというハイブリッドな運用が可能です。
     ```
    - Terraformとの統合が強化はインフラリソースとアプリケーションリソースを分けて管理するケースはあるから便利だろうな

- コンサルティング相当は別プランが用意される予定（https://www.ragate.co.jp/blog/articles/21131）
- slsの代替となると、SAM、SAM + CDK、CDK、Terraformあたりの選択肢になる