Closed18

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

kobokobo

プロジェクト作成

npm create hono@latest hono-lambda
kobokobo

Lambdaも選択肢で出てくるが、これを選択してしまうとCDKによるデプロイになってしまい、コンテナでのデプロイでなくなってしまうので、Nodeを選択

kobokobo

package managerは何でも良さそうなので、pnpm installしておく

kobokobo

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

でバックエンドを設定

kobokobo

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
kobokobo

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
}
kobokobo

Actionsに与える権限はもう少し弱くても良さそうだけど、一旦(どうせ後で消すので)

kobokobo
outputs.tf
output "GITHUB_ACTIONS_IAM_ROLE" {
  value = aws_iam_role.github_actions.arn
}

を用意しておくことでarnがapply後に見れるので良い

kobokobo

出力されたARNをGitHub Actionsのsecretsとして設定しておく

kobokobo

仮の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
kobokobo

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

kobokobo

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 --from=deps /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 }}
kobokobo

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
...
kobokobo

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

kobokobo

そして、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
}
kobokobo

あとはAPI Gateway経由にするなり、複雑な認可がいらないのであれば、Lambda function urlを発行するなりすればホスティング完了

このスクラップは2023/10/13にクローズされました