2
3

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】API Gateway + Hono を使って、Lambda1つでバックエンドAPIを全部作ってみた

Posted at

はじめに

バックエンドを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 していきます。

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を試してみました。めっちゃ楽しかったです!

2
3
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
2
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?