Closed18
HonoのコンテナイメージをLambdaにデプロイしてみる

プロジェクト作成
npm create hono@latest hono-lambda

Terraformの初期設定
mkdir terraform
cd terraform
versions.tf
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.19.0"
}
}
required_version = "~> 1.0"
}
locals.tf
# AWS
locals {
aws_region = "ap-northeast-1"
}
locals {
github_repo = "hono-lambda"
}
config.tf
provider "aws" {
region = local.aws_region
default_tags {
tags = {
Terraform = "hono-lambda"
Environment = terraform.workspace
}
}
}
main.tf
data "aws_caller_identity" "self" {}
# S3
module "terraform_state_s3_bucket" {
source = "terraform-aws-modules/s3-bucket/aws"
version = "~> 3.0"
bucket = "tf-state-${local.github_repo}"
attach_deny_insecure_transport_policy = true
attach_require_latest_tls_policy = true
acl = "private"
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
control_object_ownership = true
object_ownership = "BucketOwnerPreferred"
versioning = {
status = true
mfa_delete = false
}
server_side_encryption_configuration = {
rule = {
apply_server_side_encryption_by_default = {
sse_algorithm = "AES256"
}
}
}
}
上記コードで一旦、State管理用のS3バケットを作成する
terraform init
terraform plan
terraform apply
その後
backend.tf
terraform {
backend "s3" {
bucket = "tf-state-hono-lambda"
key = "terraform.tfstate"
}
}
を作成したのち、
terraform init
でバックエンドを設定

ECRの作成
Repositoryを作り、Policyをつけておく
main.tf
...
resource "aws_ecr_repository" "hono_lambda" {
name = local.github_repo
}
resource "aws_ecr_lifecycle_policy" "hono_lambda" {
repository = aws_ecr_repository.hono_lambda.name
policy = <<EOF
{
"rules": [
{
"rulePriority": 10,
"description": "Expire images count more than 15",
"selection": {
"tagStatus": "any",
"countType": "imageCountMoreThan",
"countNumber": 15
},
"action": {
"type": "expire"
}
}
]
}
EOF
}
terraform plan
terraform apply

GitHub Actions用のIAMを作成する
OIDCの設定とかもする
main.tf
data "aws_iam_openid_connect_provider" "github" {
url = "https://token.actions.githubusercontent.com"
}
data "tls_certificate" "github_actions_oidc_provider" {
url = "https://token.actions.githubusercontent.com/.well-known/openid-configuration"
}
resource "aws_iam_role" "github_actions" {
name = "github-actions-${local.github_repo}"
assume_role_policy = data.aws_iam_policy_document.github_actions_assume_role.json
}
resource "aws_iam_policy" "github_actions_attachment_policy" {
name = "github-actions-${local.github_repo}-attachment-policy"
policy = data.aws_iam_policy_document.github_actions_attachment_policy.json
}
data "aws_iam_policy_document" "github_actions_assume_role" {
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [data.aws_iam_openid_connect_provider.github.arn]
}
condition {
test = "StringLike"
variable = "token.actions.githubusercontent.com:sub"
values = ["repo:${local.github_org}/${local.github_repo}:*"]
}
}
}
data "aws_iam_policy_document" "github_actions_attachment_policy" {
statement {
actions = [
"ecr:*",
"lambda:*",
]
resources = ["*"]
}
}
resource "aws_iam_role_policy_attachment" "github_actions" {
role = aws_iam_role.github_actions.name
policy_arn = aws_iam_policy.github_actions_attachment_policy.arn
}

仮のLambdaを作っておく
後で、GitHub Actions側でupdate-function-code
するが、一旦functionsがないとなので作っておく
main.tf
...
data "aws_iam_policy_document" "github_actions_assume_role" {
statement {
actions = ["sts:AssumeRoleWithWebIdentity"]
principals {
type = "Federated"
identifiers = [data.aws_iam_openid_connect_provider.github.arn]
}
condition {
test = "StringLike"
variable = "token.actions.githubusercontent.com:sub"
values = ["repo:${local.github_org}/${local.github_repo}:*"]
}
}
}
data "aws_iam_policy_document" "github_actions_attachment_policy" {
statement {
actions = [
"ecr:*",
"lambda:*",
]
resources = ["*"]
}
}
resource "aws_iam_role" "hono_lambda" {
name = "${local.github_repo}-lambda-role"
assume_role_policy = data.aws_iam_policy_document.hono_lambda_principals.json
}
data "aws_iam_policy_document" "hono_lambda_principals" {
statement {
actions = ["sts:AssumeRole"]
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
}
}
resource "aws_iam_role_policy" "hono_lambda_policy" {
name = "${local.github_repo}-lambda-policy"
role = aws_iam_role.hono_lambda.id
policy = data.aws_iam_policy_document.hono_lambda_policy.json
}
data "aws_iam_policy_document" "hono_lambda_policy" {
statement {
actions = [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
]
resources = ["*"]
}
}
resource "aws_lambda_function" "hono_lambda" {
function_name = local.github_repo
package_type = "Image"
image_uri = local.dummy_ecr_image
role = aws_iam_role.hono_lambda.arn
timeout = 300
memory_size = 512
publish = true
environment {
variables = {
}
}
lifecycle {
ignore_changes = [image_uri]
}
}
terraform plan
terraform apply

local.dummy_ecr_image
を設定しておいた(別でECRのリソースは作ってある)

GitHub ActionsでECRへのpushとLambdaへのデプロイを行う
Dockerfile
FROM node:20-alpine AS base
FROM base AS deps
WORKDIR /app
RUN npm install -g pnpm
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
FROM base AS runner
WORKDIR /app
RUN npm install -g pnpm
COPY /app/node_modules ./node_modules
COPY . .
CMD pnpm run start
.github/workflows/build-deploy.yaml
name: Build and Deploy
on:
push:
branches:
- main
jobs:
build-and-deploy:
name: Build and Deploy
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- uses: aws-actions/configure-aws-credentials@v4
with:
aws-region: ap-northeast-1
role-to-assume: ${{ secrets.TERRAFORM_AWS_IAM_ARN }}
- uses: aws-actions/amazon-ecr-login@v1
id: login-ecr
with:
mask-password: "true"
- name: Login Docker
uses: docker/login-action@v3
with:
registry: ${{ steps.login-ecr.outputs.registry }}
- uses: docker/build-push-action@v5
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
REPOSITORY: hono-lambda
IMAGE_TAG: latest
with:
context: .
file: ./Dockerfile
push: true
provenance: false
tags: ${{ env.REGISTRY }}/${{ env.REPOSITORY }}:${{ env.IMAGE_TAG }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Update Lambda
env:
REGISTRY: ${{ steps.login-ecr.outputs.registry }}
REPOSITORY: hono-lambda
IMAGE_TAG: latest
run: |
aws lambda update-function-code --function-name hono-lambda --image-uri ${{ env.REGISTRY }}/${{ env.REPOSITORY }}:${{ env.IMAGE_TAG }}

上記でデプロイまで完了

HonoをLambdaでも動くように変更
Lambdaは特有のcontextやeventを持っており、それらを引数として受け取る関数を定義する必要がある
export const handler = async (event, context) => {
...
}
一方、Hono含め色々なWebアプリケーションはもちろんこのような形式でexportされたりはしていないため、間に処理を噛ませる必要がある。
AWS Lambda Web Adapterというものが用意されているので、これを利用する
Dockerfileに直接書いてしまうと使い回ししづらくなってしまうので、GitHub Actions内でDockerfileに挿入したのちビルドするように書き換える
.github/workflows/build-deploy.yaml
...
- name: Login Docker
uses: docker/login-action@v3
with:
registry: ${{ steps.login-ecr.outputs.registry }}
- name: Lambda-ify
run: |
echo "COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.7.1 /lambda-adapter /opt/extensions/lambda-adapter" >> Dockerfile
echo "ENV PORT=3000" >> Dockerfile
- uses: docker/build-push-action@v5
...

一行、追加するのと、実行ポートを指定する必要があるので、Honoのデフォルト立ち上げポートである3000番ポートをDockerfileに追加する

そして、AWSコンソール上でテストを実行すればちゃんと返ってくる
{
"statusCode": 200,
"headers": {
"content-type": "text/plain;charset=UTF-8",
"content-length": "11",
"date": "Fri, 13 Oct 2023 07:04:31 GMT",
"connection": "keep-alive",
"keep-alive": "timeout=5"
},
"multiValueHeaders": {
"content-type": [
"text/plain;charset=UTF-8"
],
"content-length": [
"11"
],
"date": [
"Fri, 13 Oct 2023 07:04:31 GMT"
],
"connection": [
"keep-alive"
],
"keep-alive": [
"timeout=5"
]
},
"body": "Hello Hono!",
"isBase64Encoded": false
}

あとはAPI Gateway経由にするなり、複雑な認可がいらないのであれば、Lambda function urlを発行するなりすればホスティング完了
このスクラップは2023/10/13にクローズされました