Serverless FrameworkでCustom RuntimeなLambdaをコンテナ対応する

6 min読了の目安(約6200字TECH技術記事

ServerlessがLambdaのコンテナ化に対応していたようなので早速試してみました。
今回は記事中のサンプルでは言語はCrystal、表題の通りCustom Runtimeを採用していますが他の環境でも参考になると思います。
また今回は言語としてCustom Runtimeを使用するのに必要な実装については特に解説しませんので、もし必要な場合はこちらから過去の記事をを参照していただければと思います。
いくつかの言語でのサンプルをよういしているので下記も参考にしてください。

Serverlessでコンテナを利用する方法

とりあえず動かしてみるだけなら特に難しくはありません。
下記の例(Serverlessの公式の紹介記事より拝借)のように、今までserverless.ymlで指定していたhandlerの代わりにimageという項目名でECR上にプッシュされている使用したいイメージのURIを指定するだけです。
一応注意点として、タグなどではなく必ずdigestを指定する必要があります。

functions:
  someFunction:
    image: <account>.dkr.ecr.<region>.amazonaws.com/<repository>@<digest>

つまり実践ではデプロイの前準備として「コンテナをビルドしてECRへプッシュする処理」と「プッシュしたコンテナの情報を取得する処理」が必要になるということでもあります。

コンテナをビルドしてECRへプッシュする

兎にも角にもまずECRにコンテナをプッシュしなくてはなにも始まりません。
ということでCrystalの場合のDockerfileの例を貼ってしまいます。

FROM crystallang/crystal:latest as build-image

WORKDIR /work
COPY ./ ./

RUN crystal build --link-flags -static -o bootstrap src/main.cr
RUN chmod +x bootstrap

FROM public.ecr.aws/lambda/provided:al2

COPY --from=build-image /work/ /var/runtime/

# NOTE: ここで指定した文字列がhandlerとして利用される
CMD ["hello"]

みていただければ分かる通り、そんなに難しいものではありません。
Crystalの公式イメージを利用してビルドをします。
Custom Runtimeの場合は /var/runtime/bootstrap を実行するようになっているのでビルドしたバイナリをAWSが公式で配布しているLambda用のイメージにへコピーするだけです。
ここで1点注意しなければならないことは、Lambdaに使用するイメージは必ずAWSの公式のLambda用イメージでなくてはならないということです。
この例の場合はCustom Runtime用のイメージを使用しています。

というわけでこのイメージをビルドしてプッシュするのですが、まずECRにプッシュするためのリポジトリが必要です。
aws cliを利用する場合は下記のコマンドで作成します。

$ aws ecr create-repository --repository-name <リポジトリ名> --image-scanning-configuration scanOnPush=true

そうしたら次はECRへログインします。

$ aws ecr get-login-password --region <リージョン>                                               |
$ docker login --username AWS --password-stdin <アカウントID>.dkr.ecr.<リージョン>.amazonaws.com

参考として、AWSのアカウントIDは下記のようにすると自動で取得できます。

$ aws sts get-caller-identity | jq -r .Account

ログインをしたら下記のコマンドでビルドとプッシュが出来ます。

$ docker build -t <コンテナ名> .
$ docker tag <コンテナ名>:<タグ名> <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/<コンテナ名>:<タグ名>
$ docker push <アカウントID>.dkr.ecr.ap-northeast-1.amazonaws.com/<コンテナ名>:<タグ名>

プッシュしたコンテナの情報を取得する

次に前章でプッシュしたコンテナの情報を取得し、それを使用してServerlessでLambdaのデプロイを実行します。
前提として、serverless.ymlを下記のように記述することによって、オプションから値を渡せるようにします。

custom:
  # NOTE: sls removeなどを実行するのにオプションを指定せず実行できるようにするためのもの
  defaultAccount: dummy
  defaultDigest: dummy

functions:
  hello:
    image: ${opt:account, self:custom.defaultAccount}.dkr.ecr.<リージョン>.<コンテナ名>@${opt:digest, self:custom.defaultDigest}
    events:
      - http:
          path: test
          method: get

ということでServerlessへ引き渡すための情報を取得していきます。
まず前述の通り、アカウントIDは下記のコマンドで取得出来ます。

$ aws sts get-caller-identity | jq -r .Account

そしてコンテナのdigestですが、aws-cliを利用してリポジトリの一覧を取得できるのでjqを使って使いたいイメージのタグ利用してを絞り込んでやるとdigestが取得出来ます。

$ aws ecr list-images --repository-name <コンテナ名>               |
$ jq '.imageIds[] | select(.imageTag=="<タグ名>") | .imageDigest' |
$ tr -d '"'

あとは下記のように取得した値をServerlessへ引き渡してやればデプロイ出来ます。

$ sls deploy --account <アカウントID> --digest <digest>

ここまでの一連の流れをスクリプトにまとめるとこんな感じになります。

region="ap-northeast-1"

account=$(aws sts get-caller-identity | jq -r .Account)
tag=$(uuidgen)

aws ecr get-login-password --region $region                                         |
docker login --username AWS --password-stdin $account.dkr.ecr.$region.amazonaws.com

container="lambda_container_sample"
target="$account.dkr.ecr.ap-northeast-1.amazonaws.com/$container"

docker build -t $container .
docker tag $container:$tag $target:$tag
docker push $target:$tag

digest=$(aws ecr list-images --repository-name $container | jq '.imageIds[] | select(.imageTag=="latest") | .imageDigest' | tr -d '"')

sls deploy --account $account --digest $digest

まとめ

というわけでCustom Runtimeな環境でのServerless Frameworkのコンテナ対応の紹介でした。
正直なところ触ってみた感じまだとりあえず対応しましたという感じでこれから便利使えるように改善されていくのかな?という感じですね。
少なくとも今回例に出したCrystalや公式対応してる言語ではGoなんかの可搬性の高いシングルバイナリを作れる言語の場合なんかでは今すぐ飛びついて対応する必要があるかというと微妙な感じです。
逆にスクリプト言語とかでコンテナを使って前準備とかをしたいなら使ってもいいかもですね。
先程のデプロイ用スクリプトなんかをみてもらえればわかる通り、正直そんなに難しいことはしていないので近いうちにローカルのDockerfileを自動でデプロイしてくれるようになる気はしています。

おまけ

Github Actions用のデプロイ用のworkflowsを作ったので載せておきます。
もしよかったら参考にしてください。

name: lambda_container_sample

on:
  push:
    branches: [ develop ]

jobs:
  deploy:
    name: deploy
    runs-on: ubuntu-latest
    steps:

    - name: setup node.js
      uses: actions/setup-node@v1

    - name: install sls
      run: npm i -g serverless

    - name: checkout
      uses: actions/checkout@v1

    - name: configure aws credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ap-northeast-1

    - name: login ecr
      id: login-ecr
      uses: aws-actions/amazon-ecr-login@master

    - name: build and push ecr
      env:
        ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }}
        ECR_REPOSITORY: lambda_container_sample
        IMAGE_TAG: ${{ github.sha }}
      run: |
        docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG .
        docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG

    - name: get aws account id
      id: get-aws-account
      run: |
        echo "::set-output name=ACCOUNT_ID::$(aws sts get-caller-identity | jq -r .Account)"

    - name: get ecr digest
      id: get-ecr-digest
      env:
        IMAGE_TAG: ${{ github.sha }}
      run: |
        echo "::set-output name=DIGEST::$(aws ecr list-images --repository-name lambda_container_sample | jq '.imageIds[] | select(.imageTag=="'$IMAGE_TAG'") | .imageDigest' | tr -d '"')"

    - name: deploy
      env:
        ACCOUNT_ID: ${{ steps.get-aws-account.outputs.ACCOUNT_ID }}
        DIGEST: ${{ steps.get-ecr-digest.outputs.DIGEST }}
      run: sls deploy --verbose --account $ACCOUNT_ID --digest $DIGEST