「今日から使える AWS Lambda」
おさらい。
- サーバーをプロビジョニングしたり管理する必要なくコードを実行できるコンピューティングサービスです
- 必要時にのみコードを実行し、1 日あたり数個のリクエストから 1 秒あたり数千のリクエストまで 自動的にスケーリング します
- 管理を全く必要とせずに、任意のアプリケーションやバックエンドサービスで仮想的にコードを実行できます
- 高度な可用性のコンピューティングインフラストラクチャでコードを実行し、サーバーとオペレーティングシステム、システムのメンテナンス、容量のプロビショニングと自動スケーリング、コードのモニタリングやログ記録など、コンピューティングリソースのすべての管理を実行します
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/welcome.html
2019年8月現在、次のランタイムが標準でサポートされています
runtime | version |
---|---|
Node.js | Node.js 10, Node.js 8.10 |
Python | Python 3.6, Python 3.7, Python 2.7 |
Ruby | Ruby 2.5 |
Java | Java 8 |
Go | Go 1.x |
.NET | .NET Core 2.1, .NET Core 1.1 |
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/lambda-runtimes.html
2018 年 11 月に標準で Ruby を使えるようになりました。
AWS Lambda が Ruby をサポート
投稿日: Nov 29, 2018
Ruby を使用した AWS Lambda 関数コードの開発が可能になりました。
https://aws.amazon.com/jp/about-aws/whats-new/2018/11/aws-lambda-supports-ruby/
AWS Lambda を使うための道具の、主な三つを簡単に紹介します。
- AWS CLI
- Serverless
- Apex
Amazon が提供するコマンドラインツールです。 AWS Lambda に限らない、AWS を利用するプリミティブな機能を提供します。
必要なファイルを自分で ZIP ファイルにまとめて、AWS CLI のコマンドを使ってデプロイします。
# デプロイするファイルを ZIP ファイルにまとめる
$ zip function.zip index.js
# AWS CLI のコマンドでデプロイする
$ aws lambda create-function --function-name my-function \
--zip-file fileb://function.zip \
--handler index.handler \
--runtime nodejs10.x \
--role arn:aws:iam::123456789012:role/lambda-cli-role
Node.js 製のツールです。AWS 以外にも Google Cloud Platform や Microsoft Azure もカバーしています。
_______ __
| _ .-----.----.--.--.-----.----| .-----.-----.-----.
| |___| -__| _| | | -__| _| | -__|__ --|__ --|
|____ |_____|__| \___/|_____|__| |__|_____|_____|_____|
| | | The Serverless Application Framework
| | serverless.com, v1.49.0
-------'
次のような特徴があります。
- AWS のリソース管理は CloudFormation を利用
- Lambda のリソースも CloudFormation で管理されています
- Serverless の設定ファイル serverless.yml に直接 YAML 形式の CloudFormation の設定を記述できます
- serverless の設定で利用される変数を CloundFormation の記述でも利用できます
- web インタフェースやスケジューリングされた実行など、イベントの設定を簡単に記述できます
Go 言語製の AWS Lambda 用のツールです。
_ ____ _______ __
/ \ | _ \| ____\ \/ /
/ _ \ | |_) | _| \ /
/ ___ \| __/| |___ / \
/_/ \_\_| |_____/_/\_\
次のような特徴があります。
- 動作はとても軽快
- 設定は JSON 形式のファイルに記述します
- AWS のリソースは Terraform を利用
- infrastructure というディレクトリの下に、一般的な Terraform の設定を記述します
- リソースを構築するコマンドが用意されていますが、背後で Terraform のコマンドを呼び出しています
Ruby で書いた AWS Lambda の関数をデプロイし、利用していきます。
以降、ツールは Serverless で説明します。
なお、シークレットキーやプロファイルの設定が必要ですが、今回の説明では省略します。 実際に利用する際には適宜設定してください。
Serverless は serverless
というコマンドで操作を行います。
$ serverless -v
1.49.0 (Enterprise Plugin: 1.3.6, Platform SDK: 2.1.0)
タイプ数を節約した sls
というエイリアスも用意されています。
$ sls -v
1.49.0 (Enterprise Plugin: 1.3.6, Platform SDK: 2.1.0)
- 先のランタイムで説明したように、現在標準でサポートしているバージョンは 2.5
- 開発環境の Ruby のバージョンも rbenv などを利用して 2.5 に合わせておきます
$ rbenv local 2.5.3
- 標準添付ライブラリはインストール済みです
- AWS SDK for Ruby (gem で提供されている AWS のライブラリ)もインストール済みです
- それ以外の gem は別途インストールする必要があります(後述)
event
と context
という二つのキーワード引数をとるメソッドを定義します
メソッドはインスタンスを伴わずに呼び出せる形式で定義する必要があります
- トップレベルで定義したメソッド
- クラスメソッド
def hello(event:, context:)
'Hello AWS Lambda + Ruby world!'
end
class Handler
def self.hello(event:, context:)
'Hello AWS Lambda + Ruby world!'
end
end
Serverless は serverless.yml
という YAML ファイルに設定を記述します。
最小の構成では service
, provider
, function
を定義します。
# サービス名を指定します
service: foo
# プラットフォームやランタイムなどを指定します
provider:
name: aws
runtime: ruby2.5
stage: dev
region: ap-northeast-1
# 関数を指定します
functions:
hello:
handler: handler.hello # ファイル名 + `.` + メソッド名
ハンドラの名前は「ファイル名 + .
+ メソッド名」という形式で記述します。
クラスメソッドで定義した場合は、クラス名を含めた「ファイル名 + .
+ クラス名 + .
+ メソッド名」という形式で記述します。
functions:
hello:
handler: handler.Handler.hello # ファイル名 + `.` + クラス名 + `.` + メソッド名
Serverless はこれらのコードや設定ファイルのテンプレートが用意されています。テンプレート名は aws-ruby
です。
次のコマンドは foo
というディレクトリを作成し、そこに handler.rb
と、ランタイムに ruby 2.5 、サービス名に foo
を指定した serverless.yml
を生成します。
生成された serverless.yml
には、最低限の設定とコメントで設定例が記述されています。
$ sls create -p foo -t aws-ruby
$ tree foo
foo
├── handler.rb
└── serverless.yml
あるいは先にディレクトリを作成し、そこでパスを指定しないでコマンドを実行しても同じことができます。
$ mkdir foo
$ cd foo
$ sls create -t aws-ruby
Serverless のコマンドを利用してデプロイします。
$ sls deploy
デプロイを実行すると、 AWS に CloudFormation スタックが作成され、ディレクトリにあるファイルがデプロイされます。
デプロイされた関数の AWS 上での名前(識別子)は、サービス名、ステージ名、関数名 をハイフンでつないだものになります。
ここではサービス名 foo
、ステージ名 dev
、関数名 hello
ですので、AWS 上では foo-dev-hello
という名前になります。
削除も Serverless のコマンドで実行します。CloudFormation スタックが削除されます。
$ sls remove
三種類の起動方法を説明します。
- Serverless
- AWS CLI
- AWS SDK for Ruby
関数をデプロイした Serverless の環境があれば、Serverless のコマンドで関数を起動できます。
invoke
コマンドを利用し -f
オプションで起動する関数を指定します。このときの関数名は serverless.yml
の function
で指定した名前になります。
$ sls invoke -f hello
"Hello AWS Lambda + Ruby world!"
AWS CLI のコマンドを利用しても自由に起動することができます。 この場合、関数名には AWS 上の名前を指定します。 実行では AWS Lmabda 関数の戻り値を出力するファイルを指定する必要があります。
# 実行結果の出力先のファイルに `outout.txt` を指定する
# コンソールにはコマンドの実行結果が表示される
$ aws lambda invoke --function-name foo-dev-hello output.txt
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
}
# 関数の実行結果
$ cat output.txt
Hello AWS Lambda + Ruby world!
AWS SDK for Ruby の Aws::Lambda::Client#invoke
を利用すると、Ruby から関数を起動できます。
まず gem をインストールします。
$ gem install aws-sdk-lambda
コードでは Aws::Lambda::Client
のインスタンスを作成し、#invoke
メソッドで関数名を指定して起動します。ここで指定する関数名も AWS 上の名前です。
require 'aws-sdk-lambda'
client = Aws::Lambda::Client.new(
region: 'ap-northeast-1',
profile: 'default'
)
response = client.invoke(function_name: 'foo-dev-hello')
pp response
puts response.payload.read
#invoke
メソッドが返すオブジェクトの #payload
は関数の実行結果を格納しています。実態は StringIO
のオブジェクトなので、#read
等で結果を取得します。
$ ruby invoke_with_sdk.rb
#<struct Aws::Lambda::Types::InvocationResponse
status_code=200,
function_error=nil,
log_result=nil,
payload=#<StringIO:0x00007fb503a2d888>,
executed_version="$LATEST">
Hello AWS Lambda + Ruby world!
ハンドラの引数 event
には、関数が起動された時のパラメータが渡されます。
AWS SDK for Ruby を利用する場合、 引数の payload
に JSON の文字列を渡すと、
response = client.invoke(
function_name: 'foo-dev-hello',
payload: {name: 'World'}.to_json
)
ハンドラでは Hash のオブジェクトとして受け取ることができます。
def hello(event:, context:)
"Hello #{event['name']}!"
end
ハンドラの戻り値は JSON の文字列として返されます。 戻り値に Hash オブジェクトを返すと、呼び出した側は JSON の文字列を受け取ります。
def hello(event:, context:)
{message: "Hello #{event['name']}!"}
end
require 'aws-sdk-lambda'
client = Aws::Lambda::Client.new(region: 'ap-northeast-1')
payload = {name: 'AWS Lambda + Ruby'}
response = client.invoke(
function_name: 'foo-dev-hello',
payload: payload.to_json
)
payload = response.payload.read
pp payload
pp JSON.parse(payload)
$ ruby invoke_with_sdk.rb
"{\"message\":\"Hello AWS Lambda + Ruby!\"}"
{"message"=>"Hello AWS Lambda + Ruby!"}
先ほどディレクトリにある環境がそのままパッケージされてデプロイされると説明しました。
つまり利用したい gem がディレクトリ内に配置されていれば、一緒にパッケージされてデプロイされ、AWS Lambda 上で利用することができるようになります。
$ bundle init
# Gemfile を編集
$ bundle install --path vendor/bundle # パッケージに含まれる位置に gem をインストールする
ちなみに。「ディレクトリにある環境がそのままパッケージされてデプロイされる」ため、ワークファイルやドキュメントなど実行に不要なファイルでも、ディレクトリに存在しているとパッケージに含められてしまいます。
不要なファイルがデプロイされるのを防ぐには、serverless.yml
に package
という項目を追加し、除外するファイルを指定します。
package:
exclude:
- README.md
ちなみに。ランタイムが Node.js の例ですが、npm install を実行しないでデプロイされていたため、機能していなかったというケースに実際に遭遇しました。 デプロイする内容をローカルの環境でパッケージにするために発生する現象です。
閑話休題。
デプロイするときに gem のインストールを忘れないように、デプロイ時に自動的に gem のインストールをするようにしてみます。
Serverless には便利なプラグインが用意されています。 そのうち Serverless Hooks Plugin は Serverless の実行をフックし、特定の時点でコマンドを実行できます。
yarn や npm でプラグインをインストールします。
$ yarn add serverless-hook-plugin --dev
Serverless の設定にプラグインの利用を指定します。
plugins:
- serverless-hooks-plugin
フックを設定します。serverless.yml
に custom
という項目を追加し、hooks
に Serverless のイベントと、そのイベントのときに実行するコマンドを記述します。
この例では、パッケージの初期化の前に gem のインストールを実行しています。
custom:
hooks:
before:package:initialize:
- bundle install --path vendor/bundle
プラグインのインストールで作成されたファイルはデプロイするパッケージに含める必要がないので、パッケージする対象から除外しておきます。
package:
exclude:
- README.md
- package.json
- yarn.lock
- node_modules/**
パッケージされたファイルがデプロイされ、それが実行されるということは、それらのファイルは AWS の環境で実行できる形式でなければなりません。
ありがたいことに AWS Lambda と同等の環境を提供する lambci/lambda:build-ruby2.5
という Docker イメージが公開されています。
このイメージでコンテナを起動し、そこで gem のインストールを実行します。 開発を行っているディレクトリをコンテナ内の実行ディレクトリにマウントして、インストールした gem がデプロイ対象に含まれるようにします。
$ docker run --rm -v $(pwd):/var/task lambci/lambda:build-ruby2.5 bundle install --path vendor/bundle
複数の AWS Lambda でライブラリを共有したい場合や、デプロイのたびにライブラリを含むパッケージをアップロードする負担を軽くするためなどに、関数本体から分離して利用するレイヤーという仕組みがあります。
レイヤーは、ライブラリ、カスタムランタイム、またはその他の依存関係を含む ZIP アーカイブです。 レイヤーを使用することで、関数のライブラリを使用することができます。 デプロイパッケージに含める必要はありません。
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-layers.html
serverless.yml
にレイヤーの設定を追加します。
layers:
hello: # 任意の名前
path: layers # layers に配置する gem を格納するディレクトリ
name: ${self:service}-${self:provider.stage}-gems # レイヤー名
compatibleRuntimes: # レイヤーのランタイム
- ruby2.5
allowedAccounts: # 許可するアカウント
- '*'
レイヤー名の指定で、serverless.yml
の他の箇所で記述された値を参照しています。
${self:service}
は同じファイルで設定されたサービス名、${self:provider.stage}
は同じファイルで設定された provider
の stage
の値を参照しています。
結果、全体で ${self:service}-${self:provider.stage}-gems
は foo-dev-gem
という値に展開されます。
path
で指定したディレクトリに gem をインストールします。
$ mkdir layers
$ bundle install --path laylers/vendor/bundle
レイヤーの設定を追加してデプロイすると、path
で指定したディレクトリ以下の内容が ZIP ファイルにまとめられ、foo-dev-gems
という名前でレイヤーが作成されます。
作成されたレイヤーは、関数の実行時に実行環境の /opt
というディレクトリに展開されます。
今回の場合 vendor/bundle
に gem をインストールしたので、実行時には /opt/vendor/bundle
というディレクトリにインストールした gem が展開されることになります。
関数からレイヤーを利用するには、関数の設定にレイヤー参照の設定 layers
を追加します。
レイヤーは ARN で指定する必要がありますが、Ref
関数を利用するとレイヤー名から ARN を取得することができます。
ただし、ここで指定するレイヤー名は、layers
で定義したレイヤー名をキャピタライズしたものに LambdaLayer
を連結したものになります。
今回は hello
という名前で定義していますので、キャピタライズした Hello
に LambdaLayer
を連結した HelloLambdaLayer
という名前で指定します。
また実行時に gem を参照できるようにするために、gem が展開されているディレクトリを環境変数 GEM_PATH
に設定します。
functions:
hello:
handler: handler.hello
layers:
- {Ref: HelloLambdaLayer} # レイヤーの ARN
environment:
GEM_PATH: /opt/vendor/bundle/ruby/2.5.0 # インストールされた gem が展開されるディレクトリ
※ 以下 gem のディレクトリ構成に詳しい方がいらっしゃったらアドバイスお願いします。
上の例では GEM_PATH
を指定しましたが、インストール済みの gem が利用できるということは GEM_PATH
はすでに設定されていることになります。
次のようなコードをデプロイして環境変数を調べます。
def get_path(event:, context:)
ENV.to_h
end
調べてみたところ、下の二つのパスが最初から設定されていることがわかりました。
GEM_PATH
/var/task/vendor/bundle/ruby/2.5.0
/opt/ruby/gems/2.5.0
このうち一方は /opt/
の下のディレクトリが指定されています。ここはレイヤー内容が展開される場所ですので、レイヤーで指定するディレクトリの下の ruby/gems/2.5.0
に Ruby のコードから参照できる状態の gem を配置すれば、GEM_PATH
を設定せずにレイヤーで追加した gem が利用できるはずです。
パスを指定してインストールした時のディレクトリの状態を確認します。
$ bundle install --path layers
ここで
layers/ruby/gems/2.5.0
という階層のディレクトリができれば事は解決するのですが、実際には
layers/ruby/2.5.0/
という階層が作成され、うまく解決できません。
今回はこれをシンボリックリンクで解決します。
別途任意のディレクトリに gem をインストールします。
このディレクトリへのシンボリックリンクを、GEM_PATH
に一致する位置のレイヤーのディレクトリ内に作成します。
# 任意のディレクトリにインストールする
$ bundle install --path vendor/bundle
# layers 以下に /ruby/gems というディレクトリを作成する
$ mkdir -p layers/ruby/gems
$ cd layers/ruby/gems/
# インストールしたディレクトリへシンボリックリンクを作成する
$ ln -s ../../../vendor/bundle/ruby/2.5.0 2.5.0
レイヤーのディレクトリにあるのはシンボリックリンクだけですが、ZIP ファイルにまとめられるときにリンク先の実体のファイルが格納されるので、これでも問題なくレイヤーを作成することができます。
ローカル環境での開発も Docker で済ませてしまえるならば開発環境と実行環境を揃えることができるので問題ありませんが、開発はローカルのネイティブな環境でしたいという場合にはローカル環境と実行環境で使いわける必要があります。
この場合も、上記で紹介したシンボリックリンクを利用する方法が使えます。 シンボリックリンクを使えば gem 本体は任意のディレクトにインストールできるので、実行環境用の gem をインストールするワークディレクトリを用意してそこで gem をインストールします。 インストールができたら、レイヤーのディレクトリの中にシンボリックリンクを作成します。
$ mkdir _build
$ cp Gemfile* _build/.
$ (cd _build && docker run --rm -v $(pwd):/var/task lambci/lambda:build-ruby2.5 bundle install --path .)
これで、ローカル環境で実行する場合にはローカル環境にインストールされている gem が参照され、実行環境ではレイヤーのディレクトリからリンクされている実行環境用の gem が参照されるようになりました。
ワークディレクトリがパッケージに含まれないように対象から除外しておきましょう。
package:
exclude:
- README.md
- package.json
- yarn.lock
- node_modules/**
- spec/**
- _build/**
と、いうわけで。
もう一つのプラットフォームとして AWS Lambda でも Ruby を使いましょう!