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


こちらは、AWS LambdaとServerless Advent Calendar 2020 25日目の記事になります!

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

はじめに

AWS re:Invent 2020にてLambdaのコンテナイメージのサポートが発表されましたが、
我らが愛するServerless Frameworkでも早々にサポートが行われたので、お試しで使ってみました🎄

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

作ったもの

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

なるべく簡単に動作を試せるように、
環境構築からデプロイまでに必要なもろもろをGitHub Actionsにまとめてます
変更すべき箇所はREADMEにまとめていますので併せてご覧ください

ポイント

Dockerファイルは、 public.ecr.aws/lambda/xxxxx から作成する必要があるそうです
こちらにてベースイメージが公開されています🐋

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

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 は現在無効となっているので、新しい書き方でやってます

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

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の働きにより実値が埋め込まれます

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

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

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で実行してみると、ちゃんと動いてそうでいい感じです!

おわりに

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

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