RubyのSinatraをAWS Lambdaで動かす

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

今回の投稿では、Sinatraをローカル実行で確認したのち、Lambdaに移行するところまでです。
他の方の記事もありましたが、CloudFormationやSAMを使ったりして、まだ知識が追い付かずちょっとわかりにくかったので、LambdaとAPI GatewayをAWS管理コンソールを使って１つ１つ手順を追ってみたかったのです。

また、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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/5f703212-cd16-a56d-41e7-25f0e3261d16.png)

POST http://localhost:4567/test
body={ "message": "hello" }

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/8bb7fe4f-334d-11eb-8a75-1e980a749171.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/6dcd934c-722a-f2bc-8f72-8499333597ff.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/239cff44-9fc9-fd8b-c4fe-647384e09aeb.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/21eeed8b-3ee7-caa2-1098-f84f7b80eb8d.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/524a3584-f003-2d29-20fa-a2498a76326c.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/c7c3467a-fa9a-32c4-d3f5-6379ea7cf2f1.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/d17df997-ef82-c1fc-1b1c-1168041a3cfc.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/16f1bb53-37e2-2280-3dd8-1d66f38e2967.png)
 

Choose the protocolには、RESTを選択します。
API名は適当でよいですが「Ruby Test」とでもしておきます。

あとはいつもの通り、
リソース /test を作成。そのとき「API Gateway CORS を有効にする」のチェックはOnにします。
/testに、メソッドGETとPOSTを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/d9949283-5189-5327-6bfb-a5eaa7fafc1a.png)

統合タイプはLambda関数を選択し、「Lambda プロキシ統合の使用」のチェックはOnにします。
Lambda関数には、さきほどこしらえた「ruby_lambda_test」を選択して最後に「保存」ボタンを押下します。
POSTも同様です。
最後に、APIのデプロイを選択し、新しいステージとして例えば「v1」として作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/ba524fe8-ccac-6fd8-0bf2-4929b3107114.png)

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/6d11f0f0-23d1-6402-c405-ce700b17765b.png)

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

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

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/324e813e-2c45-56e9-b9e3-5e9328eb0d7b.png)

POST https://[XXXXXXXX].execute-api.ap-northeast-1.amazonaws.com/v1/test
body={ "message": "hello" }

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/9b4ac23a-11ec-6476-e00a-55dfcf6ad9c7.png)

おおっ、動いたっ！

# 今後

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

＜残作業＞

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

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

以上