Ruby
Sinatra
AWS
lambda
APIGateway

RubyのSinatraをAWS Lambdaで動かす

Rubyを初めて数日の超初心者です。

これまでもLambdaでNode.jsを使っていますが、昨年末にRubyもLambdaに対応したとのことので、Rubyにも手を出してみようと思います。

今回の投稿では、Sinatraをローカル実行で確認したのち、Lambdaに移行するところまでです。

他の方の記事もありましたが、CloudFormationやSAMを使ったりして、まだ知識が追い付かずちょっとわかりにくかったので、LambdaとAPI GatewayをAWS管理コンソールを使って1つ1つ手順を追ってみたかったのです。

また、RailsをLambdaにしたかったのですが、Lambdaが目指すマイクロサービスアーキテクチャ的にRailsシステムまでLambdaに持っていくのはお奨めしないそうで、また今度にします。以下、参考。

https://blog.eq8.eu/article/sinatra-on-aws-lambda.html

「So no Rails is not Designed for this. Don’t do it even if you theoretically could.(Railsはそう設計されていません。理論的には可能であっても、やらないでください。)」


Rubyのセットアップ

Rubyのセットアップが完了している前提で話を進めます。

いろいろなサイトで紹介されていますので、難しくはないと思います。

Windowsでもいいですし、Macでもいいですし、Linuxでもいいです。Windowsであれば、WSLでもよいです。


Sinatraを使ったアプリをローカル実行する。

まずは単純にSinatraの環境を作ります。


mkdir lambda_test

cd lambda_test

bundle init


Gemfileが生成されたかと思います。

以下を追記します。


gem 'sinatra'

gem 'json'

gem 'rack'

gem 'rack-contrib'

gem 'rubysl-base64'


あと、開発時には以下も追加します。


gem 'aws-sdk'

gem 'aws-record'


最終的にはこんな感じです。


Gemfile

# frozen_string_literal: true

source "https://rubygems.org"

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

# gem "rails"
gem 'sinatra'
gem 'json'
gem 'rack'
gem 'rack-contrib'
gem 'rubysl-base64'

group :development do
gem 'aws-sdk'
gem 'aws-record'
end


aws-sdkやaws-recordは必須ではありません。

せっかくLambdaにアップロードするので、AWSの機能を最大限に使うことになると思ったためです。

そして、Bundlerを使ってGemをインストールします。


bundle install --path vendor/bundle --with development


それでは、さっそくコーディングします。とは言っても非常に単純なREST APIです。


main.rb

require 'sinatra'

get '/test' do
params.to_json
end

post '/test' do
body = request.body.read
body.to_json
end



Sinatraのローカル実行環境

とりあえず動かす分には、以下でもいいです。


bundle exec ruby main.rb


ですが、config.ruを作っておきます。

ローカルではデフォルトではWEBrickが使われるのですが、Lambdaに上げた後はこれを使わないため、そのときにはちょっとしたカスタマイズを入れます。


config.ru

require 'rack'

require 'rack/contrib'
require_relative './main'

set :root, File.dirname(__FILE__)
set :views, Proc.new { File.join(root, "views") }

run Sinatra::Application


require_relativeのところには、さきほどコーディングしたmain.rbを指定します。

また、今回は使いませんが、viewsフォルダも作っておきます。


mkdir views

touch views/.keep


それでは、Sinatraを立ち上げます。-pには適当なポート番号を指定して下さい。

> bundle exec rackup -p 4567

[2019-04-14 21:51:11] INFO WEBrick 1.4.2
[2019-04-14 21:51:11] INFO ruby 2.5.3 (2018-10-18) [x64-mingw32]
[2019-04-14 21:51:11] INFO WEBrick::HTTPServer#start: pid=5268 port=4567

それでは早速アクセスしてみましょう。

GET http://localhost:4567/test?param1=abcd&param2=efgh

image.png

POST http://localhost:4567/test

body={ "message": "hello" }

image.png

OKです。


GemをLambdaにアップロードする

これからLambdaにアップロードします。

最初にGemファイル群をアップロードします。

もちろん、Gemファイル群とmain.rbを両方いっしょにアップしてもよいのですが、ファイルが大きくなってしまうと、AWS管理コンソールからLambdaのRubyソースコードを編集できなくなってしまうため、Layerとmain.rbに分けてアップします。


bundle install --path . --without development


rubyというフォルダが出来上がっているかと思います。

--without developmentは、AWS SDKはLambda上にあるため、アップロードする必要がないため、ZIPに含めないようにするために指定しています。


zip -r ruby.zip ./ruby/


これで、Gemファイルが詰まったruby.zipが出来上がりました。(ZIPファイル名は何でもよいです)

それでは、AWS管理コンソールから、LambdaのLayerを作成します。

 https://ap-northeast-1.console.aws.amazon.com/lambda/home?region=ap-northeast-1#/create/layer

名前は、例えば「sinatra_test」としました。アップロードするZIPは、先ほど作成したruby.zipです。

互換性のあるランタイムは、当然「Ruby 2.5」を選択します。

image.png

アップできました。

バージョンは1から振られ、更新するたびに番号が大きくなっていきます。

image.png

今度は、AWS管理コンソールから、Lambda関数を作成します。

 https://ap-northeast-1.console.aws.amazon.com/lambda/home?region=ap-northeast-1#/create

例えば、ruby_lambda_testという名前を付けてみました。

ランタイムには「Ruby 2.5」を選択します。

image.png

とりあえず、中身がほぼ空の関数ができました。

image.png

次に、レイヤを追加します。

Layersのアイコンをクリックし、「レイヤ追加」ボタンを押下して、さきほど作成したsinatra_testを追加します。右上の「保存」ボタンも忘れずに。

image.png

それでは、main.rbをアップロードします。他のファイルも使うので、ZIPにまとめてアップロードしましょう。

ちょっとその前に、1つだけRubyファイルを追加する必要があります。


lambda.rb

# MIT No Attribution

# Permission is hereby granted, free of charge, to any person obtaining a copy of this
# software and associated documentation files (the "Software"), to deal in the Software
# without restriction, including without limitation the rights to use, copy, modify,
# merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so.

# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
# INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
# PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
# SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

require 'json'
require 'rack'
require 'base64'

# Global object that responds to the call method. Stay outside of the handler
# to take advantage of container reuse
$app ||= Rack::Builder.parse_file("#{__dir__}/config.ru").first

def handler(event:, context:)
# Check if the body is base64 encoded. If it is, try to decode it
body =
if event['isBase64Encoded']
Base64.decode64(event['body'])
else
event['body']
end
# Rack expects the querystring in plain text, not a hash
querystring = Rack::Utils.build_query(event['queryStringParameters']) if event['queryStringParameters']
# Environment required by Rack (http://www.rubydoc.info/github/rack/rack/file/SPEC)
env = {
'REQUEST_METHOD' => event['httpMethod'],
'SCRIPT_NAME' => '',
'PATH_INFO' => event['path'] || '',
'QUERY_STRING' => querystring || '',
'SERVER_NAME' => 'localhost',
'SERVER_PORT' => 443,
'CONTENT_TYPE' => event['headers']['content-type'],

'rack.version' => Rack::VERSION,
'rack.url_scheme' => 'https',
'rack.input' => StringIO.new(body || ''),
'rack.errors' => $stderr,
}
# Pass request headers to Rack if they are available
unless event['headers'].nil?
event['headers'].each { |key, value| env["HTTP_#{key}"] = value }
end

begin
# Response from Rack must have status, headers and body
status, headers, body = $app.call(env)

# body is an array. We combine all the items to a single string
body_content = ""
body.each do |item|
body_content += item.to_s
end

# We return the structure required by AWS API Gateway since we integrate with it
# https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html
response = {
'statusCode' => status,
'headers' => headers,
'body' => body_content
}
if event['requestContext'].key?('elb')
# Required if we use Application Load Balancer instead of API Gateway
response['isBase64Encoded'] = false
end
rescue Exception => msg
# If there is any exception, we return a 500 error with an error message
response = {
'statusCode' => 500,
'body' => msg
}
end
# By default, the response serializer will call #to_json for us
response
end


以下にあるlambda.rb、ほぼそのままです。

 https://github.com/aws-samples/serverless-sinatra-sample

config.ruの場所だけ変えています。

WEBrickを使わないので、API Gatewayからの呼び出しをSinatraの呼び出しに仲介します。

結局、以下をZIPに固めてアップロードします。


  • lambda.rb

  • config.ru

  • views\*

  • main.rb

zipコマンドでは以下の感じです。


zip -r upload.zip views config.ru lambda.rb main.rb


関数コードのところから、「.zipファイルをアップロード」を選択して、さきほど作成したupload.zipを指定します。

また、ハンドラには「lambda.handler」を指定します。lambda.rbを呼ぶようにしています。

加えて、環境変数も追加します。

 GEM_PATH => /opt/ruby/2.5.0

これは、LayerでアップロードしたGemファイルを参照するために必要です。

最後に、右上の「保存」ボタンを押下してください。

image.png

ご覧の通り、AWS管理コンソールから、ソースコードを編集できます!


API Gatewayを設定する

受け皿であるLambdaの準備ができましたので、外部からの受け口であるAPI Gatewayを設定します。

main.rbで、ルーティングとして、以下の2つのエンドポイントを指定していました。


  • GET /test

  • POST /test

まずは、適当なAPIを作成します。「APIを作成」ボタンをクリックします。

 https://ap-northeast-1.console.aws.amazon.com/apigateway/home?region=ap-northeast-1#/apis

image.png

Choose the protocolには、RESTを選択します。

API名は適当でよいですが「Ruby Test」とでもしておきます。

あとはいつもの通り、

リソース /test を作成。そのとき「API Gateway CORS を有効にする」のチェックはOnにします。

/testに、メソッドGETとPOSTを作成します。

image.png

統合タイプはLambda関数を選択し、「Lambda プロキシ統合の使用」のチェックはOnにします。

Lambda関数には、さきほどこしらえた「ruby_lambda_test」を選択して最後に「保存」ボタンを押下します。

POSTも同様です。

最後に、APIのデプロイを選択し、新しいステージとして例えば「v1」として作成します。

image.png

できあがりました。

青帯のところに、URLの呼び出しでURLが付記されていますので、それが呼び出し口です。

image.png

さあ、いよいよ呼び出してみましょう。

https://[XXXXXXXX].execute-api.ap-northeast-1.amazonaws.com/v1/test?param1=abcd&param2=efgh

image.png

POST https://[XXXXXXXX].execute-api.ap-northeast-1.amazonaws.com/v1/test

body={ "message": "hello" }

image.png

おおっ、動いたっ!


今後

ちょっと大変でしたけど、動いてしまいました。(Sinatraの中身はよくわかりませんが。。。)

<残作業>


  • Sinatraのローカル環境を立ち上げたのですが、Visual Studio Codeでなぜかデバッグできせん。他の方の投稿を見ると、できると言われているのですが、私はできませんでした。すごく悲しいです。どなたか助けていただけると助かります!


  • Railsも頑張る??初心者の私にはかなりハードル高そうですが、APIモードだったら何とかなるかもしれません。。。


以上