AWS Lambda に Ruby 関数をデプロイするために zip ファイルをつくる

ふとしです。

AWS Lambda が Ruby をサポートしてからもうだいぶん経ってしまいましたが、最近やっと試してみました。そこで、デプロイに少し手間取ったので日記にします。

問題: ネイティブエクステンション起動できない

EC2 などでの実行とは異なり、デプロイ先で bundle install するわけではないので Gem を同梱しなければなりません。そこで環境にあわせてビルドを行うネイティブエクステンションが問題になります。簡単に言うとローカルでビルドしてもデプロイ先で動きません。

例えば Nokogiri では以下のようなエラーになります。

{
    "errorMessage": "liblzma.so.5: cannot open shared object file: No such file or directory - /var/task/vendor/bundle/ruby/2.7.0/gems/nokogiri-1.10.10/lib/nokogiri/nokogiri.so",
...

対応: AWS Lambda と同等の環境で用意する

初期化からデプロイまでの包括的な仕組みを提供するフレームワークはありますが、それを使うためにディレクトリ構造をあわせたりしたくありません。

そこで、その中で使われている Docker イメージだけを流用して、AWS Lambda の環境に合わせて zip 化します。

以下のような docker-compose.yml を用意してプロジェクトトップディレクトリに配置して docker-compose up すると ruby.zip が雑にできあがります。

version: "3"
services:
  app:
    image: amazon/aws-sam-cli-build-image-ruby2.7
    volumes:
      - .:/var/task
    command:
      - /bin/bash
      - -c
      - |
        # パスで ! フラグを使用出来るようにする
        shopt -s extglob

        mkdir -p ./.deploy
        cd .deploy

        ls | grep -v -e vendor | xargs -r rm -fr
        cp -r /var/task/!(vendor) /var/task/.deploy

        bundle install --path="vendor/bundle"

        zip -qr ruby.zip * .bundle
        mv ruby.zip /var/task/ruby.zip

ローカルでテストをするために配置した vendor/bundle をコピーに含めると、すでにインストール済みあると判断してコンテナ環境にあわせたビルドは行われないので注意しましょう。

(.bundle は同梱しなくても動くはずですが、気持ち悪いので入れています)

おまけ Terraform

デプロイは Terraform で行っています。

data "aws_iam_role" "user" {
  name = "common-lambda-executor"
}

resource "aws_lambda_function" "ruby" {
  filename = "./ruby.zip"
  function_name = "foo"

  role = data.aws_iam_role.user.arn

  handler = "main.lambda_handler"
  source_code_hash = base64sha256(filesha256("./ruby.zip"))
  runtime = "ruby2.7"
  publish = false
  timeout = 600
}

resource "aws_cloudwatch_log_group" "ruby" {
  name              = "/aws/lambda/${aws_lambda_function.ruby.function_name}"
  retention_in_days = 14
}

ロールをいじれるのはなんかこわいという宗教上の理由から、ロールは Terraform でいじれるようにはしていません。

おわりに

これで Gem を同梱しなければならない Ruby 関数も気軽に使うことができるようになりました。

余談ですが、少し気になったのが Gem を使うタイプの関数のコールドスタート時の Init Duration がバイナリをデプロイするタイプの言語よりも大分長いことです。

この現象はなにも require しない関数では観測できません。

REPORT
  RequestId: 4833df14-1864-43ed-b994-09c6063f3072
  Duration: 5878.56 ms
  Billed Duration: 5879 ms
  Memory Size: 1024 MB
  Max Memory Used: 774 MB
  Init Duration: 448.42 ms

Gem の性質にもよるのでしょうが require による Init Duration の延長現象は Nodejs にもあったので、動的言語を使用する場合は少し気をつけたほうが良いのかもしれません。