Skip to content

Instantly share code, notes, and snippets.

@mattsan
Last active September 6, 2019 10:06
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save mattsan/69526a71db1a7c188f244edd946e14f0 to your computer and use it in GitHub Desktop.
Save mattsan/69526a71db1a7c188f244edd946e14f0 to your computer and use it in GitHub Desktop.
ESM Real Lounge #1

実践 AWS Lambda + Ruby

実践 AWS Lambda + Ruby \ ESM Real Lounge #1 \ 2019/08/21 (Wed) \ 松本栄二 / emattsan

「今日から使える AWS Lambda」

AWS Lambda とは

おさらい。

  • サーバーをプロビジョニングしたり管理する必要なくコードを実行できるコンピューティングサービスです
  • 必要時にのみコードを実行し、1 日あたり数個のリクエストから 1 秒あたり数千のリクエストまで 自動的にスケーリング します
  • 管理を全く必要とせずに、任意のアプリケーションやバックエンドサービスで仮想的にコードを実行できます
  • 高度な可用性のコンピューティングインフラストラクチャでコードを実行し、サーバーとオペレーティングシステム、システムのメンテナンス、容量のプロビショニングと自動スケーリング、コードのモニタリングやログ記録など、コンピューティングリソースのすべての管理を実行します

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/welcome.html

AWS Lambda ランタイム

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

Ruby ランタイム

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 Lmabda を使おう

AWS Lambda を使うための道具の、主な三つを簡単に紹介します。

  • AWS CLI
  • Serverless
  • Apex

AWS CLI (AWS Command Line Interface)

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

Serverless

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 インタフェースやスケジューリングされた実行など、イベントの設定を簡単に記述できます

Apex

Go 言語製の AWS Lambda 用のツールです。

             _    ____  _______  __
            / \  |  _ \| ____\ \/ /
           / _ \ | |_) |  _|  \  /
          / ___ \|  __/| |___ /  \
         /_/   \_\_|   |_____/_/\_\

次のような特徴があります。

  • 動作はとても軽快
  • 設定は JSON 形式のファイルに記述します
  • AWS のリソースは Terraform を利用
  • infrastructure というディレクトリの下に、一般的な Terraform の設定を記述します
  • リソースを構築するコマンドが用意されていますが、背後で Terraform のコマンドを呼び出しています

Ruby で AWS Lambda 関数を書こう

Ruby で書いた AWS Lambda の関数をデプロイし、利用していきます。

以降、ツールは Serverless で説明します。

なお、シークレットキーやプロファイルの設定が必要ですが、今回の説明では省略します。 実際に利用する際には適宜設定してください。

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)

Ruby の環境

  • 先のランタイムで説明したように、現在標準でサポートしているバージョンは 2.5
    • 開発環境の Ruby のバージョンも rbenv などを利用して 2.5 に合わせておきます
$ rbenv local 2.5.3
  • 標準添付ライブラリはインストール済みです
  • AWS SDK for Ruby (gem で提供されている AWS のライブラリ)もインストール済みです
  • それ以外の gem は別途インストールする必要があります(後述)

最小の AWS Lambda 関数を書こう

Ruby で書く AWS Lambda 関数のコード

eventcontext という二つのキーワード引数をとるメソッドを定義します

メソッドはインスタンスを伴わずに呼び出せる形式で定義する必要があります

  • トップレベルで定義したメソッド
  • クラスメソッド
トップレベルで定義したメソッド
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.yml / Serverless の設定ファイル

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

Lambda function を起動しよう

三種類の起動方法を説明します。

  • Serverless
  • AWS CLI
  • AWS SDK for Ruby

Seerverless

関数をデプロイした Serverless の環境があれば、Serverless のコマンドで関数を起動できます。

invoke コマンドを利用し -f オプションで起動する関数を指定します。このときの関数名は serverless.ymlfunction で指定した名前になります。

$ sls invoke -f hello
"Hello AWS Lambda + Ruby world!"

AWS CLI

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 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 を使おう

先ほどディレクトリにある環境がそのままパッケージされてデプロイされると説明しました。

つまり利用したい gem がディレクトリ内に配置されていれば、一緒にパッケージされてデプロイされ、AWS Lambda 上で利用することができるようになります。

$ bundle init

# Gemfile を編集

$ bundle install --path vendor/bundle # パッケージに含まれる位置に gem をインストールする

ちなみに。「ディレクトリにある環境がそのままパッケージされてデプロイされる」ため、ワークファイルやドキュメントなど実行に不要なファイルでも、ディレクトリに存在しているとパッケージに含められてしまいます。 不要なファイルがデプロイされるのを防ぐには、serverless.ymlpackage という項目を追加し、除外するファイルを指定します。

package:
  exclude:
    - README.md

ちなみに。ランタイムが Node.js の例ですが、npm install を実行しないでデプロイされていたため、機能していなかったというケースに実際に遭遇しました。 デプロイする内容をローカルの環境でパッケージにするために発生する現象です。

閑話休題。

自動的に gem をインストールするようにしよう

デプロイするときに gem のインストールを忘れないように、デプロイ時に自動的に gem のインストールをするようにしてみます。

Serverless には便利なプラグインが用意されています。 そのうち Serverless Hooks Plugin は Serverless の実行をフックし、特定の時点でコマンドを実行できます。

yarn や npm でプラグインをインストールします。

$ yarn add serverless-hook-plugin --dev

Serverless の設定にプラグインの利用を指定します。

plugins:
  - serverless-hooks-plugin

フックを設定します。serverless.ymlcustom という項目を追加し、hooks に Serverless のイベントと、そのイベントのときに実行するコマンドを記述します。 この例では、パッケージの初期化の前に gem のインストールを実行しています。

custom:
  hooks:
    before:package:initialize:
      - bundle install --path vendor/bundle

プラグインのインストールで作成されたファイルはデプロイするパッケージに含める必要がないので、パッケージする対象から除外しておきます。

package:
  exclude:
    - README.md
    - package.json
    - yarn.lock
    - node_modules/**

Native Extension を利用する gem を使おう

パッケージされたファイルがデプロイされ、それが実行されるということは、それらのファイルは 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 レイヤーを使って gem を分離しよう

複数の 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} は同じファイルで設定された providerstage の値を参照しています。 結果、全体で ${self:service}-${self:provider.stage}-gemsfoo-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 という名前で定義していますので、キャピタライズした HelloLambdaLayer を連結した 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 のディレクトリ構成に詳しい方がいらっしゃったらアドバイスお願いします。

上の例では 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 をインストールします。

aws_lambda_ruby_directory-2.svg

このディレクトリへのシンボリックリンクを、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

aws_lambda_ruby_directory-1.svg

レイヤーのディレクトリにあるのはシンボリックリンクだけですが、ZIP ファイルにまとめられるときにリンク先の実体のファイルが格納されるので、これでも問題なくレイヤーを作成することができます。

ローカル環境の gem と実行環境の gem を両立させよう

ローカル環境での開発も 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 .)

aws_lambda_ruby_directory.svg

これで、ローカル環境で実行する場合にはローカル環境にインストールされている gem が参照され、実行環境ではレイヤーのディレクトリからリンクされている実行環境用の gem が参照されるようになりました。

ワークディレクトリがパッケージに含まれないように対象から除外しておきましょう。

package:
  exclude:
    - README.md
    - package.json
    - yarn.lock
    - node_modules/**
    - spec/**
    - _build/**

Ruby で AWS Lambda を使おう

と、いうわけで。

もう一つのプラットフォームとして AWS Lambda でも Ruby を使いましょう!

Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
<?xml version="1.0"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" width="593px" height="286px" viewBox="-0.5 -0.5 593 286" style="background-color: rgb(255, 255, 255);">
<defs/>
<g>
<g transform="translate(20.5,-0.5)">
<switch>
<foreignObject style="overflow:visible;" pointer-events="all" width="552" height="130" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; font-size: 55px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; vertical-align: top; width: 552px; white-space: nowrap; overflow-wrap: normal; font-weight: bold; text-align: center;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display:inline-block;text-align:inherit;text-decoration:inherit;white-space:normal;">実践<br/>AWS Lambda + Ruby</div>
</div>
</foreignObject>
</switch>
</g>
<g transform="translate(113.5,173.5)">
<switch>
<foreignObject style="overflow:visible;" pointer-events="all" width="364" height="112" requiredFeatures="http://www.w3.org/TR/SVG11/feature#Extensibility">
<div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; font-size: 32px; font-family: Helvetica; color: rgb(0, 0, 0); line-height: 1.2; vertical-align: top; width: 366px; white-space: nowrap; overflow-wrap: normal; text-align: center;">
<div xmlns="http://www.w3.org/1999/xhtml" style="display:inline-block;text-align:inherit;text-decoration:inherit;white-space:normal;">ESM Real Lounge #1<br/>2019/08/21 Wed<br/>松本栄二 / emattsan</div>
</div>
</foreignObject>
</switch>
</g>
</g>
</svg>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment