AWS と Ruby + Sinatra で自分用に本棚アプリを作った
要約
冬休みに AWS を勉強していて実践してみたくなったので簡単なアプリを作ることにした.作ったのは自分のホームページの管理アプリ.読んだ本や映画の感想を一覧にしているのだが,シンプルにhtml直書きのソースゆえ,いちいち手打ちで <tr>
とか書かなきゃいけなくて管理がしんどかったので,Slack に投稿するだけで html をいい感じに書き換えてくれるアプリを作ることにした.
タイトルにもあるとおり,登場人物は AWS,Ruby+Sinatra,Slack API の3名である.
完成品はこちら
アプリの仕様
/addtonoerpage 種類 タイトル 監督 視聴のきっかけ 感想
アプリをインストールした Slack ワークスペースでこんな感じでスラッシュコマンドを打つ.種類は本(book),映画(film),アニメ(anime) の3種類.コマンドの実行が成功すると html を書き換えて新しいブランチにコミットを積んでくれる.登録情報と作成したブランチへのリンクが以下のように返ってくる.
ブランチへのリンクを踏んでPRを作成してマージすればホームページの本棚が更新されるという算段.こんな感じのPRが出来上がる.
欲を言えば PR を作るところまでやりたかったが,そのためには GitHub の API を叩かなきゃいけないっぽくて面倒だったのでブランチの作成に止めた.
スラッシュコマンドのフォーマットが正しくない場合はこんな感じで怒られるようにした.
大まかな構成
Slack API
Slack アプリの受け口.今回はスラッシュコマンドのみ利用.他にも,botにメンションを送るなどのイベントをトリガとすることもできる.
AWS Elastic Beanstalk
スラッシュコマンドが叩かれると Slack API は設定された URL に HTTP POST を送る(参考).ローカル環境でこのポストを受け取れるようにするのはちょっと面倒だったので AWS を利用することにした.AWS 便利.
ちょうど友人と遊びでアプリ開発をしていて,その時に Elastic Beanstalk を使ってみてたところだったので,今回も同じく Elastic Beanstalk を利用することにした.
AWS 上で Web アプリが動く環境を整えようとすると,EC2・S3・ELBなどなど,さまざまな設定を行う必要がある.Elastic Beanstalk はその辺りの各種設定をいい感じに行ってくれて,利用者はアプリ本体の開発にのみ専念できる.最低限動くソースが提供されていたので,今回のアプリ開発はそれに手を加えるだけでよくてお手軽だった.
アプリ本体(Ruby+Sinatra)
Slack API から HTTP POST を受け取って処理を実行する部分.今回は作品情報を受け取ってファイルを書き換えるだけの簡単な処理なので,手軽にWebアプリケーションを作成できるSinatraを利用した.
処理の流れはざっと以下
- 受け取った作品情報のフォーマットを検証.
- 検証結果に基づきレスポンスを返却.フォーマットが正しい場合はファイル書き換えに必要な情報をインスタンス変数に保持しておく.
- レスポンス返却後,インスタンス変数に格納した情報を元に html ファイルを書き換えてコミットを積む.
先にレスポンスを返してからファイル編集の処理を行っているのはスラッシュコマンドのタイムアウト除けのため.3秒以内にレスポンスを返せなければタイムアウトとして処理されてしまうから.結構シビア.
デプロイ方法
私以外にこのアプリをデプロイする旨みのある人はいないけれど,アプリ作成の参考になるかもしれないのでデプロイ方法を詳しめに書いておく.
AWS Elastic Beanstalk の設定
AWS Elastic Beanstalk 上でアプリケーションを作成する.この辺りを参考にいい感じに設定していってください. ざっと設定を列挙します.
項目 | 値 |
---|---|
環境枠 | Webサーバー環境 |
プラットフォーム | Ruby |
プラットフォームのブランチ | Ruby 2.7 running on Amazon Linux 2 |
プラットフォームのバージョン | 3.2.1 |
アプリケーションコード | コードのアップロード |
ソースコード元 | ローカルファイル |
ローカルファイルは以下のようにして作成した zip ファイルを選択.
git clone https://github.com/NomotoEriko/NoerPageManager.git cd NoerPageManager/bookshelf zip ../bookshelf.zip -r * .[^.gD]*
以上を設定し環境の作成をクリックする.
しばらく待つと環境が作成されてアプリが立ち上がる.画面にアプリケーションへのURLが表示されるのでそのリンクをクリックして,画面に OK が表示されたらデプロイ成功.環境変数の設定がまだなので,この時点ではデプロイに失敗するかもしれない.
Elastic Beanstalk の管理画面でサイドバーからこの環境のタブを開き,設定をクリックする.ソフトウェアとかインスタンスとかの設定が一覧で表示される.ソフトウェア設定の編集から環境プロパティを編集できるので,本アプリに必要な各種環境変数を設定する.
変数名 | 値 |
---|---|
GIT_REPOSITORY_URI | ホームページのリポジトリのURI.認証を通すためにhttps://USER_NAME:PASSWORD@github.com/USER_NAME/REPOSITORY_NAME.git というフォーマットにしておく必要がある. |
GIT_USER_NAME | コミットを積むユーザー名.雑にNoerPageManagerにした. |
GIT_USER_EMAIL | コミットを積むユーザーのメールアドレス.雑に自分のメアドを設定した. |
ちなみに GIT_REPOSITORY_URI の PASSWORD は GitHub で 2 段階認証をしている場合はパスワードではなくアクセストークンとなる.作成方法はこちらを参照.
環境変数を設定すると再度デプロイが走るのでしばらく放置する.
Slack API の設定
公式ドキュメントを参考に Slack アプリを作成する.Slack アプリの Slash Commands からスラッシュコマンドを新規作成する. Request URL には先ほど AWS で作成した Web アプリのURLの末尾に /add
をくっつけたものとする.
好きなワークスペースにこの Slack アプリをインストールして完了.
工夫したポイント
開発してて工夫したポイントを,開発時の時系列順に書いていく.
手元で動作確認する
いくら手軽とはいえ動作確認にいちいち Elastic Beanstalk を経由していたらキリがないので,早い段階で手元で動作確認する方法を確立した.
ローカルでのアプリの立ち上げ
これはそんなに難しくない. bundle exec rackup -o 0.0.0.0
で立ち上がる.
Slack API からのリクエストを模倣する
これには Postman を利用した.
Slack API のドキュメントをよく読むと,スラッシュコマンドのデータは application/x-www-form-urlencoded
形式で送られると書いてあった.
This data will be sent with a Content-type header set as application/x-www-form-urlencoded.
Postman アプリでこんな感じで x-www-form-urlencoded を選択して POST を送ることで Slack API からのリクエストを模倣することができた.
Elastic Beanstalk でデプロイに失敗した時の原因調査
いくら手元で動作しても,Elastic Beanstalk でのデプロイで失敗することはある.Elastic Beanstalk を使い慣れていないこともあり,原因調査に戸惑った.
とりあえずログを確認してみる
Elastic Beanstalk の環境管理画面に「ログ」という項目があったので開いてみると,「ログのリクエスト」からログを取得することができた.ただ...正直あまり参考にならなかった.ちゃんと設定すればええんやろうなぁと思いつつ,ログは参考程度に見るだけに留めた.
EC2 インスタンスを見にいく
デフォルトだと EC2 はキーペア(ssh 接続に利用する公開鍵と秘密鍵のペア)無しで作成されるのでアクセスできない.環境変数を設定するのに利用した「設定」画面のセキュリティ欄にEC2 キーペアという項目を見つけたので,そこからキーペアを登録した.
キーペアさえあれば EC2 に ssh 接続できる.具体的な接続方法についてはこちらを参照.
EC2 インスタンスに接続してとりあえずその場で ls
してみたけれど,なんにもなくて一瞬面食らった.めげずにどこかにあるはずのソースコードを探そうと find / -name app.rb 2>/dev/null
を実行./var/app/current/
に目当てのコードがあることがわかったのでそこに移動した.
あとはソースのディレクトリの状態を確認したり,環境変数を確認したり,その場で ruby のコンソールを立ち上げてみたり,ログを確認してみたりといういつものデバッグ作業を行うだけ.
ここまではどちらかといえば作業環境を整える上での工夫だった.以降は毛色が変わって実装上の工夫について話す.
Ruby から git 操作を行う
ローカル環境で git 操作を行うのはそんなに難しくなかった.普段行っている git 操作を Ruby 経由で叩くだけだから.いちいち Kernel.#system を呼ぶのは面倒だったので gem を導入した.
躓いたのは AWS の EC2 インスタンスで上での git 操作.
git をインストールする
まず引っかかったのは,そもそも git が環境に入っていなかったこと.これは .ebextensions を設定することで解決した.
本棚アプリのソースを参照してもらえれば,.ebextensions フォルダ内に git インストール用のファイルが入っているのがわかる.
github の認証を通す
何も考えずにソースをデプロイしたら,デプロイ自体には成功しているけれどスラッシュコマンドの実行に失敗していた.EC2 インスタンスに ssh 接続して原因を調査したところ,ブランチの作成,コミットまではうまくいっていたが,どうやらブランチのプッシュに失敗しているようであった.試しにその場で ブランチをプッシュしようとしたら,ユーザー名とパスワードを聞かれた.Ruby から push を成功させるにはユーザー名とパスワードを聞かれないようにする必要がある.
Qiita とかで調べたところ,HTTPS でユーザー名,パスワードを聞かれないようにするには,origin の URL にユーザー名とパスワードを含めれば良いということがわかった.GIT_REPOSITORY_URI をそのように変更して無事認証の問題が解決した.
3秒以内にレスポンスを返す
認証も通ったしこれで動くやろと勇んでスラッシュコマンドを実行したら,今度はタイムアウトで失敗した.Slack 上では失敗しているけれど,実はブランチのプッシュまで行われているという惜しい状況だった.
Slack API の仕様上,レスポンスは 3 秒以内に返さないとタイムアウトとして処理されるらしい.ブランチを作成してコミットを積んでブランチをプッシュするのを3秒以内に完了するのは不可能なので,先にレスポンスを返してからそれらの処理を行う必要がある.
重い処理を分離
何はともあれとりあえず重たい処理を分離することは必須だったので,ひとまずそれを行った.軽い処理はリクエストデータを評価してレスポンスを構成する部分,重い処理はそれ以外という分け方にした.
次に重い処理に必要な材料をリクエストデータなどから構成してインスタンス変数に入れた.これによりインスタンス変数を介して軽い処理と重い処理を分離できる.あとは重い処理をメソッドに切り出して完了.
after の利用
Sinatra には before と after というフィルタがあり,それぞれリクエストのルーティングの前後で呼ばれる.とりあえず after に重い処理を移して試してみた.だが,after 評価後にレスポンスを返すらしく,効果がなかった.(ドキュメントをちゃんと読んだら書いてあった)
Thread の利用
Thread は並行プログラミングを行うための Ruby のクラスである.簡単にいうと,同時に複数の処理を行う,みたいなことを実現することができる.
Thread を生成してその中で重い処理を行えば,レスポンスを返しつつ重い処理を並行して行うことができる.
Mutex の利用
重い処理実行中に新たなリクエストがきた場合,ファイルとかコミットログとか branch がおかしなことになることが容易に想像できる.同じファイルを複数人で編集している場面を想像してもらえれば良いと思う.このようなことを防ぐため,重い処理を行っている最中は新たな重い処理を実行しないようにする必要がある.ちなみに,このような制御を排他制御という.
今回は Mutex クラスを利用して実装した.
グローバル変数に Mutex インスタンス m を置いておき,重い処理中は m をロックする.スラッシュコマンド受け取り時に m の状態をみて,ロック中であれば「処理中です。少し時間をおいて再度お試しください。」というメッセージをレスポンスとして返すことにした.
おわりに
AWS の実践をしたくて作り始めたアプリだけれど,github の認証とか Ruby での排他制御の実装とか思いがけない収穫があった.楽しかった.
このアプリは AWS の本を読む合間に息抜きがてらゆるっと 2 日で作った.ずっと本を読むだけだとどうしても集中力が切れてしまうのでね.
Slackのスラッシュコマンドを作って遊んでたら遅い時間になっててびっくりした。大急ぎで教科書のS3の章に目を通した。ノルマ達成!晩御飯つーくろ!
— ノエル (@saya_hakuren) 2020年12月30日
スラッシュコマンド実行したらgitのブランチが作られてほしくてローカルではそれなりに動くんだけど、デプロイすると動かんかってんな。たぶん認証周りで死んでる。あと一歩!
— ノエル (@saya_hakuren) 2020年12月30日
認証の問題はクリアしたけど、今度はタイムアウトに引っ掛かるようになってしもうた。内部的には動いてるっちゃ動いてるんだよなぁ。
— ノエル (@saya_hakuren) 2020年12月30日
時間がかかる処理を切り離して、レスポンス先に返してから残りの処理を行えば良いんだろうが…
よっし重たい処理を非同期にしてタイムアウト問題を解決した.ちゃんとMutexを使ってスレッドセーフにもしたぞ!
— ノエル (@saya_hakuren) 2020年12月31日
このブログを書いて思ったけど,重い処理をわざわざ after のなかでやらんでもええなぁ.ま,いっか.