環境
- 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
が使えるようになるかと思います。
当然この方法だと、関数が増えた時に全てに分岐を加えなければいけないので、対応策としては微妙ですが...
もっといい方法があればコメントいただけますと幸いです。