LoginSignup
8

More than 5 years have passed since last update.

AWS LambdaでサーバレスSinatraカウントアプリ

Last updated at Posted at 2018-12-01

こんにちは、Life is Tech!でWebサービスプログラミングコースのメンターをしております「がはく」です。

昨日のAWS re:Inventのkeynoteで発表されリリースされたAWS LambdaのRubyランタイム対応、早速試してみました。

今回作るもの

スクリーンショット 2018-12-01 8.18.16.png
初学者のメンバーには最初に作っていただく、カウントアプリを今回作っていきたいと思います。
プラスボタンが押されたらDB内の指定のレコードの値が1足されるというような実装を行っております。
AWSよりSinatraをLambdaで動かすサンプルがアップされていたので参考にしました。
こちら→serverless-sinatra-sample

構成図

Untitled Diagram.png

node.jsでWeb作るときの構成とほぼ同じです。API GatewayよりLambda関数を呼び出し、Lambdaと相性のいいNoSQLのDynamoDBにカウントした数字を入れます。

作業環境

・git
・Ruby(2.5.0)
・AWS CLI
等は必要になるので適宜インストールしてください。

使うgem

今回は以下のgemを使用します。

Gemfile
gem 'sinatra'
gem 'rack-contrib'
gem 'aws-record'

dynamoDBを使用するためこのような構成になっています。終わったら、bundle installをかけるのをお忘れなく。

#1 コントローラの作成

基本的には/がリクエストされたらdynamoDB内の指定のレコードの数字を返し、/minusや/plusがリクエストされたらdynamoDB内の指定のレコードの数字を1ずつ増減させ、/にリダイレクトさせるような実装を行っております。

app.rb
require 'bundler/setup'
Bundler.require
require 'aws-record'

class CountTable
  include Aws::Record
  string_attr :id, hash_key: true
  integer_attr :number
  epoch_time_attr :ts
end


before do
  if CountTable.scan().empty?
    table = CountTable.new(id: SecureRandom.uuid,number: 0,ts: Time.now)
    table.save!
  end
end

get '/' do
  table = CountTable.scan()
  @number = table.first.number
  erb :index
end

get '/plus' do
  table = CountTable.scan()
  count = table.first
  number = count.number + 1
  record = CountTable.update(id: count.id,number: number,ts: Time.now)
  redirect "/"
end

get '/minus' do
  table = CountTable.scan()
  count = table.first
  number = count.number - 1
  record = CountTable.update(id: count.id,number: number,ts: Time.now)
  redirect "/"
end

#2 viewの実装

viewはよしなに作っていきます。

index.erb
<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <title>Serverless-Count</title>
</head>

<body>
<h1><%= @number %></h1>
<form action="/plus" method="get">
  <input type="submit" value="+">
</form>

<form action="/minus" method="get">
  <input type="submit" value="-">
</form>
</body>
</html>

#3 lambdaの動きの実装

この辺はAWSが提供しているサンプルをそのまま使わせていただきました。(特に弄る必要はないと思ったので)
ただ、結構queryの返し方に癖があったり、rackの動きに気持ち悪いところがあるので、時間あるときに直していきたいですね。

lambda.rb
require 'json'
require 'rack'

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

def handler(event:, context:)
  # 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" => event['queryStringParameters'] || "",
    "SERVER_NAME" => "localhost",
    "SERVER_PORT" => 443,

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

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

    # body is an array. We simply 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
    }
  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
config.ru
require 'rack'
require 'rack/contrib'
require_relative './app'

set :root, File.dirname(__FILE__)

run Sinatra::Application

#4 CloudFormationのyaml作成

こちらも先程のAWSのサンプルを参考にし、改変しました。
変えた点がテーブル名だったり、Lambda関数の名前だったりします。ここでガガッとAWS周りの設定をしていきます。

template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'

Resources:
  SinatraFunction:
    Type: 'AWS::Serverless::Function'
    Properties:
      FunctionName: CountApp
      Handler: lambda.handler
      Runtime: ruby2.5
      Policies:
        - DynamoDBCrudPolicy:
            TableName: !Ref CountTable
      CodeUri: "./"
      MemorySize: 512
      Timeout: 30
      Events:
        SinatraApi:
            Type: Api
            Properties:
                Path: /
                Method: ANY
                RestApiId: !Ref SinatraAPI
  SinatraAPI:
    Type: AWS::Serverless::Api
    Properties:
      Name: SinatraAPI
      StageName: Prod
      DefinitionBody:
        swagger: '2.0'
        basePath: '/Prod'
        info:
          title: !Ref AWS::StackName
        paths:
          /{proxy+}:
            x-amazon-apigateway-any-method:
              responses: {}
              x-amazon-apigateway-integration:
                uri:
                  !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SinatraFunction.Arn}/invocations'
                passthroughBehavior: "when_no_match"
                httpMethod: POST
                type: "aws_proxy"
          /:
            get:
              responses: {}
              x-amazon-apigateway-integration:
                uri:
                  !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SinatraFunction.Arn}/invocations'
                passthroughBehavior: "when_no_match"
                httpMethod: POST
                type: "aws_proxy"
  ConfigLambdaPermission:
    Type: "AWS::Lambda::Permission"
    DependsOn:
    - SinatraFunction
    Properties:
      Action: lambda:InvokeFunction
      FunctionName: !Ref SinatraFunction
      Principal: apigateway.amazonaws.com
  CountTable:
    Type: AWS::Serverless::SimpleTable
    Properties:
      TableName: CountTable
      PrimaryKey:
        Name: id
        Type: String
      ProvisionedThroughput:
        ReadCapacityUnits: 5
        WriteCapacityUnits: 5
Outputs:
  SinatraAppUrl:
    Description: App endpoint URL
    Value: !Sub "https://${SinatraAPI}.execute-api.${AWS::Region}.amazonaws.com/Prod/"

#5 デプロイ

ここまで来たらあとはデプロイするだけです。まずはgemを vendor/bundle に入れます。(gemごとS3にアップし、lambdaにインポートしないといけないため)

bundle install --deployment

続いて、今までのファイルをパッケージし、S3にアップロードします。(S3バゲットは事前に作っておいてください)

$ aws cloudformation package \
     --template-file template.yaml \
     --output-template-file serverless-output.yaml \
     --s3-bucket <S3 budget>

最後にこちらをCloudFormationを用いてデプロイします。

$ aws cloudformation deploy --template-file serverless-output.yaml \
     --stack-name serverless-sinatra \
     --capabilities CAPABILITY_IAM

これで完成です。
https://<hogehoge>.execute-api.ap-northeast-1.amazonaws.com/Prod でアクセスできるはずです。
カスタムドメインにしたければARMを用意して、API GatewayとRoute53でよしなにやるとできます。

まとめ

いよいよRubyにもサーバレスの時代がやってきました。
乗るしかねえこのビッグウェーブにと触ってみましたが、IoTを用いたサービス等、ミニマムなサービスをデプロイするのにすごく便利だなと思いました。これからしっかり活用して行きたいですね。

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
8