月曜日, 5月 5, 2025
ホームニューステックニュース【Hono】API Gateway + Hono を使って、Lambda1つでバックエンドAPIを全部作ってみた #AWS

【Hono】API Gateway + Hono を使って、Lambda1つでバックエンドAPIを全部作ってみた #AWS



【Hono】API Gateway + Hono を使って、Lambda1つでバックエンドAPIを全部作ってみた #AWS

バックエンドをAPI Gatewayに任せる時にめんどくさいのが、「複数のパスの管理」です。
各パスに対応するlambdaを書いたり、また、IaCのコード上で各パスをだらっと定義しまくったり・・・(for_eachなど工夫の余地はあると思いますが)。
そういうの面倒なので、今回はLambdalith という手法を試してみます。
Lambdalithとは、1つのlambdaで複数のルート(パス)を処理しようというlambdaの設計スタイルです!
簡単にLambdalithを実現できるhonoというライブラリを使って、楽に実装してみます!!

Lambdalith.png

※参考
Lambdalithについてまとまっている読みやすい記事です

アーキ

アーキ図.png

今回のアーキです。
API Gatewayと、lambdaが1つです!
Lambdalithを実現するために、lambda上のコードはhono で実装します。

honoについては後述します。とりあえずドキュメント貼っておきます。

リポジトリ

参考になるかわかりませんが、今回作業したリポジトリです。

リポジトリの概要は以下

①honoでlambdaのコードを準備する

まずは今回の主役であるhonoというライブラリでlambdaを実装していきます!

honoのプロジェクトを準備

package.json

{
  "name": "function",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "esbuild src/handler.ts --bundle --platform=node --outdir=dist"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "hono": "^4.7.8"
  },
  "devDependencies": {
    "@hono/node-server": "^1.14.1",
    "esbuild": "^0.25.3",
    "tsx": "^4.19.4"
  }
}

上記package.json を参照し、pnpm i していきます。

登場するライブラリを軽く紹介します。
hono
この記事の主役です。
REST APIサーバーを簡単に構築できるnodejsのライブラリです!!
ポータビリティが売りの一つで、今回やるように、lambdaなどにも簡単にのせることができます!!!

他にもいろいろ凄いです。やばいっす。
日本語版もあるくらい公式Docがフレンドリーで、よくまとめてくれています(AI向けのマークダウン形式ページもあったりしてそこも良い)。
この記事では紹介しきれないので、ぜひ読んでみてください。

@hono/node-server
honoをローカルに立てるためのライブラリです。

esbuild
tsをjsにトランスパイルするライブラリです。tscより速いらしいです。

tsx
tsを実行するライブラリです。ts-nodeより速いらしいです。

色々なルートをさばくhonoインスタンスを準備

では早速honoでREST APIを作っていきます。
中身はなんでもいいので、公式Docからコピペして以下のような構成にしました。

.
└── function
    ├── package.json
    └── src
        ├── app.ts
        ├── handler.ts
        ├── index.ts
        └── routes
            ├── book.ts
            └── user.ts

1つずつ見ていきます!

src/routes/book.ts

APIGatewayの「/book」パスに一致するリクエストをさばくAPIを定義します。
複数のルーティングをまとめる場所で「/book」に対応させます。

src/routes/book.ts

import { Hono } from 'hono'

const book = new Hono()

book.get('/', (c) => c.text('List Books')) // GET /book
book.get('/:id', (c) => {
    // GET /book/:id
    const id = c.req.param('id')
    return c.text('Get Book: ' + id)
})
book.post('/', (c) => c.text('Create Book')) // POST /book

export default book

src/routes/user.ts

src/routes/book.ts と同じです。置換しただけですね。

src/routes/user.ts

import { Hono } from 'hono'

const user = new Hono()

user.get('/', (c) => c.text('List Users')) // GET /user
user.get('/:id', (c) => {
    // GET /user/:id
    const id = c.req.param('id')
    return c.text('Get User: ' + id)
})
user.post('/', (c) => c.text('Create User')) // POST /user

export default user

src/app.ts

上記で作成した複数のルーティングインスタンスを統合するファイルです。
ルートが増えるたびにここに追加していくイメージです。

src/app.ts

import { Hono } from 'hono'

import book from './routes/book'
import user from './routes/user'

const app = new Hono()

app.route('/book', book)
app.route('/user', user)

export default app

src/handler.ts

上記で作成した、メインのインスタンスをlambda用のラッパーでつつんでexportするファイルです。
記載量の少なさがやばいですね。lambdaにはこのコードを載せます。これだけで良いのがビックリです。

src/handler.ts

import { handle } from 'hono/aws-lambda'

import app from './app'

export const handler = handle(app)

src/index.ts

lambdaには直接は不要です。
テスト用にlocalhostで上記のsrc/app.tsをREST APIサーバーとしてたてるためのファイルです。

src/index.ts

import { serve } from '@hono/node-server'
import app from './app'

const port = 3000
console.log(`Server is running on http://localhost:${port}`)

serve({
    fetch: app.fetch,
    port
})

package.json

  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "esbuild src/handler.ts --bundle --platform=node --outdir=dist"
  },
"dev": "tsx watch src/index.ts",

これで動きます。

> pnpm run dev
Server is running on http://localhost:3000

動かしてlocalでテストします!

>curl localhost:3000/user
List Users
>curl localhost:3000/user/1234
Get User: 1234
>curl -X POST localhost:3000/user
Create User
>curl -X POST localhost:3000/book
Create Book
>

良い感じですね!!

jsへトランスパイル

紹介したesbuildでトランスパイルします。

package.json

  "scripts": {
    "dev": "tsx watch src/index.ts",
    "build": "esbuild src/handler.ts --bundle --platform=node --outdir=dist"
  },
> pnpm run build

> function@1.0.0 build C:/work/repo/hono-aws-hello-world/function
> esbuild src/handler.ts --bundle --platform=node --outdir=dist


  dist/handler.js  55.1kb

Done in 71ms

爆速!!!

Lambda + API Gatewayを準備する

terraformでさっくりと

リソースの作成自体はterraformでさくっと終わらせます。
上記jsをのせたlambdaと、空のAPI Gatewayリソースを作成します。
つなげるところだけ手でやりましょう!

main.tf (長いので略)

main.tf

############################################################################
## terraformブロック
############################################################################
terraform {
  # Terraformのバージョン指定
  required_version = "~> 1.7.0"

  # Terraformのaws用ライブラリのバージョン指定
  required_providers {
    aws = {
      source  = "hashicorp/aws"
      version = "~> 5.33.0"
    }
  }
}

############################################################################
## providerブロック
############################################################################
provider "aws" {
  # リージョンを指定
  region = "ap-northeast-1"
}

locals {
  project = "hono_aws_hello_world"
  dir_path = "${path.module}/../function/dist"
}

############################################################################
## lambda
############################################################################
/* lambda実行ロール */
# lambda用AWSマネージドポリシーを準備
# ロギング用ポリシードキュメント
data "aws_iam_policy_document" "logging" {
  statement {
    effect = "Allow"

    actions = [
      "logs:CreateLogStream",
      "logs:PutLogEvents",
    ]

    resources = ["${aws_cloudwatch_log_group.lambda.arn}:*"]
  }
}

# ロギング用ポリシー
resource "aws_iam_policy" "logging" {
  name        = "lambda-logging-policy"
  description = "IAM policy for Lambda to write logs to CloudWatch"
  policy      = data.aws_iam_policy_document.logging.json
}

# lambda assume用ポリシードキュメント
data "aws_iam_policy_document" "lambda_assume_role" {
  statement {
    effect  = "Allow"
    principals {
      type        = "Service"
      identifiers = ["lambda.amazonaws.com"]
    }
    actions = ["sts:AssumeRole"]
  }
}

# lambda用IAMロール作成
resource "aws_iam_role" "lambda_execution_role" {
  name               = "my-lambda-execution-role"
  assume_role_policy = data.aws_iam_policy_document.lambda_assume_role.json
}

# lambda用IAMロールへポリシーをアタッチ
resource "aws_iam_role_policy_attachment" "lambda_logs_attach" {
  role       = aws_iam_role.lambda_execution_role.name
  policy_arn = aws_iam_policy.logging.arn
}

# ロググループを作成
resource "aws_cloudwatch_log_group" "lambda" {
  name              = "/aws/lambda/${local.project}_lambda"
  retention_in_days = 14
}

# zipを作成
data "archive_file" "lambda_my_function" {
  type             = "zip"
  output_file_mode = "0666"
  source_dir       = local.dir_path
  output_path      = "${local.dir_path}.zip"
}

/* lambda関数 */
resource "aws_lambda_function" "lambda" {
  function_name = "${local.project}_lambda"
  role          = aws_iam_role.lambda_execution_role.arn

  runtime  = "nodejs20.x" # TODO:Terraform古い
  filename = data.archive_file.lambda_my_function.output_path
  handler  = "handler.handler"

  logging_config {
    log_format = "Text"
    log_group  = aws_cloudwatch_log_group.lambda.name
  }

  # Terraformに変更を無視させるため、lifecycle ルールを追加
  lifecycle {
    ignore_changes = [filename, source_code_hash]
  }
}

# API Gateway
resource "aws_api_gateway_rest_api" "rest" {
  name = "${local.project}_rest_api"
}

# lambdaをAPI Gatewayから実行できるように許可する
resource "aws_lambda_permission" "apigw_lambda" {
  statement_id  = "AllowExecutionFromAPIGateway"
  action        = "lambda:InvokeFunction"
  function_name = aws_lambda_function.lambda.function_name
  principal     = "apigateway.amazonaws.com"

  source_arn = "${aws_api_gateway_rest_api.rest.execution_arn}/*/*"
}

API GatewayとLambdaをくっつける

API Gatewayのプロキシリソース機能とは

複数のパスをAPI Gatewayに設定する場合、個々にメソッドを設定する必要があると思います。
ただ今回は、すべてのリクエストを1つのLambdaでまとめて処理したいので、APIGatewayの「プロキシリソース機能」を使って、全パスのリクエストを1つのLambda関数に集約する構成にしてみます。

マネコンから操作してみる

プロキシリソース作成画面へ

cap1.PNG

作成したAPI Gatewayリソースを表示し、「リソースを作成」をクリックします

プロキシリソースとして定義する

cap2.PNG

「プロキシのリソース」トグルをチェックします。

リソース名を設定する

cap3.PNG

リソース名が求められるので定義します。文字列に意味はないため、例示されている{proxy+} を設定します。
設定は以上なので、「リソースを作成」をクリックします。

リソース作成結果を確認する

cap4.PNG

良い感じですね。
ただ、メソッドの統合タイプが設定なしになっています。
リクエストはキャッチしますが、そのリクエストの伝搬先が設定されていない状況です。

メソッド設定画面へ

cap4_2.PNG

「ANY」をクリックします。

バックエンドリソースが未定義であることを確認する

cap5.PNG

未定義ですね。

統合先設定画面へ

cap5_2.PNG

「統合を編集」をクリックします。

Lambdaとプロキシ統合するよう設定する

cap6.PNG

「Lambdaプロキシ統合」トグルをオンにします。
honoは単体でもREST APIとして必要なレスポンスヘッダーやbodyを定義可能なので、プロキシ統合でOKです。ここら辺も簡単で素晴らしいですね。

作成したLambdaを指定する

cap7.PNG

作成したlambdaをドロップダウンから選択して、「保存」をクリックします。

喜ぶ

cap8.PNG

設定が完了しました。これだけで全てのパスがさばけます!!!!
今までIaCの中でだらだらパスを定義していたのはなんだったのでしょうか、最高!!!!

動作確認する!

GETしてみる

>curl https://ilm77o6qdl.execute-api.ap-northeast-1.amazonaws.com/dev/user
List Users
>curl https://ilm77o6qdl.execute-api.ap-northeast-1.amazonaws.com/dev/user/1234
Get User: 1234
>

良い感じ!

POSTしてみる

>curl -X POST https://ilm77o6qdl.execute-api.ap-northeast-1.amazonaws.com/dev/user
Create User
>curl -X POST https://ilm77o6qdl.execute-api.ap-northeast-1.amazonaws.com/dev/book
Create Book

最高!

存在しないパスを指定してみる

>curl https://ilm77o6qdl.execute-api.ap-northeast-1.amazonaws.com/dev/test
404 Not Found

なんとなにもしていないのにfallbackなレスポンスも返却してくれます!優しい

バックエンドも作らないと!でもめんどくさい!
テストも書きたいとなるとコードは軽量でローカルで動くようにしたい!でもめんどくさい!
めんどくさい!けどlambdaへのデプロイとか、その後のAPI Gatewayとの統合とか、更にいうとそれらのIaC管理も楽にしたい!!でもめんどくさい!

このような色んな欲求から、今回はhonoを試してみました。めっちゃ楽しかったです!



フラッグシティパートナーズ海外不動産投資セミナー 【DMM FX】入金

Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -

Most Popular

Recent Comments