あしたのチーム Tech Blog

はたらくすべての人に「ワクワク」を届けるべく、日々奮闘するエンジニアの日常をお伝えします。

re:Invent 2020 で発表された AWS Lambda でのコンテナイメージのサポートを試してみた

こんにちは。
今年7月からあしたのチームでバックエンドリード兼SREをやっている snaka です。

今年も AWS re:Invent が開催され、多くの新しいサービスや既存サービスに対する新しい機能が発表されていますね。
毎年すごい勢いでサービスが追加・更新されるので、そのキャッチアップが大変です;

その中でも特に私が今気になっているのは、下記のブログ記事でも紹介されている AWS Lambda でのコンテナイメージのサポートです。

さっそく、上記記事を参考にコンテナイメージを利用した Lambda を試してみましたので手順を紹介します。

なお、本記事に掲載しているコードは GitHub の以下のリポジトリから取得可能ですので、参考としてください。

https://github.com/snaka/helloworld-lambda-container

Lambda 関数本体の Docker Image を用意する

まず、Lambda 関数の本体となるコードを用意します。
今回は、Ruby を使います。

bundle init を実行し Gemfile の雛形を作成します。 作成された Gemfile をエディタで編集し、に以下のように gem "prawn" の1行を追加します。

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

gem "prawn"

次に、Lambda 関数本体の実装(app.rb)です。
以下のような、シンプルなコードになります。

# app.rb
require 'base64'
require 'json'
require 'prawn'

def handler(event:, context:)
  pdf = Prawn::Document.new
  pdf.text "Hello World!"
  pdf_base64_encoded = Base64.encode64(pdf.render)

  {
    statusCode: 200,
    headers: {
      'Content-Length': pdf_base64_encoded.length,
      'Content-Type': 'application/pdf',
      'Content-disposition': 'attachment;filename=hello.pdf'
    },
    isBase64Encoded: true,
    body: pdf_base64_encoded
  }
end

Lambda で実行される関数は Lambda サービスが規定する Runtime API でやりとりを行う必要があります(下図のオレンジの点線部分)
f:id:ashita-team:20201224162825p:plain (AWS Lambda - Developer Guide より引用)

AWS からは上記 Runtime API と 関数本体の間を取り持つ Runtime Interface Client が各言語向けに提供されており、それを含む Docker Image を base image という形で Docker Hub で公開しています。

Runtime Interface Client から呼び出される関数本体は Handler と呼ばれます。
Handler は以下の規則に従って実装する必要があります。

  • 引数として event, context の2つの引数を受け取ること
    • event にはリクエストのリクエスト元からのペイロード等のデータが含まれます
    • context には実行環境に関する情報などが含まれます
  • 戻り値として JSON(※) を返却すること

※戻り値である JSON の仕様がどこかにあるはずですが、ブログ公開時点で見つけられなかったので見つけ次第追記します。

前述の関数本体を含む Docker Image を作成するための Dockerfile を作成します。

FROM amazon/aws-lambda-ruby:2.7
COPY app.rb Gemfile* ./
RUN bundle config set path 'vendor/bundle'
RUN bundle install
CMD [ "app.handler" ]

FROM 行を見たらわかるように amazon 公式の Ruby 向け base image を使っています。
それ以外は、関数本体である app.rb と Gemfile 、そして bundle install で必要なライブラリ等をインストールしています。

bundle config set path では Gem のインストール先を明示的に vendor/bundle としています。
この設定が無いと、require 時に Gem が見つけられずに実行時にエラーが出ていました。

最後の CMD 行では、{Handlerを含むファイル名(拡張子を除く)}.{Handlerの関数名} という形で Runtime Interface Client に Handler を呼び出すための情報を渡しています。

コンテナイメージをビルドし動作確認する

前節までで、コンテナイメージをビルドする準備が整いましたので、引き続きコンテナイメージのビルドを行います。

ビルドは通常どおり以下のようなコマンドで行います。

$ docker build -t hello-lambda-world .

ビルドが完了したら、ローカルでの動作確認として Lambda Runtime Emulator を起動してみます。
以下のようにビルドしたコンテナイメージを使ってコンテナを起動します。

$ docker run -p 9000:8080 hello-lambda-world:latest

コンテナを起動したのとは別にターミナルを開き、 cURL で Lambda Runtime API を叩いてみます。

$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'
{"statusCode":200,"headers":{"Content-Length":1261,"Content-Type":"application/pdf","Content-disposition":"attachment;filename=hello.pdf"},"isBase64Encoded":true,"body":"JVBERi0xLjM ... (略) ... RU9GCg==\n"}

実行の結果、上記のように JSON の文字列が返却されれば OK です。

ECR に Docker Image を登録

コンテナイメージのビルドが完了したら、Lambda サービスから利用できるように ECR リポジトリに登録します。

最初に、以下のように ECR リポジトリを作成します。(AWSコンソールから作成しても良いです)

$ aws ecr create-repository --repository-name hello-lambda-world

リポジトリ作成に成功すると実行結果にリポジトリURI(repositoryUri)が出力されます。
URIは後述のコマンドで利用するので控えておきます。

{
    "repository": {
        "repositoryArn": "arn:aws:ecr:ap-northeast-1:123456789012:repository/hello-lambda-world",
        "registryId": "123456789012",
        "repositoryName": "hello-lambda-world",
        "repositoryUri": "123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/hello-lambda-world",
        "createdAt": "2020-12-17T00:25:40+09:00",
        "imageTagMutability": "MUTABLE",
        "imageScanningConfiguration": {
            "scanOnPush": false
        },
        "encryptionConfiguration": {
            "encryptionType": "AES256"
        }
    }
}

コンテナイメージに に tag として リポジトリURI を設定します。

$ docker tag hello-lambda-world:latest 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/hello-lambda-world:latest

docker login コマンドで ECR にログインします。
AWS CLI のコマンド aws ecr get-login-password でログインに必要な一時的なパスワードを取得し、パイプで docker login コマンドに連携しています。

aws ecr get-login-password | \
docker login --username AWS --password-stdin 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com

ログインが完了したら、ECR にコンテナイメージを push します。

docker push 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/hello-lambda-world:latest

コンテナイメージの push に成功したことを確認するため、AWS コンソールで ECR リポジトリを参照してみます。 以下のように表示されていれば push 成功です。

f:id:snaka72:20201225192108p:plain

Lambda 関数を登録する

以降、手順の都合で AWS コンソールでの作業となりますが、AWS CLIAWS SAM CLI で行うことも可能です。

AWS コンソールから Lambda サービスを開き、以下のように関数の作成を行います。

f:id:snaka72:20201225192154p:plain

  • オプションから「コンテナイメージ」を選択する
  • 関数名を任意に入力する
  • 「画像を参照」(恐らく「画像」はコンテナイメージを指す)ボタンをクリックする

「コンテナイメージを選択」のダイアログが開くので以下のように ECR に push したコンテナイメージを選択し、「イメージを選択」をクリックします。
f:id:snaka72:20201225192238p:plain

あとはそのまま「関数の作成」をクリックすれば関数が作成されます。 f:id:snaka72:20201225192303p:plain

テスト実行してみる

ここまでで、作成された関数をテストしてみます。

右上のドロップダウンから「テストイベント設定」を選択します。
f:id:snaka72:20201225192337p:plain

「テストイベントの設定」ダイアログが開くので、「イベント名」に任意の名前を入力し「作成」をクリックします。
f:id:snaka72:20201225192401p:plain

作成されたイベントテンプレートをドロップダウンリストから選択し、「テスト」ボタンをクリックすると関数が呼び出されます。
f:id:snaka72:20201225192426p:plain

関数の呼び出しに成功すると、以下のように実行結果が表示されます。 f:id:snaka72:20201225192448p:plain

実行結果が「成功」となっていればテスト成功です。

Lambda 関数の入り口となる API Gateway を作成

これが最後の手順です。 Lambda 関数を HTTP 経由で呼び出せるように API Gateway を作成します。

前節で作成した Lambda 関数にトリガーを追加します。 f:id:snaka72:20201225192536p:plain

トリガーの種類は API Gateway を選択し、その下に表示されるドロップダウンリストから「API を作成する」を選択します。 f:id:snaka72:20201225192605p:plain

  • API タイプは「HTTP API
  • セキュリティは「オープン」

上記のように選択し「追加」をクリックします。

API Gateway のエンドポイント経由で Lambda 関数を実行する

前節で API Gateway を作成すると、その API エンドポイントが公開されインターネットから呼び出し可能な状態になります。

f:id:snaka72:20201225192647p:plain

上記エンドポイントをブラウザでアクセスしてみます。
以下のような PDF がダウンロードされたら成功です 🎉 🥳

f:id:snaka72:20201225192719p:plain

さいごに

AWS Lambda がコンテナイメージをサポートすることによって、既存のコードベースをほぼそのままで Lambda 実行環境で動作させることが可能になります。

これを使って、今からバッチサーバを重くする原因となっている「あんなジョブ」や「こんなジョブ」を Lambda 化して運用コストが削減できるんじゃないかと夢が広がっています。

以上、年末・年始は re:Invent 2020 の動画視聴で潰れそうな気がしてる snaka でした。

参考リンク