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

**※ この記事は2019年7月12日に作成した [とってもRailsライクなサーバーレスフレームワーク「Ruby on Jets」を本番環境に導入した話 - LiBz Tech Blog](https://tech.libinc.co.jp/entry/2019/07/12/113215) と同じ内容です**

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

今回が３回目のブログ投稿になります。
前回の[Kubernetes(GKE)にお安く入門する](https://tech.libinc.co.jp/entry/2019/03/22/105911)ではたくさんのブックマークをいただきありがとうございました。

今回は実際に業務での利用をしはじめた**Railsライク**なRuby製 サーバーレスフレームワーク **「[Ruby on Jets](https://rubyonjets.com)」**について書きます。

<img width="500" alt="20190710145705.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/760714/59c7b587-2692-8842-8f06-283dd0bd6b8b.jpeg">

# 経緯

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

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

<img width="200" alt="20190711145732.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/760714/b6bf008d-65fb-fff5-4f95-85d6efe15490.png">

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

# 構成

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

![20190710195302.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/760714/1cfbf0a9-7ffa-3b48-3250-d7339700594c.png)

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

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

上記の3つだけでした。  

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

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

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


# 技術選定

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

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

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

の２つです。

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

<img width="300" alt="20190711144928.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/760714/fa2f1381-faa7-d492-1b4d-e53a81bd69f5.png">


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

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

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

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

- [Ruby on Jets](https://rubyonjets.com/)
  今回はこのRuby on Jetsを採用しました。
  実際に使ってみると驚きます。RailsライクどころかほぼRailsでした。
  詳しくは[こちらのQiita](https://qiita.com/gotchane/items/fd3c1e54f7bdaa4ab813)にわかりやすくまとまっているのですが、routesに書いたルーティングがそのままAPI Gatewayに反映され、Controllerにで書いた処理がLambdaに反映されたりします。  
  
Jetsに決めたものの、メインコミッターが一人だけだったり、3日に１回のペースでアップデートされている等いろいろと不安定な部分も多いので何か問題が起きた場合は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](https://www.keisuke69.net/entry/2017/06/21/121501)



ちなみに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でリクエストされた場合は`HogeController`の`def 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ポリシー

[こちら](https://rubyonjets.com/docs/extras/minimal-deploy-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できますね。

<img width="200" alt="20190711145146.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/760714/84ee2a66-a67e-f2f7-3421-0632f5999af5.png">


### デプロイ方法

デプロイは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回の頻度でくるアップデートに対応していく覚悟も必要です。笑)

https://rubyonjets.com


# おまけ

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

Docker imageは [softwaremill/elasticmq](https://hub.docker.com/r/softwaremill/elasticmq) を利用させていただきました。


```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上で動作するアプリケーションをローカルで開発するためのツールも出揃ってきていて開発者にとっては大変ありがたいですね。