Serverless FrameworkでLambdaコンテナイメージを利用する

こちらは、[AWS LambdaとServerless Advent Calendar 2020](https://qiita.com/advent-calendar/2020/lambda) 25日目の記事になります！

クリスマスの公開ですが季節感はないです
悪しからず🦵

# はじめに

AWS re:Invent 2020にて[Lambdaのコンテナイメージのサポート](https://aws.amazon.com/jp/blogs/news/new-for-aws-lambda-container-image-support/)が発表されましたが、
我らが愛するServerless Frameworkでも早々に[サポートが行われた](https://github.com/serverless/serverless/issues/8570)ので、お試しで使ってみました🎄

`serverless.yml` における定義については、[こちら](https://www.serverless.com/blog/container-support-for-lambda)のブログにある通り、 `<account>.dkr.ecr.<region>.amazonaws.com/<repository>@<digest>` の形式で指定する必要があるそうです
タグでなく、イメージのダイジェストで指定する必要があるとのことなので、 `sls deploy` 時にいい感じに注入する必要があります🥛

# 作ったもの

以下で公開しています🐙
https://github.com/yktakaha4/lambda-docker-serverless

なるべく簡単に動作を試せるように、
環境構築からデプロイまでに必要なもろもろをGitHub Actionsに[まとめてます](https://github.com/yktakaha4/lambda-docker-serverless/blob/master/.github/workflows/deployment.yml)
変更すべき箇所はREADMEにまとめていますので併せてご覧ください

# ポイント

Dockerファイルは、 `public.ecr.aws/lambda/xxxxx` から作成する必要があるそうです
[こちら](https://github.com/aws/aws-lambda-base-images)にてベースイメージが公開されています🐋

あと、従来は `serverless.yml` にて関数のエントリーポイントを設定していたものと思いますが、Dockerfileにて設定しておく必要があるようです
個人的に、ECSのScheduled Taskで単一のコンテナに複数の関数を含めて、定義ごとに動かすものを変えるということをやっていたのですが、現状だと個別にビルドしておく必要がある感じなのでしょうか...？

```dockerfile:Dockerfile
FROM public.ecr.aws/lambda/nodejs:12 AS builder

WORKDIR /opt/build

COPY . .

RUN npm ci

RUN npm run build

##### ##### ##### #####
FROM public.ecr.aws/lambda/nodejs:12 AS runner

COPY --from=builder \
  /opt/build/package*.json \
  ./

COPY --from=builder \
  /opt/build/dist \
  ./dist

RUN npm ci --only=production

# !!! ここポイント !!!
CMD ["dist/index.handler"]
```

イメージのビルドとECRへのプッシュが済んだら、イメージのダイジェストを取得し、 `sls` コマンドに渡せるように環境変数に設定します
従来の `set-env` は現在無効となっているので、[新しい書き方](https://qiita.com/shonansurvivors/items/f256a0443d346f09448e)でやってます

あと、今回初めて知ったのですが、GitHub Actionsの環境にはデフォルトで `aws` コマンドがインストールされてるんですね...！

```yaml:deployment.yml(抜粋)
jobs:
  deploy:
    runs-on: ubuntu-18.04
    timeout-minutes: 300

    steps:
      # 略

      # ECR
      - uses: aws-actions/amazon-ecr-login@v1
        id: login-ecr

      - run: |
          docker build -t $REGISTRY/$REPOSITORY:$TAG .
          docker push $REGISTRY/$REPOSITORY:$TAG

          # !!! ここポイント !!!
          echo "IMAGE_DIGEST=$(aws ecr describe-images --repository-name $REPOSITORY --image-ids imageTag=$TAG --output text --query 'imageDetails[0].imageDigest')" >> $GITHUB_ENV
        env:
          REGISTRY: ${{ steps.login-ecr.outputs.registry }}
          REPOSITORY: lambda-docker-serverless-repos
          TAG: latest

      # Serverless Framework
      - run: npm ci

      - run: npm run deploy
        env:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          AWS_DEFAULT_REGION: ${{ secrets.AWS_DEFAULT_REGION }}
          IMAGE_DIGEST: ${{ env.IMAGE_DIGEST }}
```

`serverless.yml` では、従来 `handler` を使っていたところを、 `image` でコンテナを指定します
`#{AWS::AccountId}` など、シャープで始まるものは [serverless-pseudo-parameters]
(https://www.serverless.com/plugins/serverless-pseudo-parameters)の働きにより実値が埋め込まれます

```yml:serverless.yml
service: lambda-docker-serverless

provider:
  name: aws
  stage: prod
  region: ${env:AWS_DEFAULT_REGION}
  deploymentBucket: lambda-docker-serverless-deployment

functions:
  index:
    # !!! ここポイント !!!
    image: "#{AWS::AccountId}.dkr.ecr.#{AWS::Region}.amazonaws.com/lambda-docker-serverless-repos@${env:IMAGE_DIGEST}"
    events:
      - http:
          path: index
          method: post
          cors: true

plugins:
  - serverless-pseudo-parameters
```

今回は、こんなシンプルな関数を作ってみました
リクエストで受け取った名前を大文字にして、挨拶を返す処理になります👋

```typescript:index.ts
import { APIGatewayProxyHandler } from "aws-lambda";
import "source-map-support/register";

interface Request {
  name?: string;
}

export const handler: APIGatewayProxyHandler = async (event) => {
  try {
    const { name } = JSON.parse(event.body ?? "{}") as Request;

    return {
      statusCode: 200,
      body: JSON.stringify({
        message: `Hello, ${(name ?? "nanashi-san").toUpperCase()} !`,
      }),
    };
  } catch (e) {
    return {
      statusCode: 500,
      body: JSON.stringify({
        error: String(e),
      }),
    };
  }
};
```

`https://xxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/index` にアクセスしてみます
[Insomnia](https://insomnia.rest/)で実行してみると、ちゃんと動いてそうでいい感じです！

![insomnia.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/106259/a4b3d0a3-7574-ea1a-3761-4e362a8d12f4.png)

# おわりに

当初は、関数内で[Puppeteer](https://pptr.dev/)を動かそうと少し試していたのですが、
従来の一番安定したやり方だった[chrome-aws-lambda](https://github.com/alixaxel/chrome-aws-lambda)を使うよりも楽に構築できると思いきや、ライブラリの不足やディレクトリの権限周りの問題で結構留意することが多かったので諦めました...
（ちなその残骸は[こちら](https://github.com/yktakaha4/lambda-docker-puppeteer)にあります⚰️）

従来実行環境の微妙な差異に悩まされることもちょいちょいあったように思いますが、
今後コンテナベースの環境で開発ができるようになると利便性が上がるので、引き続き動向を追っていきたいですね🦌🦌🎅🎁