皆さん Lambda 関数は書いてますか?? Lambda と言えば現在では多数の言語でのランタイムをサポートしていますが、その中でもコンテナイメージを使った開発が個人的には楽だと感じていて日々使っているので紹介していきます。
Lambda のコンテナイメージ自体は 2018 年ぐらいからサポートされているので目新しい機能ではないのですが皆さん活用していますでしょうか?
コンテナイメージを使った Lambda 関数は ECR にイメージを Push して使います。開発時から同じイメージを使えるので、デバッグも容易に行う事ができます。
今回は Docker を使ったコンテナイメージ版 Lambda 関数の開発手順についてまとめますが、 Lambda や Docker を使った事がある前提で書くので細かい部分の説明は省略します。
準備
今回は、 Ruby ランタイムのベースイメージを使う事にします。このベースイメージ自体が、カスタムランタイムを使っているだけなので自身でカスタムランタイムの要件を満たす様にイメージを構築さえすればなんの言語でも開発する事が可能ですが、 Ruby 等の言語は従来型のランタイム同様最初からベースイメージが用意されているので簡単です。
基本的には公式ドキュメントに沿って書いていきますが、執筆時点 (2021 年 12 月) より内容が古くなっている可能性があるので、新しく作る場合はドキュメントの方を一読する事をおすすめします。
Docker イメージの作成
まずは核となるイメージを作ります。適当なワーキングディレクトリに以下の Dockerfile
を作りましょう:
FROM public.ecr.aws/lambda/ruby:2.7
# Copy function code
COPY app.rb ${LAMBDA_TASK_ROOT}
# Copy dependency management file
#COPY Gemfile ${LAMBDA_TASK_ROOT}
# Install dependencies under LAMBDA_TASK_ROOT
#ENV GEM_HOME=${LAMBDA_TASK_ROOT}
#RUN bundle install
# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
CMD [ "app.LambdaFunction::Handler.process" ]
一旦、 Gem は使わないのでその辺の行はコメントアウトしています。不要であれば削除してしまって構いません。
最後の CMD
に書かれている引数ですが、 Lambda 関数のハンドラを表していて、 {拡張子を抜いたファイル名}.{Ruby クラス名}.{メソッド名}
と言う形式になっています。
この例でいうと、スクリプトの名前が app.rb なので {拡張子を抜いたファイル名}
は app
となります。
さて、続いて app.rb を作っていきます:
module LambdaFunctions
class Handler
def self.process(event:, context:)
{ "name": event["name"] || "(unknown)" }
end
end
end
これで完了です。それではイメージを作っていきましょう:
$ docker build -t docker-lambda-ruby .
イメージの作成に成功したら次はコンテナを起動してみます:
$ docker run -it --rm -p 8080:8080 docker-lambda-ruby
カスタムランタイムはポート番号 8080 で Listen するので、ホストマシンに同じポートを割り当てています。これはお使いの環境に合わせて変更して下さい。
また、ワーキングディレクトリの中身をそのまま /var/task
にマウントしています。後述するデバッグの為です。
では実際に Lambda 関数を実行してみましょう。先程のポートに curl で HTTP アクセスする事で実行する事ができます:
$ curl -XPOST "http://localhost:8080/2015-03-31/functions/function/invocations" -d '{ "name": "issei-m" }'
{"name":"issei-m"}
結果が出力されました。簡単ですね。
効率の良い開発の方法
コンテナを起動する時、次の様にすると少しだけ効率よく作業が可能です:
$ docker run -d --name docker-lambda-ruby --rm -p 8080:8080 -v $(pwd):/var/task --entrypoint '' docker-lambda-ruby tail -f /dev/null
コンテナがバックグラウンドで動作しますが、 Lambda のランタイムは動かしていません。また、ワーキングディレクトリを /var/task
にボリュームマウントしています。
実際にカスタムランタイムを動作させたい時は、次のコマンドを実行します:
$ docker exec -it docker-lambda-ruby /lambda-entrypoint.sh app.LambdaFunctions::Handler.process
これでコンテナ上でカスタムランタイムを起動できますが、 init プロセスで起動していないのでこのプロセスを落としてもコンテナは終了しません。なのでスクリプトを更新したら、 Ctrl-C
とかでカスタムランタイムを落とした後、再度同じコマンドで起動し直せば簡単にスクリプトをリロードする事ができます。
Docker Compose
ここでは詳しくは書きませんが、 Docker Compose を使って他にも必要なリソースを開発中に使う事ができます。例えば LocalStack とかを立てておくと、 S3 のモックが作れるので開発中には大変便利です。(気が向いたらこの辺りは今度書こうと思います)
ユニットテスト
テストも簡単に書けます。以下の通り app_sepc.rb を追加してみましょう:
require_relative 'app.rb'
RSpec.describe :app do
describe LambdaFunctions::Handler do
describe :process do
it 'should return JSON containing the name fetched from the event' do
result = LambdaFunctions::Handler.process(event: { 'name' => 'issei' }, context: nil)
expect(result).to eq({ 'name': 'issei' })
end
end
end
end
続いて RSpec をインストールします:
$ docker exec -it docker-lambda-ruby gem install rspec
※RSpec は Lambda 関数の実行に関係がないので Dockerfile には含めません。
実行してみます:
$ docker exec -it docker-lambda-ruby rspec /var/task/app_spec.rb
.
Finished in 0.00266 seconds (files took 0.11013 seconds to load)
1 example, 0 failures
デプロイ
デプロイ自体は殆ど普通の Lambda 関数と同様なので割愛します。 1 点だけ、事前にイメージを格納する ECR リポジトリを作る必要がある点だけ異なります。
開発が終わったら先程と同様の手順で docker build
でイメージを作り、 ECR リポジトリのタグをつけて docker push
します。
Lambda 関数では Push したイメージの URL を指定すれば完了です。