※ この記事は2019年7月12日に作成した とってもRailsライクなサーバーレスフレームワーク「Ruby on Jets」を本番環境に導入した話 - LiBz Tech Blog と同じ内容です
はじめに
こんにちは!先日26歳を迎え、30歳への恐怖感が着々と増してきた渡邊です。
今回が3回目のブログ投稿になります。
前回のKubernetes(GKE)にお安く入門するではたくさんのブックマークをいただきありがとうございました。
今回は実際に業務での利用をしはじめたRailsライクなRuby製 サーバーレスフレームワーク **「Ruby on Jets」**について書きます。
経緯
自分が開発を担当しているプロダクトには、求職者の方と弊社のキャリアアドバイザーがLINEを介してメッセージのやりとりができるような機能があります。
この機能はLINEのMessaging APIを利用して実装されているのですが、求職者が送信したメッセージを取得する方法が「webhookでデータを受け取る」という手段しかなく、
webhookが弊社サーバーにリクエストされた際にサーバーに何らかの障害が発生している場合、もしくはメンテナンス状態だった場合に、求職者の方が送信したメッセージが消失してしまうという大きな問題がありました。
そのためECSで運用されている弊社のサーバーの生死に影響されないようなサーバーレスなシステムを構築し、LINEメッセージの受信をする処理はそちらで捌くように改修することになりました。
構成
最初にどのような構成になったかを紹介してしまいますが、最終的に現在の本番環境は以下のようになりました。
サーバーレスアーキテクチャといっても今回必要な機能は、
- LINEサーバーからのwebhookを受け取る
- 本当にLINEサーバーからのリクエストなのかを検証する
- 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月と、比較的最近なため不安もありました。
サーバーレスアーキテクチャの構成管理に何を利用するのか?
サーバーレスアーキテクチャでネックになってくるのが構成管理です。
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になるのでしょうか。
参考: なぜAWS LambdaとRDBMSの相性が悪いかを簡単に説明する - Sweet Escape
ちなみにDynamoDBをActiveRecordのようにマイグレーション管理したり、CRUD操作を楽に扱えるようにするgem dynomite
の作者もこのJetsの開発者だったりします。
ルーティングの設定
Jets.application.routes.draw do
get 'hoge', to: 'hoge#huga'
post 'foo', to: 'foo#bar'
end
Railsと全く同じです。
上記のように設定すると、 /hoge
にgetでリクエストされた場合はHogeController
のdef huga
が実行されます。
実際はAPI GatewayにGET /hoge
でアクセスがきた際にhuga
メソッドのコードを実行するLambda functionを紐づけています。
コントローラー
RailsではActionController::Base
を継承した各種Controllerを作成するのが基本だと思いますが、JetsではJets::Controller::Base
を継承したControllerを作成します。
# 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できますね。
デプロイ方法
デプロイは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の番号]
といったリソースが作成され、
十分な検証を行った後にリソースのエンドポイントを切り替えるといったような運用をすることができます。
最後に
今回はたまたま規模が小さい要件だったのでJetsの利用に踏み切れましたが、大規模なサービスで利用するにはまだ成熟度が足りない印象です。
とはいえ自分的には既存のアプリケーションとサーバーレスアプリケーションの違いがどんどんなくなってきているなと、驚きを感じたプロダクトでした。
そのうちアプリケーションエンジニアはサーバー(インフラ)を全く気にせずに開発できるようになったりするのでしょうか?笑
Jetsの機能的にもまだ半分も使いこなせていないのではないかなーとドキュメントを読みながら思う今日このごろです。
ぜひRubyを代表する1大プロダクトへと成長してほしいですね!
(これからJetsを使う人は3日に1回の頻度でくるアップデートに対応していく覚悟も必要です。笑)
おまけ
今回SQSを扱う開発をするにあたり、ローカル環境に疑似SQSを構築できるElasticMQが大変便利だったので紹介します。
Docker imageは softwaremill/elasticmq を利用させていただきました。
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キューを利用したかったので以下のように設定しました。
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上で動作するアプリケーションをローカルで開発するためのツールも出揃ってきていて開発者にとっては大変ありがたいですね。