何番煎じか分からないけど、最近やったので。
前提知識
- AWS Lambda + Amazon API Gateway で HTTP リクエストを受け付けることができる
- AWS Lambda ではコンテナイメージを動かせる
- AWS Lambda で Sinatra アプリを動かすための公式サンプルがある
つまりコンテナ化した Sinatra アプリを Lambda 上にデプロイして HTTP リクエストを受け付けることができる。
動かす準備はもう全部整っていて、お手軽そうですね。
Ruby アプリを Lambda で動かすコンテナイメージを作る
Sinatra 以前に、そもそも Ruby はどうやって Lambda Container Image 上で動くのか。公式にチュートリアルがあるのでこの通りで良い。
Deploy Ruby Lambda functions with container images - AWS Lambda
https://gallery.ecr.aws/lambda/ruby の Usage をなぞる。
- https://gallery.ecr.aws/lambda/ruby から base image を選んで、Dockerfile を作って
- docker build して
- docker run で Image を立ち上げると HTTP で待ち受けるので
- curl で発火させる
curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"payload":"hello world!"}'
謎 URL だけど「こういうもの」として覚えておけば良い。
特筆すべき点
ENV["GEM_PATH"] #=> "/var/task/vendor/bundle/ruby/2.7.0:/opt/ruby/gems/2.7.0"
なので、ここに gem を入れておくと bundle exec しなくても gem を使える。いやまぁ bundler 使えば良いと思いますが。。
Rack アプリを Lambda で動かす
前述した公式の https://github.com/aws-samples/serverless-sinatra-sample の他に、 https://github.com/logandk/serverless-rack というものもある。
仕組み
どちらも肝は Rack::Builder.parse_file
です。 config.ru
を eval することで実行したい Rack App を取り出す処理。
- https://github.com/aws-samples/serverless-sinatra-sample/blob/807901d90c4ad0b9d1f55b8a44c9217eb709d161/lambda.rb#L22
- https://github.com/logandk/serverless-rack/blob/1.0.7/lib/rack_adapter.rb#L17
App を取得できたので、env
を組み立てて
- https://github.com/aws-samples/serverless-sinatra-sample/blob/807901d90c4ad0b9d1f55b8a44c9217eb709d161/lambda.rb#L38-L50
- https://github.com/logandk/serverless-rack/blob/1.0.7/lib/serverless_rack.rb#L85-L109
app.call(env)
して
- https://github.com/aws-samples/serverless-sinatra-sample/blob/807901d90c4ad0b9d1f55b8a44c9217eb709d161/lambda.rb#L68
- https://github.com/logandk/serverless-rack/blob/1.0.7/lib/serverless_rack.rb#L226-L233
Rack の status, headers, body
の組から、Lambda の response になるように JSON を組み立て直す
- https://github.com/aws-samples/serverless-sinatra-sample/blob/807901d90c4ad0b9d1f55b8a44c9217eb709d161/lambda.rb#L78-L82
- https://github.com/logandk/serverless-rack/blob/1.0.7/lib/serverless_rack.rb#L235-L241
Lambda の response というのはコレ。https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-output-format
{ "isBase64Encoded": true|false, "statusCode": httpStatusCode, "headers": { "headerName": "headerValue", ... }, "multiValueHeaders": { "headerName": ["headerValue", "headerValue2", ...], ... }, "body": "..." }
実装
これを自分の Rack アプリにどう組み込むかというと
app/config.ru と lambda.rb を置いて Dockerfile の CMD では lambda.handler
を指定すると良い。
PROJECT_ROOT ├── app/ │ ├── config.ru │ ├── Gemfile │ └── Gemfile.lock ├── Dockerfile └── lambda.rb
lambda.rb は https://github.com/aws-samples/serverless-sinatra-sample/ からコピーします。*1
他のファイルはこんな感じ。
# app/Gemfile source "https://rubygems.org" gem "rack"
# app/config.ru run ->(env) { ["200", { "Content-Type" => "text/plain" }, ["OK"]] }
FROM public.ecr.aws/lambda/ruby:2.7 COPY lambda.rb ${LAMBDA_TASK_ROOT} # rack を ${LAMBDA_TASK_ROOT}/vendor/bundle に入れたい WORKDIR ${LAMBDA_TASK_ROOT}/app COPY app/Gemfile app/Gemfile.lock . RUN bundle config set path ${LAMBDA_TASK_ROOT}/vendor/bundle RUN bundle install COPY app/config.ru . WORKDIR /var/task CMD [ "lambda.handler" ]
で、実行するときは
curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"httpMethod":"GET","requestContext":{}}'
すると、Lambda の JSON response が取得できる。
{"statusCode":"200","headers":{"Content-Type":"text/plain"},"body":"OK"}
渡す JSON のパラメータは https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format を参照。
{ "resource": "Resource path", "path": "Path parameter", "httpMethod": "Incoming request's method name" "headers": {String containing incoming request headers} "multiValueHeaders": {List of strings containing incoming request headers} "queryStringParameters": {query string parameters } "multiValueQueryStringParameters": {List of query string parameters} "pathParameters": {path parameters} "stageVariables": {Applicable stage variables} "requestContext": {Request context, including authorizer-returned key-value pairs} "body": "A JSON string of the request payload." "isBase64Encoded": "A boolean flag to indicate if the applicable request payload is Base64-encoded" }
rackup した HTTP Server として Docker Image を実行したい
Lambda の Docker Image として実行するときは Lambda function for proxy integration の形式で JSON をやりとりしないといけない、というのは今まで書いてきた通り。
開発中は bundle exec rackup
しておけばいいんだけど、正しく焼けてるか不安なときがあるので、Docker Image 上の Rack アプリをブラウザから実行したい。
つまり
してくれる proxy (つまり API Gateway や ALB 相当のもの) があると便利だよね。というわけで雑にこんなのを用意しました。
コンテナを立ててた上でこの proxy を立てておくと、http://localhost/ でコンテナの中の Lambda の中の Rack アプリとやりとりできる。
絶対どこかにもっと良いやつあると思うので全然作り込んでない。(例えば multiValueHeaders
や isBase64Encoded
は対応していない)
#!/usr/bin/env ruby require "json" require "net/http" require "webrick" LAMBDA_ENDPOINT = "http://localhost:9000/2015-03-31/functions/function/invocations" s = WEBrick::HTTPServer.new s.mount_proc("/") do |req, res| uri = URI(LAMBDA_ENDPOINT) http = Net::HTTP.new(uri.host, uri.port) http.use_ssl = uri.scheme === "https" headers = {} req.each do |k, v| headers[k] = v end json_data = { httpMethod: req.request_method, path: req.path, queryStringParameters: req.query, body: req.body, headers: headers, requestContext: {}, } lambda_res = http.post( uri.path, JSON.generate(json_data), { "Content-Type" => "application/json" }, ) lambda_inner_res = JSON.parse(lambda_res.body) res.status = lambda_inner_res["statusCode"] lambda_inner_res["headers"].each do |k, v| res[k] = v end res.body = lambda_inner_res["body"] end Signal.trap("INT") { s.shutdown } s.start