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'
最終的にはこんな感じです。
# 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です。
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に上げた後はこれを使わないため、そのときにはちょっとしたカスタマイズを入れます。
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¶m2=efgh
POST http://localhost:4567/test
body={ "message": "hello" }
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」を選択します。
アップできました。
バージョンは1から振られ、更新するたびに番号が大きくなっていきます。
今度は、AWS管理コンソールから、Lambda関数を作成します。
https://ap-northeast-1.console.aws.amazon.com/lambda/home?region=ap-northeast-1#/create
例えば、ruby_lambda_testという名前を付けてみました。
ランタイムには「Ruby 2.5」を選択します。
とりあえず、中身がほぼ空の関数ができました。
次に、レイヤを追加します。
Layersのアイコンをクリックし、「レイヤ追加」ボタンを押下して、さきほど作成したsinatra_testを追加します。右上の「保存」ボタンも忘れずに。
それでは、main.rbをアップロードします。他のファイルも使うので、ZIPにまとめてアップロードしましょう。
ちょっとその前に、1つだけRubyファイルを追加する必要があります。
# 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ファイルを参照するために必要です。
最後に、右上の「保存」ボタンを押下してください。
ご覧の通り、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
Choose the protocolには、RESTを選択します。
API名は適当でよいですが「Ruby Test」とでもしておきます。
あとはいつもの通り、
リソース /test を作成。そのとき「API Gateway CORS を有効にする」のチェックはOnにします。
/testに、メソッドGETとPOSTを作成します。
統合タイプはLambda関数を選択し、「Lambda プロキシ統合の使用」のチェックはOnにします。
Lambda関数には、さきほどこしらえた「ruby_lambda_test」を選択して最後に「保存」ボタンを押下します。
POSTも同様です。
最後に、APIのデプロイを選択し、新しいステージとして例えば「v1」として作成します。
できあがりました。
青帯のところに、URLの呼び出しでURLが付記されていますので、それが呼び出し口です。
さあ、いよいよ呼び出してみましょう。
https://[XXXXXXXX].execute-api.ap-northeast-1.amazonaws.com/v1/test?param1=abcd¶m2=efgh
POST https://[XXXXXXXX].execute-api.ap-northeast-1.amazonaws.com/v1/test
body={ "message": "hello" }
おおっ、動いたっ!
今後
ちょっと大変でしたけど、動いてしまいました。(Sinatraの中身はよくわかりませんが。。。)
<残作業>
-
Sinatraのローカル環境を立ち上げたのですが、Visual Studio Codeでなぜかデバッグできせん。他の方の投稿を見ると、できると言われているのですが、私はできませんでした。すごく悲しいです。どなたか助けていただけると助かります!
-
Railsも頑張る??初心者の私にはかなりハードル高そうですが、APIモードだったら何とかなるかもしれません。。。
以上