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
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
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
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
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
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用の定義を追加
"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#
- 対処例
- 対処例1、bun用のbun.lockbを一時的にリネームする。
- 対処例2,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コンテナ外のファイルだとホットリードがきかない
環境変数で対応する場合
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 で対応する場合
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あたりの選択肢になる