LiBz Tech Blog

LiBの開発者ブログ

とってもRailsライクなサーバーレスフレームワーク「Ruby on Jets」を本番環境に導入した話

はじめに

こんにちは!先日26歳を迎え、30歳への恐怖感が着々と増してきた渡邊です。

今回が3回目のブログ投稿になります。
前回のKubernetes(GKE)にお安く入門するではたくさんのブックマークをいただきありがとうございました。

今回は実際に業務での利用をしはじめたRailsライクなRuby製 サーバーレスフレームワーク Ruby on Jetsについて書きます。

f:id:kkwatanabe:20190710145705j:plain:w500

経緯

自分が開発を担当しているプロダクトには、求職者の方と弊社のキャリアアドバイザーがLINEを介してメッセージのやりとりができるような機能があります。

この機能はLINEのMessaging APIを利用して実装されているのですが、求職者が送信したメッセージを取得する方法が「webhookでデータを受け取る」という手段しかなく、
webhookが弊社サーバーにリクエストされた際にサーバーに何らかの障害が発生している場合、もしくはメンテナンス状態だった場合に、求職者の方が送信したメッセージが消失してしまうという大きな問題がありました。

f:id:kkwatanabe:20190711145732p:plain:w200

そのためECSで運用されている弊社のサーバーの生死に影響されないようなサーバーレスなシステムを構築し、LINEメッセージの受信をする処理はそちらで捌くように改修することになりました。

構成

最初にどのような構成になったかを紹介してしまいますが、最終的に現在の本番環境は以下のようになりました。

f:id:kkwatanabe:20190710195302p:plain

サーバーレスアーキテクチャといっても今回必要な機能は、

  1. LINEサーバーからのwebhookを受け取る
  2. 本当にLINEサーバーからのリクエストなのかを検証する
  3. webhookで受け取ったデータを保存する

上記の3つだけでした。

データの保存にはDynamoDBを利用する話もでていましたが最終的にはSQSのFIFOキューを利用することになり、

LINEサーバー → API Gateway → Lambda → SQS(FIFOキュー) ← 既存アプリケーション

という非常にシンプルな構成になりました。

技術選定

Lambdaのランタイム(言語)は何で実装するのか?

Lambdaが担当することになる処理は、

  • LINEサーバーからのリクエスト(webhook)なのかを検証
  • リクエストされたデータをSQSのキューに保存

の2つです。

「LINEサーバーからのリクエスト(webhook)なのかを検証する処理」に関してはすでにRailsでも実装されている処理のため、コードを使い回せるRubyが1番楽かなーと考えてはいたものの、
LambdaがRubyに対応したのは2018年12月と、比較的最近なため不安もありました。

f:id:kkwatanabe:20190711144928p:plain:w300

サーバーレスアーキテクチャの構成管理に何を利用するのか?

サーバーレスアーキテクチャでネックになってくるのが構成管理です。
Lambdaだけならコードのバージョン管理ができるので問題ないのかもしれませんが、コードと一緒にAPI Gateway等の設定も管理するとなると何らかのツールを利用した方がよいです。

  • Serverless Framework
    おそらくサーバレスアーキテクチャのツールとして一番利用されており、ドキュメントや日本語記事もかなり多いです。
    自分もLiBに来る前の会社ではよくお世話になっていました。
    個人的には安定性をとるなら Serverless Framework + Node になるのかなと。

  • AWS SAM
    AWS公式のサーバーレスアプリケーション構築のためのフレームワーク。
    ローカル環境に擬似API Gatewayサーバーを立てたりできるSAM Local(今はSAM CLIという名前らしい)があってデバッグが非常に楽だったのですが、
    自分が使用していたときはバグが多くて辛かったです、、
    (今は修正されていると思います!)

  • Ruby on Jets
    今回はこのRuby on Jetsを採用しました。
    実際に使ってみると驚きます。RailsライクどころかほぼRailsでした。
    詳しくはこちらのQiitaにわかりやすくまとまっているのですが、routesに書いたルーティングがそのままAPI Gatewayに反映され、Controllerにで書いた処理がLambdaに反映されたりします。

Jetsに決めたものの、メインコミッターが一人だけだったり、3日に1回のペースでアップデートされている等いろいろと不安定な部分も多いので何か問題が起きた場合はServerless Frameworkに乗り換えることも視野にいれての採用でした。
(実装がそこまで複雑ではないため乗り換えコストもそれほどないだろうと判断しました。)

Jetsをちょっとだけ解説

プロジェクトの作成

rails new よろしく、 jets new コマンドでプロジェクトを作成できます。
モードはAPIを指定。また、今回はDBを使用しないため --no-database オプションも指定しました。

$ jets new プロジェクト名 --mode api --no-database

JetsはMySQLやPostgreSQLといったRDBとDynamoDBに対応していますが、RDBとLambdaはコネクションの関係で相性が非常に悪いとされているので利用するとしたらDynamoDBになるのでしょうか。

www.keisuke69.net

ちなみにDynamoDBをActiveRecordのようにマイグレーション管理したり、CRUD操作を楽に扱えるようにするgem dynomiteの作者もこのJetsの開発者だったりします。

ルーティングの設定

# config/routes.rb

Jets.application.routes.draw do
  get  'hoge', to: 'hoge#huga'
  post 'foo',  to: 'foo#bar'
end

Railsと全く同じです。 上記のように設定すると、 /hoge にgetでリクエストされた場合はHogeControllerdef huga が実行されます。

実際はAPI GatewayにGET /hogeでアクセスがきた際にhugaメソッドのコードを実行するLambda functionを紐づけています。

コントローラー

RailsではActionController::Baseを継承した各種Controllerを作成するのが基本だと思いますが、JetsではJets::Controller::Baseを継承したControllerを作成します。

# app/controllers/hoge_controller.rb

# Jets::Controller::Baseを継承したApplicationControllerを継承しています
class HogeController < ApplicationController

  def huga
    response_body = {
      hello: 'world!!',
      request_params: {
        headers: event['headers'],
        body: event['body'],
        query_parameters: event['queryStringParameters'],
        path_parameters: event['pathParameters']
      } 
    }

    render json: response_body
  end

end

Lambdaは何をトリガーにして起動したかによってevent変数も変わってくるのですが、API Gatewayの場合は上記のように簡単にリクエストパラメータを取得できます。

必要なIAMポリシー

こちらに記載されている権限が必要になります。 今回の本番導入では DynamoDB と Route53 は使用しなかったので不要でした。

CloudFormationの権限が必要なのは最終的に構成がCloudFormationのテンプレートとして変換/実行されるためです。
これはJetsに限ったことではなく、ほとんどのサーバレスフレームワークはAPI Gateway等のリソースの設定をCloudFormationのテンプレートに変換することで構成管理を行っています。

シークレットキーなどの扱い方

Railsでいうconfig/secrets.yml です。
残念ながらJetsにはsecrets.ymlは存在しませんがenvファイルを使用することで同等の扱い方をすることができました。

# .env.development
SECRET_KEY_BASE=abcdefg
SECRET_ACCESS_KEY=12345
SECRET_ACCESS_TOKEN=7890

上記のように記述することでLambda functionの環境変数に設定され、ENV['key_name']で取得できるようになります。

AWSのSSM Parameterにも対応しており、下記のように記述することもできます。

# .env.production
SECRET_KEY_BASE=ssm:/secret_key_base
SECRET_ACCESS_KEY=ssm:/secret_access_key
SECRET_ACCESS_TOKEN=ssm:/secret_access_token

もちろんSSM周りの権限も必要になってきますが、あらかじめSSMにパラメータを設定しておくことでsecret_keyのハードコーディングを避けることができます。
これならgithubにもpushできますね。

f:id:kkwatanabe:20190711145146p:plain:w200

デプロイ方法

デプロイはjets deployコマンドで行います。

# デプロイ
$ AWS_PROFILE=[profile名] bundle exec jets deploy [環境名]

# デプロイ済みのリソースを削除
$ AWS_PROFILE=[profile名] bundle exec jets remove [環境名]

さらにJetsにはBlue-Greenデプロイ用のコマンドも用意されています。

# Blue-Greenデプロイ
$ AWS_PROFILE=[profile名] JETS_ENV_EXTRA=[1~9の番号] bundle exec jets deploy [環境名]

JETS_ENV_EXTRA で番号を指定してデプロイすることで hoge-resources-[環境名]-[1~9の番号] といったリソースが作成され、
十分な検証を行った後にリソースのエンドポイントを切り替えるといったような運用をすることができます。

https://rubyonjets.com/docs/env-extra/

最後に

今回はたまたま規模が小さい要件だったのでJetsの利用に踏み切れましたが、大規模なサービスで利用するにはまだ成熟度が足りない印象です。

とはいえ自分的には既存のアプリケーションとサーバーレスアプリケーションの違いがどんどんなくなってきているなと、驚きを感じたプロダクトでした。
そのうちアプリケーションエンジニアはサーバー(インフラ)を全く気にせずに開発できるようになったりするのでしょうか?笑

Jetsの機能的にもまだ半分も使いこなせていないのではないかなーとドキュメントを読みながら思う今日このごろです。
ぜひRubyを代表する1大プロダクトへと成長してほしいですね!
(これからJetsを使う人は3日に1回の頻度でくるアップデートに対応していく覚悟も必要です。笑)

rubyonjets.com

おまけ

今回SQSを扱う開発をするにあたり、ローカル環境に疑似SQSを構築できるElasticMQが大変便利だったので紹介します。

Docker imageは softwaremill/elasticmq を利用させていただきました。

hub.docker.com

# docker-compose.yml
version: '3.2'
services:
  jets: 
    ..省略..

  local_sqs:
    image: softwaremill/elasticmq
    container_name: local_sqs
    ports:
      - "9324:9324"
    volumes:
      - local_sqs.conf:/opt/elasticmq.conf

local_sqs.conf をElasticMQの/opt/elasticmq.confにマウントすることでキューをカスタマイズすることができます。
今回はFIFOキューを利用したかったので以下のように設定しました。

# local_sqs.conf
include classpath("application.conf")

node-address {
    protocol = http
    host = local_sqs
    port = 9324
    context-path = ""
}
rest-sqs {
    enabled = true
    bind-port = 9324
    bind-hostname = "0.0.0.0"
    sqs-limits = strict
}
generate-node-address = false
queues {
    "キューの名前.fifo" {
        fifo = true
    }
}

キューの名前に.fifoをつけているのはSQSで作成したFIFOキューは接尾に.fifoが自動で付与されるためです。

最近はAWS上で動作するアプリケーションをローカルで開発するためのツールも出揃ってきていて開発者にとっては大変ありがたいですね。