LoginSignup
4
4

More than 5 years have passed since last update.

RubyのSinatraをAWS Lambdaで動かす

Last updated at Posted at 2019-04-14

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モードだったら何とかなるかもしれません。。。

以上

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4