0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Hono + AWS CDK + サーバーレス(AWS Lambda + Amazon API Gateway)

Posted at

HonoをCDKでデプロイしてAWS Lambda(Amazon API Gateway)で動かす

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

参考サイト

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
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# 
  • 対処例
    • 対処例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あたりの選択肢になる

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?