lambda-web-adapterを使って、lambdaでWebサーバー(echo)をデプロイしてみる[Go,AWS]

awsでのサーバーレスでのアプリケーションの実行環境として、lambdaとecsFargateが挙げられると思います。

その中でlambdaは一つのhandler関数を実行するため、echo等のwebサーバーを実行するにはまた別の方法が必要になります。

本来であればWebサーバーをlambdaで安直にデプロイしてホスティングするのは無理なのですが、代替方法としてlambda-web-adapterというものがaws側で用意されているそうです。

今回はlambda-web-adapterを使ってechoサーバーをホスティングしてみようと思います。

# lambda-web-adapterとは
lambdaはawsのコンピューティングサービスの一つで、一つのhandler関数を作成し実行します。

動かす際にGoでは下のようにコードを書きます。
```go
func main() {
	lambda.Start(handleRequest)
}

func handleRequest(ctx context.Context, event json.RawMessage) error {
```

上のコードのように引数にevent(JSON)が指定され、それをlambdaがよしなに受け取れるようになっています。

しかし、echo等のwebサーバーフレームワーク使った場合は下のような書き方になり、入力インターフェースが多少異なってきます。

```go
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レスポンス

- lambda-web-adapterの変換の流れ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766230/8963e7c6-57e1-4908-897d-1ada0aeeee59.png)
>引用:https://aws.amazon.com/jp/builders-flash/202301/lambda-web-adapter/

このようにしてlambdaのインターフェースとwebサーバーにおけるHTTPを使ったインターフェースの相互変換の仕組みを使って、lambdaでもwebサーバーを実行できるようにしています。

# lambda-web-adapterの実装
lambda-web-adapterを使って簡単なAPIサーバーを実装してみようと思います。

## アプリケーションの実装
まず、Dockerfileを準備します。

```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サーバーをホスティングするためのコードを作成したいと思います。
```main.go
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環境の作成をします。

:::note
### ECR(Elastic Container Registry)とは

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766230/0e58e4d9-6f3c-43d8-8aa6-965d983cb11e.png)

AWSのマネージドコンテナイメージレジストリサービスで、Dockerのコンテナイメージの簡単な保存、管理、デプロイが行うことができます。
:::


```main.tf
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のリソースを作成します。

```ecr.tf
resource "aws_ecr_repository" "lambda-web-adapter" {
  name                 = "lambda-test"
  image_tag_mutability = "MUTABLE"

  image_scanning_configuration {
    scan_on_push = true
  }

}
```

ここまでできたらapplyします。

その後、イメージをawscliでpushします。
```zsh:zsh
 aws lambda update-function-code --function-name <lambda関数名名> --image-uri ${{ secrets.AWS_ACCOUNT_ID }}.dkr.ecr.ap-northeast-3.amazonaws.com/<リポジトリ名>:latest
```

ここでコンソール確認して下のようになってたらイメージの準備OKです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766230/5d4b9e97-aeb6-4a70-bae9-c720b0017b50.png)

### lambdaのリソースを作成する

ecrにpushできたらlambdaのリソースを作成します。

:::note
### Lambdaとは

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766230/e146e6bc-e528-46ae-b48a-e47e24a09b49.png)

AWSのコンピューティングサービスの一つで、アプリケーションのデプロイのためのサーバーのセットアップ(プロビジョニング)やメンテナンスが必要なく、アプリケーションの実行が行えるフルマネージドサービスです。

:::


```lambda.tf
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が帰ってきます。ホスティングはできてそうですね。
```zsh:zsh
❯ curl https://t2ff4wsmviyocn7kkqdwj6bbjq0cburc.lambda-url.ap-northeast-3.on.aws/demo
{"message":"Hello World!"}
```

# CD
今回はGithubActionsでついでにCDも組んでみます。

:::note
### CD(継続デリバリー)
あるアクションをトリガーとして自動でサーバーにホスティングする開発手法のことをCDという。
CDを行うことで迅速な開発体制を整えることができます。
:::

## 下準備

今回OIDCという仕組みを使ってAssumeRoleを行います。

:::note
### OIDCとは
OIDC(Open ID Connect)とは、認証を行うためのプロトコルで、OAuthの拡張として知られています。

OAuthは誰かに対してリソースのアクセスを許可する認可の仕組みなのに対して、OIDCはIDトークンを渡して誰がリソースにアクセスしているかを把握するための認証の仕組みです。
:::

:::note
### AssumeRoleとは
AWSではIAMロールという仕組みを使って一時的に権限を多数の人に与えることができるAssumeRoleという仕組みがあります。

AssumeRoleは、信頼ポリシーに従って、そのIAMロールをAWSリソース等から引き受けてIAMロールになりすますようなAWSの認証認可の仕組みです。
:::

### ロールを作成する。
まず、IAMロールを作成します。

:::note
### IAMとは

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766230/1a79afec-19ee-42e1-ae7f-6598b73b5dee.png)

AWS リソースへのアクセスを安全に管理するためのウェブサービスです。

IAMロールは、短期的な認証情報を作成し、複数のユーザーやリソースにアクセス権限を与えられる仕組みのことです。

:::

 ```iam.tf
//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を作成します。

```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上にリポジトリをクローンします。

https://github.com/actions/checkout

```yml
- name: checkout
  uses: actions/checkout@v4
```

次にaws-actions/configure-aws-credentialsを使ってAssumeRoleします。

https://github.com/aws-actions/configure-aws-credentials

```yml
 - 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にログインします。

https://github.com/aws-actions/amazon-ecr-login

```yml
- 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`をセットアップする。

https://github.com/docker/setup-buildx-action

```yml
- name: Set up Docker Buildx
  uses: docker/setup-buildx-action@v3.10.0
```

イメージのビルドとpushを行います。
tag付けも行っており、それを利用してイメージをecrにpushしています。

https://github.com/docker/build-push-action

```yml
- 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 
```
:::note
### provenanceフィールドとは?
docker/build-push-action時に  provenance: falseをつけないと余分だなと感じるイメージが作成されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3766230/5eb5e5b9-1088-446d-93f4-51088e736c0e.png)

この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とか)のバージョンを管理するための仕組みだそうです。
>参考:https://zenn.dev/ncdc/articles/25d03e908ce38e#%E3%83%9E%E3%83%8B%E3%83%95%E3%82%A7%E3%82%B9%E3%83%88%E4%BD%9C%E6%88%90%E3%81%AE%E6%B5%81%E3%82%8C

今回は必要ないので`provenance:false`にしています。
:::

最後にlambdaのupdate functionをawscliで実行しています。

```yml
 - 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関連の記事
https://qiita.com/maooz4426/items/20ece3bff33c3f343e9a

https://qiita.com/maooz4426/items/e513a87a5282466c47b7

https://qiita.com/maooz4426/items/fa1aa9a1a8c8c3a10701

https://qiita.com/maooz4426/items/4e70baae712c9e19300e

https://qiita.com/maooz4426/items/b01f0502a2c1cdc914c9

https://qiita.com/maooz4426/items/49aef802e3a178ed5bd6

https://qiita.com/maooz4426/items/b973490dd005e3d8e9de

https://qiita.com/maooz4426/items/f6740347eec6df320a1e

https://qiita.com/maooz4426/items/44a71129ce2f0a463817

https://qiita.com/maooz4426/items/1ab385f3aceeb660e500

https://qiita.com/maooz4426/items/422a8d6428c2504efe23

https://qiita.com/maooz4426/items/592028f297ca76830c6b

https://qiita.com/maooz4426/items/63071aba6a69c9a55ef4

https://qiita.com/maooz4426/items/e1af6b5994e0c6c067d7

https://qiita.com/maooz4426/items/575b36ded6eadfda36b0

https://qiita.com/maooz4426/items/e1933175e492883db26d

https://qiita.com/maooz4426/items/a61bbc6c32a349ce71ee

https://qiita.com/maooz4426/items/91fa49341bc2e9d8eddd

https://qiita.com/maooz4426/items/2f9fe5e8d6b6a44b3d57

https://qiita.com/maooz4426/items/17cbad84213a222df694

# GithubActions関連の記事

https://qiita.com/maooz4426/items/90021688646108e2de13

https://qiita.com/maooz4426/items/ecdfc0c4e33e76ffc432

https://qiita.com/maooz4426/items/f6740347eec6df320a1e

https://qiita.com/maooz4426/items/caf1537e469eff68e3c5

https://qiita.com/maooz4426/items/44a71129ce2f0a463817

https://qiita.com/maooz4426/items/e6ee4ed7909f7b2c0fb7

https://qiita.com/maooz4426/items/056e0fa08cd0edcee4bf

https://qiita.com/maooz4426/items/015241f9c97f5aa4f411