リモート開発メインのソフトウェア開発企業のエンジニアブログです

Docker による AWS Lambda 関数コンテナイメージ化のすすめ

皆さん 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 を指定すれば完了です。

← 前の投稿

PHPの静的メソッド呼び出しと利用について

次の投稿 →

Pythonを使ったSlackの投稿方法およびProxyについて

コメントを残す