環境
- 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
functionにlayersを追加して、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
これで、適当にテストを作成し、「テスト」ボタンを押下すると、以下のようなエラーが発生します。
{
"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が使えるようになるかと思います。
当然この方法だと、関数が増えた時に全てに分岐を加えなければいけないので、対応策としては微妙ですが...
もっといい方法があればコメントいただけますと幸いです。
