Serverless FrameworkでGemをLayersに切り出す


環境


  • Mac OS X 10.14.3

  • Serverless Framework 1.36.1

  • Ruby 2.5


はじめに

Serverless FrameworkでRubyを使用する場合にパッケージ容量をもっとも圧迫するvendor(gemの内包ディレクトリ)を切り出す方法について書きます。


こちらの記事を読んで出来るようになること


  • Serverless Framework x RubyでGemをLayerに切り出す

  • Layersのバージョンアップにも対応可能


Lambda Layersとは

ざっくりと、各Lambda関数で共通化した処理をLambda関数の中に内包するのではなく、外に切り出してしまう仕組みのこと。


1. サンプルアプリケーションの作成


この記事ではserverless frameworkの使い方などは書きません


以下のコマンドでサンプルアプリケーションを作成します。

$ sls create -t aws-ruby -p sample

上記を実行すると、以下のようなディレクトリが作成されます。

sample/

- serverless.yml
- handler.rb


2. bundle init & gem install

$ cd sample

上記で、sampleディレクトリに移動しておきます。

sampleディレクトリ内で、以下を実行していきます。

$ bundle init

上記で、Gemfileが作成されるので、エディタでGemfileを編集していきます。

# frozen_string_literal: true

source "https://rubygems.org"

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

# 以下を追加
gem "business_time"

適当なGemをGemfileに追加して、以下を実行します。

$ bundle install --path vendor/bundle


3. serverless.ymlを編集

service: sample

provider:
name: aws
runtime: ruby2.5
region: ap-northeast-1
role: arn:aws:iam::ロールのarn

package:
exclude:
- vendor/**
- Gemfile
- Gemfile.lock

functions:
hello:
handler: handler.hello

layers:
gems:
path: vendor

この時点で一度デプロイします。

$ sls deploy

デプロイを実行すると、layersが作成されるので、再度serverless.ymlを編集します。

同一のserverless.yml内であれば、REFで参照をすることが出来ます。

公式ドキュメント

service: sample

provider:
name: aws
runtime: ruby2.5
region: ap-northeast-1
role: arn:aws:iam::ロールのarn

package:
exclude:
- vendor/**
- Gemfile
- Gemfile.lock

functions:
hello:
handler: handler.hello
layers:
- {Ref: GemsLambdaLayer}

layers:
gems:
path: vendor

functionlayersを追加して、Refで参照させるのですが、この時に参照方法のルールとして、Layer名をTitleCaseで記載し、LambdaLayerを末尾に結合させる必要があります。

今回のLayer名はgemsなので、GemsLambdaLayerとなっています。

Layer名がhogeだったら、HogeLambdaLayerとなるかと思います。

これで再度デプロイします。

$ sls deploy

これでAWSのLambda関数ページを見ても、まだLayerが反映されません。

どこが問題かと言うと、serverless.ymlには記載する順番を気をつける必要があるようで、以下のように、functionsより上にlayersを記載する必要があります。

上記を修正したserverless.ymlが以下

service: sample

provider:
name: aws
runtime: ruby2.5
region: ap-northeast-1
role: arn:aws:iam::ロールのARN

package:
exclude:
- vendor/**
- Gemfile
- Gemfile.lock

layers:
gems:
path: vendor

functions:
hello:
handler: handler.hello
layers:
- {Ref: GemsLambdaLayer}

これで再度デプロイすることでLambda関数にLayerが適用されます。


4. Lambda関数でgemを使用する

handler.rbを以下のようにします

require 'json'

require 'business_time'

def hello(event:, context:)
puts Date.today.workday?

{ statusCode: 200, body: JSON.generate('Go Serverless v1.0! Your function executed successfully!') }
end

image.png

これで、適当にテストを作成し、「テスト」ボタンを押下すると、以下のようなエラーが発生します。

{

"errorMessage": "cannot load such file -- business_time",
"errorType": "Init<LoadError>",
"stackTrace": [
"/var/lang/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'",
"/var/lang/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'",
"/var/task/handler.rb:2:in `<top (required)>'",
"/var/lang/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'",
"/var/lang/lib/ruby/2.5.0/rubygems/core_ext/kernel_require.rb:59:in `require'"
]
}

bussiness_timeを参照出来なくなっています。

Lambda Layersを使用する際、格納したファイルやディレクトリは、/optディレクトリに格納される仕様になっています。なので、こちらの対応が必要になります。

対応方法は以下の二つです。

* gemの参照先を絶対パスで記述する

* LOAD_PATHを変更する

後者で対応することにしました。

以下のように、handler.rbを変更します。

load_path = Dir["/opt/bundle/ruby/2.5.0/gems/**/lib"]

$LOAD_PATH.unshift(*load_path)

require 'json'
require 'business_time'

def hello(event:, context:)
puts Date.today.workday?

{ statusCode: 200, body: JSON.generate('Go Serverless v1.0! Your function executed successfully!') }
end

ちなみに、/opt/bundle/ruby/2.5.0/gems/**/libのパスはlayersの作成方法によって変わってしまうため、適宜変更してください。

これでテストを実行すると、200が返却され、ログにtrueが返ってくるかと思います。


5. sls invokeを使用出来るようにする

上記までだと、LOAD_PATHを変更してしまうため、ローカルでsls invokeが使用できなくなってしまうため、対応したいと思います。

serverless frameworkのsls invokeコマンドを実行すると、IS_LOCALという環境変数が作成され、trueがセットされます。※ こちらを参照ください

なので、これをフラグにLambda関数に条件分岐を設定します。

handler.rbを以下のように変更します。

if ENV['IS_LOCAL'].nil?

load_paths = Dir["/opt/bundle/ruby/2.5.0/gems/**/lib"]
$LOAD_PATH.unshift(*load_paths)
end

require 'json'
require 'business_time'

def hello(event:, context:)
puts Date.today.workday?

{ statusCode: 200, body: JSON.generate('Go Serverless v1.0! Your function executed successfully!') }
end

これで、sls invokeだった時は、LOAD_PATHが参照されなくなるので、sls invokeが使えるようになるかと思います。

当然この方法だと、関数が増えた時に全てに分岐を加えなければいけないので、対応策としては微妙ですが...

もっといい方法があればコメントいただけますと幸いです。