awsでのサーバーレスでのアプリケーションの実行環境として、lambdaとecsFargateが挙げられると思います。
その中でlambdaは一つのhandler関数を実行するため、echo等のwebサーバーを実行するにはまた別の方法が必要になります。
本来であればWebサーバーをlambdaで安直にデプロイしてホスティングするのは無理なのですが、代替方法としてlambda-web-adapterというものがaws側で用意されているそうです。
今回はlambda-web-adapterを使ってechoサーバーをホスティングしてみようと思います。
lambda-web-adapterとは
lambdaはawsのコンピューティングサービスの一つで、一つのhandler関数を作成し実行します。
動かす際にGoでは下のようにコードを書きます。
func main() {
lambda.Start(handleRequest)
}
func handleRequest(ctx context.Context, event json.RawMessage) error {
上のコードのように引数にevent(JSON)が指定され、それをlambdaがよしなに受け取れるようになっています。
しかし、echo等のwebサーバーフレームワーク使った場合は下のような書き方になり、入力インターフェースが多少異なってきます。
func main() {
e := echo.New()
e.GET("/",handleRequest)
e.Logger.Fatal(e.Start(":8000"))
}
これを解決する仕組みとしてlambda-web-adapterが存在します。
lambda-web-adapterはWebAdapterと呼ばれる箇所でlambdaとwebサーバーが扱えるようなデータ形式に相互変換を行っています。
例えば、リクエスト時にはlambdaでイベントとして扱われているリクエストをHTTPリクエストに変換します。
レスポンス時には、webサーバーでHTTPレスポンスとして処理された内容をlambdaレスポンスとして変換します。
- 変換表
ケース | 変換元 | 変換先 |
---|---|---|
リクエスト | イベント | HTTPリクエスト |
レスポンス | HTTPレスポンス | lambdaレスポンス |
引用:https://aws.amazon.com/jp/builders-flash/202301/lambda-web-adapter/
このようにしてlambdaのインターフェースとwebサーバーにおけるHTTPを使ったインターフェースの相互変換の仕組みを使って、lambdaでもwebサーバーを実行できるようにしています。
lambda-web-adapterの実装
lambda-web-adapterを使って簡単なAPIサーバーを実装してみようと思います。
アプリケーションの実装
まず、Dockerfileを準備します。
# build_baseというステージ名をつける
FROM golang:1.24-alpine AS build_base
# 依存関係の解消のためにgitを入れてるのかなと
RUN apk add --no-cache git
# 作業ディレクトリの指定
WORKDIR /tmp/echo
# コードをコンテナ内にコピー
COPY . .
# 依存関係をダウンロード
RUN go mod download
# ビルド
# GOOSでOSをlinuxに指定
# CGO_ENABLED=0でCのコード呼び出すためのツール(CGO)をオフにする(ただマルチステージビルド時はデフォルトでオフらしい?
RUN GOOS=linux CGO_ENABLED=0 go build -o bootstrap .
# 実行環境を指定する
FROM alpine:3.9
# HTTPS通信するための証明書を取得
RUN apk add ca-certificates
# webadapterを取得
COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.9.0 /lambda-adapter /opt/extensions/lambda-adapter
# アプリケーションのコピー
COPY --from=build_base /tmp/echo/bootstrap /app/bootstrap
# アプリケーションの使用するポートを指定
ENV PORT=8000
EXPOSE 8000
# アプリケーションの起動コマンド
CMD ["/app/bootstrap"]
このDockerfileは、aws公式が別の有名なwebフレームワークであるginのexampleリポジトリを作ってくれてるので、そこからコードをecho用に多少いじりました。
https://github.com/awslabs/aws-lambda-web-adapter/tree/main/examples/gin
まず、マルチステージビルドをを使ってgoのアプリケーションをビルド、そこからlinuxディストリビューションであるalpineイメージにビルドしたファイルを移して実行するようにしています。
次にechoサーバーをホスティングするためのコードを作成したいと思います。
package main
import (
"net/http"
"github.com/labstack/echo/v4"
)
func main() {
e := echo.New()
e.GET("/demo", func(c echo.Context) error {
return c.JSON(http.StatusOK, map[string]string{
"message": "Hello World!",
})
})
e.Logger.Fatal(e.Start(":8000"))
}
main.goは簡単にAPIを叩いたらJSONが返ってくるようにしてみました。
terraform
今回はAWS環境をterraformで構築してみました。
ただ、lambdaでのコンテナデプロイのために下記の流れで作成する必要があります。
ecrのリソースを作成
↓
イメージをpush
↓
lambda等のリソースを作成
イメージを先に配置しないとlambdaがイメージのあるecrのリポジトリを参照できないので、まずecrのリソースから作成します。
ecrにpush
ecrのpushをする前にterraform環境の作成をします。
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 5.0"
}
}
}
provider "aws" {
region = "ap-northeast-3"
profile = "admin-tf"
}
まずmain.tfでプロバイダーをawsに指定します。
この状態でinitコマンドを使用して必要なモジュールを準備します。
initコマンドを打ってからecrのリソースを作成します。
resource "aws_ecr_repository" "lambda-web-adapter" {
name = "lambda-test"
image_tag_mutability = "MUTABLE"
image_scanning_configuration {
scan_on_push = true
}
}
ここまでできたらapplyします。
その後、イメージをawscliでpushします。
aws lambda update-function-code --function-name <lambda関数名名> --image-uri ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-3.amazonaws.com/<リポジトリ名>:latest
ここでコンソール確認して下のようになってたらイメージの準備OKです。
lambdaのリソースを作成する
ecrにpushできたらlambdaのリソースを作成します。
resource "aws_lambda_function" "lambda-web-adapter" {
function_name = "lambda-web-adapter" //関数名
image_uri = "${aws_ecr_repository.lambda-web-adapter.repository_url}:latest" //imageがないとここが参照できない
role = aws_iam_role.iam_for_lambda.arn
package_type = "Image" //コンテナ使用時はここを指定
architectures = ["arm64"] //macでビルドしてるのでここ書いておいた
}
//関数URLで簡単に検証するためにリソース作ってます。
resource "aws_lambda_function_url" "test_latest" {
function_name = aws_lambda_function.lambda-web-adapter.function_name
authorization_type = "NONE"
}
resource "aws_iam_role" "iam_for_lambda" {
name = "iam_for_lambda"
assume_role_policy = data.aws_iam_policy_document.assume_role.json
}
//dataブロックでassumeロールしてCloudwatch等のリソースと連携できるようにポリシーを指定
data "aws_iam_policy_document" "assume_role" {
statement {
effect = "Allow"
principals {
type = "Service"
identifiers = ["lambda.amazonaws.com"]
}
actions = ["sts:AssumeRole"]
}
}
これでapplyしてデプロイします。
これでcurlコマンドを使ってgetします。
すると下のようにjsonが帰ってきます。ホスティングはできてそうですね。
❯ curl https://t2ff4wsmviyocn7kkqdwj6bbjq0cburc.lambda-url.ap-northeast-3.on.aws/demo
{"message":"Hello World!"}
CD
今回はGithubActionsでついでにCDも組んでみます。
下準備
今回OIDCという仕組みを使ってAssumeRoleを行います。
ロールを作成する。
まず、IAMロールを作成します。
//OIDCプロバイダを作成する
resource "aws_iam_openid_connect_provider" "token_actions_github" {
url = "https://token.actions.githubusercontent.com"
client_id_list = [
"sts.amazonaws.com",
]
}
//iamロールを作成する。
resource "aws_iam_role" "from_github_lambda_web_adapter_test" {
name = "from_github_lambda_web_adapter_test"
//信頼ポリシーを作成する。
assume_role_policy = jsonencode({
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
//ロールを実行できる相手を指定
"Principal": {
"Federated": aws_iam_openid_connect_provider.token_actions_github.arn
},
"Action": "sts:AssumeRoleWithWebIdentity",
//ロールの実行タイミング
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com"
},
//Githubリポジトリを指定、ブランチは指定しないようにしてる
"StringLike": {
"token.actions.githubusercontent.com:sub": "repo:maooz4426/lambda-web-adapter-test:ref:refs/heads/*"
}
}
}
]
})
}
//許可ポリシーを定義します。
resource "aws_iam_role_policy" "lambda_web_adapter_test" {
name = "lambda_web_adapter_test"
role = aws_iam_role.from_github_lambda_web_adapter_test.id
policy = jsonencode({
"Version": "2012-10-17",
//許可するアクションを指定
"Statement": [
//後述するaws-actions/amazon-ecr-loginでTokenを渡せるように
{
"Effect": "Allow",
"Action": "ecr:GetAuthorizationToken",
"Resource": "*"
},
//ecrでの操作の許可
{
"Effect": "Allow",
"Action": [
"ecr:UploadLayerPart",
"ecr:PutImage",
"ecr:InitiateLayerUpload",
"ecr:CompleteLayerUpload",
"ecr:BatchGetImage", //これをつけることでactionsでImageを扱える
"ecr:BatchCheckLayerAvailability"
],
"Resource": aws_ecr_repository.lambda-web-adapter.arn
},
//lambdaのupdateを許可
{
"Effect": "Allow",
"Action": "lambda:UpdateFunctionCode",
"Resource": "*"
}
]
})
}
JSONで書いている内容はdataブロックとjsonencodeの2種類存在します。
個人的にはjsonencodeはAWSドキュメントに書かれていることが多くコピペで流用しやすいので、jsonencodeを使用します。
このタイミングでterraform apply
して、リソースを作成します。
実際に組んでいく
実際にCDの仕組みをGithubActionsで組んでいきます。
ローカルリポジトリの.github/workflows
配下にdeploy.ymlを作成します。
name: deploy
//mainブランチにpushされたら起動
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
permissions:
id-token: write //OIDCの仕組みではここら辺のpermissionが必要
contents: read
env:
REGION: ap-northeast-3
steps:
- name: checkout
uses: actions/checkout@v4
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/from_github_lambda_web_adapter_test
aws-region: ${{ env.REGION }}
- name: Login to Amazon ECR private
id: login-ecr-private
uses: aws-actions/amazon-ecr-login@v2.0.1
env:
AWS_DEFAULT_REGION: ap-northeast-3
AWS_REGION: ${{ env.REGION }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.10.0
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
platforms: linux/arm64
provenance: false
tags: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-3.amazonaws.com/lambda-test:latest
- name: update for lambda
run: |
aws lambda update-function-code --function-name lambda-web-adapter --image-uri ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-3.amazonaws.com/lambda-test:latest
checkoutしてactions上にリポジトリをクローンします。
- name: checkout
uses: actions/checkout@v4
次にaws-actions/configure-aws-credentialsを使ってAssumeRoleします。
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/from_github_lambda_web_adapter_test
aws-region: ${{ env.REGION }}
次にECRのプライベートリポジトリを使うためにECRにログインします。
- name: Login to Amazon ECR private
id: login-ecr-private
uses: aws-actions/amazon-ecr-login@v2.0.1
env:
AWS_DEFAULT_REGION: ap-northeast-3
AWS_REGION: ${{ env.REGION }}
次にイメージをビルドするためにbuildx
をセットアップする。
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.10.0
イメージのビルドとpushを行います。
tag付けも行っており、それを利用してイメージをecrにpushしています。
- name: Build and push
uses: docker/build-push-action@v6
with:
push: true
platforms: linux/arm64
provenance: false
tags: ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast3.amazonaws.com/lambda-test:latest
provenanceフィールドとは?
docker/build-push-action時に provenance: falseをつけないと余分だなと感じるイメージが作成されます。
この0.01MBのはSLSA Provenanceというものらしく
ソフトウェアのサプライチェーン(開発からビルドまでの過程)において外部からの攻撃がされていないという完全性を把握するためのセキュリティの仕組みだそうです。
参考:https://chroju.dev/blog/docker_buildx_slsa_provenance
https://donbulinux.hatenablog.jp/entry/2023/08/03/195648
Image Indexはmanifestに関連してるものらしく、別プラットフォーム(armとかamdとか)のバージョンを管理するための仕組みだそうです。
今回は必要ないのでprovenance:false
にしています。
最後にlambdaのupdate functionをawscliで実行しています。
- name: update for lambda
run: |
aws lambda update-function-code --function-name lambda-web-adapter --image-uri ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-3.amazonaws.com/lambda-test:latest
最後に
今回はlambda-web-adapterを利用して、lambdaにechoのwebサーバーをホスティングしてみました。
これを利用することでlambdaを使って簡単にwebサーバーをホスティングすることができると思います。
lambdaは無料利用枠があり、100万リクエストまで無料です。
これを利用して学生はハッカソン等で利用して欲しいと思います。
しかし、lambdaにはボトルネックがいくつか存在します。
15分の起動制限、同時実行数が1000回n存在するので、サーバーレスかつ安定的な運用をしたいのであればECSonFargate
の方がいいと思います。
なので、脳死でlambdaを採用したら、安定的な運用ができるとも限りません。
ここら辺もまた記事でまとめていきたいなと思います。
Go関連の記事
GithubActions関連の記事