Railwayの紹介
フィヨルドブートキャンプ Part 2 Advent Calendar 2022 の8日目の記事です。(Part1はこちら)
昨日は、ham-capさんの「Vimが好きだ」でした。
目次
はじめに
Minoru MaedaさんがHeroku の代替 OSS を試した話を書かれておりますが、私もHerokuの代替として使用できそうなRailwayというホスティングサービスについて簡単に紹介をします。
プラン
プランは以下の3つあり、私が使ったStarter Plan
とDeveloper Plan
について説明します。
- Starter Plan
- Developer Plan
- Team Plan Offering
Starter Plan
メモリは512MB、最大1GBのディスク容量、メモリは共有型になってます。
価格は無料ですが、上限が決まっており、毎月5ドル分のリソースを使いきるか、500時間(約20日)使用することができます。
逆にいうと、5ドル分のリソースを使用するか500時間稼働させるとデプロイしているサービスが停止されます。
上記のため、毎月約10日くらいはサービスが停止することになります。
Developer Plan
メモリは8GB、最大100GBのディスク容量、メモリは8Coreになってます。
価格はStarter Planと一緒で5$まで無料で、5$を超えたリソースを使用すると課金されます。
しかし、5$まで無料であるので、それを超えるリソースを使用しない限り無料であり、サービスが月の途中で停止される心配はありません。
また、5$を超えない限りでリソースを使用しながら、スペックはStarter Planより大幅にアップデートされます。
そのため、私が自作したWebサービスFjordBootCamp Contiburotrsでは、Developer Planを使用しています。
デプロイ
Githubのリポジトリと連携させることで、mainブランチにマージされたタイミングで自動でデプロイをしてくれるようになります。
また、ブランチも選択できるので、mainではないブラントをデプロイしたい時は、設定から変更できます。
CLI
Railwayは、コマンドも使うことができ、ローカルのディレクトリをRailwayにデプロイすることができます。
詳細に知りたい方はこちら。
使うであろう代表的なコマンド以下に紹介します。
インストール
NPMをインストールします。
npm i -g @railway/cli
ログイン
railwayにコマンドでログインします。 以下を実行するとブラウザが立ち上がります。
railway login
ブラウザを立ち上げない場合は、以下のように--browserless
をつけます。
railway login --browserless
コマンド実行
railway run
を使うことで、Railwayの環境でコマンドを実行することができます。
私の場合、railsでバッチ処理を実行しており、以下の様にgithub actionsにてrailway runを使用しています。
name: run batch run: railway run bundle exec rails runner lib/batch/contributor_commit_collector.rb env: RAILWAY_TOKEN: ${{ secrets.RAILWAY_TOKEN }}
デプロイ
デプロイしたいプロジェクトディレクトリに移動し、以下のコマンドを実行します。
cd [プロジェクトディレクトリ] railway up
これだけで、ローカル環境をRailwayにデプロイできます。とても簡単ですね!
スケジューラ
Railwayではスケジューラはサービスとしてありません。
そのため、私はGithub Actionsを使い、Railway環境でバッチ処理を実装しております。
私のworkflowファイルはこちらです。
Github Actionsは時間通り動かないことがありますが、私の自作サービスの場合は、そこまできっちり時間通りにランキングを更新することは要件として重要ではないのでGithub Actionsで十分スケジューラとして機能しています。
おわりに
以上、Railwayというホスティングサービスについての紹介記事でした。
Railwayについては日本語記事が少なくあまり見かけることが少なかったのでここで紹介させていただきました。 詳しくRailwayについて知りたい方はこちらのDocumentを参考にしてください。
個人的にはシンプルで使いやすいという印象です。
想定外の事が起こり、日がずれちゃいましたが、なんとか記事を書かせてもらいました💦
明日はchocoさんが記事を書いてくださいます。
フィヨルドブートキャンプのシステム開発プラクティスにて、今までのコミット数ランキング化したアプリ「FjordBootCamp Contributors」をリリースしました
はじめに
フィヨルドブートキャンプの最終課題である自作サービスとして、bootcampアプリにPRを送ってマージされているかをランキング形式で確認できるFjordBootCamp Contributorsをリリースしました。 今までPRを送ってマージされたことのある人達のコミット数を期間別にランキング形式で表示されています。
目次
自己紹介
rosh-1228と申します。前職はSESの会社にて、ITインフラの運用/保守とSAP BASISをメインに設計/構築/運用/保守など仕事としておりました。
プログラミングをメインに仕事をしたいと思い、FjordBootCamp(フィヨルドブートキャンプ)というプログラミングスクールにて勉強をしております。
サービスについて
FjordBootCamp Contributorsは、元となったサービスがあり、Rails Contributersというものがあります。
こちらは、Railsに対して、PRを送った人をランキング形式で表示しており、期間別に今月
今年
今週
今日
コミットした日から今までの期間
で表示されてます。
FjordBootCampでもRails Contributorsのように誰がどれくらいbootcampアプリにPRを送ってマージされているか確認されたいとあり、私がサービスを作らせていただきました。
FjordBootCamp Contributorsも、今月
今年
今週
今日
コミットした日から今までの期間
の5つの期間別に、FjordBootCampAppにてマージされたPRに含まれるコミットの数をランキング形式で表示しております。
FjordBootCampAppの開発を行なっている人(システム開発プラクティスに参加されている生徒やメンターの方々)向けのサービスになります。
過去にコミットを行ったことがある人も、All Timeで自分がどのくらいコミットしたのか確認できます。
ちなみにAll Timeで自分は21位でしたw
また、各コントリビューターの今までのコミット一覧を確認することができます。
自分が今までどのようなコミットメッセージをしたのかを確認し、今後もっとわかりやすいメッセージに修正しようと見直すこともできます。
このランキングで、他の人よりも技術力がある・なしを図るものではないということだけ留意いただきたいです。
どこで使用するのか?
それでは、どういった場面を想定しているかというと、以下の使用場面を想定しています。
- システム開発プラクティスで自分の今月や今週のコミットランクを確認して、やる気をあげる時
- 就職活動で企業へ自分のコミット(どのようなコードを書いて貢献したのか)を見せる時
- FjordBootCampで行われるイベントで、参加者全員でランキングを見て、誰がどんなコミットをしたのかなど話して盛り上がる
contribute数の集計時間
1日4回(0時/6時/12時/18時)Githubからコミット数を取得し、ランキングを表示します。
※Github Actionsを使用しているため、タイムラグが生じる可能性もあります。参考
技術スタック
- Ruby on Rails 6.1.7
- Ruby 3.1.2
- Rsoec
- React.js
- GraphQL
- PostgreSQL
- Hosting
- Railway
- CI/CD
- Github Actions
- 定期処理
- Github Actions
開発中に苦労したこと
触ったことがあるものはRuby on Railsのみで、他の技術スタックは0からのスタートでしたので、かなり時間がかかりました。
プログラミングより環境を整えることに悩み時間をかけたと思います。
Githubから欲しい情報を入手すること
1つ目に、GithubからFjordBootCampAppのコントリビューターとコミットを全て取得する必要があり、これをGraphQLを使って実現しようとしました。
しかし、1つのリポジトリに所属しているコントリビューター全員のコミットを取得するといったことにはあまり関心を持っている方はおられず、調べても参考になりそうなものはなく、書き方だけ学びました。
そのため、Githubの公式リファレンスを参考にexplorerで何回も試しました。 ここに時間を最も使ったと思います。
また、Githubのapiは1度に100件しかコミット情報を取得することができず、FjordBootCampAppは10000件を超えるコミット数でした。
全コミット数/100回も繰り返しでGithubへ問い合わせすることになり、これで本当に良いのかと悩み他の方法を調べることにも時間を費やすほどでした。
https://docs.github.com/ja/graphql/overview/resource-limitations
今回はバックグラウンドで処理を行うので、サービスの操作が重くなるようなことはありませんが、ユーザーが何らかの登録や表示といった操作を行うサービスを作る際は、操作性について必ず考慮する必要があるなと思いました。
React.jsを初めて使った
今回、FjordBootCampのカリキュラムで勉強したVue.jsではなく、React.jsを使うことにしました。 これは、Vue.jsとReact.jsを比較して使うことを決めたとかはなく、React.jsを使ったことがないので、使ってみようという興味から決めたことです。 ただ、React.jsも使ったことがなかったので、どう書けば良いのかなど初めはわからず苦労しました。 特に、webpackerを使ってビルドする際に足りないモジュールがいくつかあったりして、システムテストでReact.jsを使ったコンポーネントが画面に描画されずに悩みました。
ホスティングサービス選択
Herokuが有料になるということもあり、代わりのホスティングサービスを探すことも苦労しました。
今回はお金をかけずにサービスを作りたいと思っていましたので、Herokuが使えないとなると完全に代替できるサービスはありませんでした。
調べるとFly.ioとRailwayのどちらかが有力な候補となりそうということがわかりました。
初めはFly.ioの方が無料で提供してくれるサーバーのスペックが良さそうだと思い、試しにデプロイを行なってみましたがここで挫折して使うことをやめました。
なぜなら、Rails.application.credentialsを使用していたのですが、Fly.io側でmaster.keyを登録しているにも関わらずデプロイ時にRails.application.credentialsがnilになってしまいデプロイがいつまでもできませんでした。
解決のための調査にも時間をかけたのですが、解決せず焦りばかりが募りました。
そこで、Railway.appを使ってみると、非常に簡単にデプロイすることができました。
Railway.appはブラウザ上でGithubと連携するだけでデプロイをしてくれる上に、登録したmaster.keyの情報もしっかり読み込み余計な時間をかけずに済みました。
Railway.appの無料プランは、毎月5$のリソースを与えてくれるのでそれを消費しきるか、約20日連続稼働させるかすると、システムがダウンするようでした。
ただ、developerプランという1つ上のプランでは、毎月5$のリソースをくれた上でそれ以上のリソースを消費した場合に初めて課金されるというものでした。
また、連続稼働時間の制限はなくなるものでしたので、システムがダウンするよりは良いかと考え、developerプランに加入しました。
無料プランよりも良いリソースを提供されているので、それもプラスでよかった点です。
最後に
webに公開して誰かに使ってもらう事を意識してサービスを作る経験ができてとても良かったと思います。
前職でお客様へシステムを提供していましたが、すでに出来上がっているパッケージを導入したり、使いにくい・値段が高いと言われ本当にお客様が欲しいものを提供できているのかずっと疑問でした。
そのため、自作サービス進捗会で、デモを動かした時にkomagataさん、machidaさんに自分達にとって価値があるものができたと仰っていただいた時、誰かの役に立つものを提供することができた!と嬉しくなりました。
受託開発の会社に就職したいと考えている自分としては、komagataさんmachidaさんの要望に合わせて、開発を行いデモで実際の使用感の感想をいただいたり、修正を行なったりできたことも良い経験でした。
やはり、誰かの需要に応えられるエンジニアになりたいと思えましたし、これからも応えられるようにスキルを上げていきたいと改めて思いました。
このサービスがFjordBootCampAppの開発に携わる人の一助となれば幸いです。
Choices.jsでセレクトボックスを複数作る・動的に作る
概要
フィヨルドブートキャンプでChoices.jsを使ったセレクトボックスを作ることになったが、Choices.jsの記事が少なく、複数作ったり動的にセレクトボックスを作ったりしている記事がなかったので、残しておきたい。
参考
@GaramMasala29さんのqiitaの記事を参考にさせていただきました。 非常にわかりやすかったです! ありがとうございました!
あとは公式を参考にした。
Choices.jsについて
以下のような文字検索ができたり、他にもオプションをつけることで様々なことができるライブラリ。
複数のセレクトボックスにChoices.jsを適用する
複数のセレクトボックスにChoices.jsを適用する場合、私はquerySelectorAll
を使ってidを全て取得し、for文を使用することで、各セレクトボックスにChoices.jsを適用できた。
f.collection_select :book_id, all_books, :first, :last, {}, { id: 'js-book-select' }
document.addEventListener('DOMContentLoaded', () => { const bookSelectCount = document.querySelectorAll('#js-book-select').length const elements = document.querySelectorAll('#js-book-select') for (let i = 0; i < bookSelectCount; i++) { // eslint-disable-next-line no-new new Choices(elements[i], { allowHTML: true, searchResultLimit: 20, searchPlaceholderValue: '検索ワード', noResultsText: '一致する情報は見つかりません', itemSelectText: '選択', shouldSort: false }) } })
動的にセレクトボックスを作る
動的にセレクトボックス場合もquerySelectorAll
を使うことで解決できた。
動的に作る場合は、querySelectorAllで取得した最後の要素に対してChoices.jsを適用することでセレクトボックスを作ることができた。
document.addEventListener('DOMContentLoaded', () => { const bookSelectCount = document.querySelectorAll('#js-book-select').length const elements = document.querySelectorAll('#js-book-select') for (let i = 0; i < bookSelectCount; i++) { // eslint-disable-next-line no-new new Choices(elements[i], { allowHTML: true, searchResultLimit: 20, searchPlaceholderValue: '検索ワード', noResultsText: '一致する情報は見つかりません', itemSelectText: '選択', shouldSort: false }) } $('.reference-books-form__add').on('cocoon:after-insert', () => { const elements = document.querySelectorAll('#js-book-select') const element = elements[elements.length - 1] if (element) { return new Choices(element, { allowHTML: true, searchResultLimit: 20, searchPlaceholderValue: '検索ワード', noResultsText: '一致する情報は見つかりません', itemSelectText: '選択', shouldSort: false }) } }) })
出来上がったセレクトボックスはこちら。
コントローラーを分けるとシンプルに記述できて良い感じだったので習慣にしたい
今回ユーザーの就職活動中かどうかのフラグをオンオフするというissueに取り組んでいて、その際にいただいたアドバイスについてここに残しておきたい。
条件としてはユーザー情報を更新するページと記事に対してコメントするページがあったとして、記事に対してコメントするページで就職活動中かどうかのフラグをオンオフするという実装が必要だった。
私は、以下のような感じで2種類のupdate処理を1つのコントローラー内に記述してレビュー依頼を行なった。
- user_controller(だいぶぼかしてある)
class UsersController < ApplicationController def update update_job_seek if only_job_seek? if @user.update(user_params) redirect_to users_url, notice: 'ユーザー情報を更新しました。' else ... # 何らかの処理 end end def update_job_seek if @user.update(user_job_seek_params) ... # 何らかの処理 return else ... # 何らかの処理 end end private def user_params params.require(:user).permit(:name, :email, :age, :job_seek) end def user_job_seek_params ... # 何らかの処理 end def only_job_seek? ... # job_seekというパラメータのみかをチェック end end
このコードをレビューいただき、アドバイスとしてDHHはどのようにRailsのコントローラを書くのかという記事をもとに別のURLを作って更新処理を行なったほうが良いとうことだった。
この記事はだいぶ前であるが、教えていただいておりすっかり忘れてしまっていた。。。(今後はしっかり覚えておく!)
こちらの記事を読み、その通りに実践したところコントローラーの記述がシンプルになり、とても読みやすいコードになった。
- user_controller
class UsersController < ApplicationController def update if @user.update(user_params) redirect_to users_url, notice: 'ユーザー情報を更新しました。' else ... # 何らかの処理 end end private def user_params params.require(:user).permit(:name, :email, :age, :job_seek) end end
- job_seek_controller
class JobSeeksController < ApplicationController before_action :set_user, only: %i[update] def update if @user.update(user_params) ... # 何らかの処理 else ... # 何らかの処理 end end private def set_user @user = User.find(params[:id]) end def user_params params.require(:user).permit(:job_seeking) end end
上記の通り、user_controller
でonly_job_seek?
というメソッドを使って、job_seekというパラメータだけかどうかを判断していたが、コントローラーを2つに分けることでuser_controller
とjob_seek_controller
のそれぞれがすごくシンプルになった。
1つのコントローラーにたくさんの処理を任せれば任せるほどコードは読みにくくなるし、URLの設計の観点からもPUT /user
でjob_seek
だけをupdate
するかどうかわからなくなる。
そのため、コントローラー分ける(責任を分ける)ことで、コードは読みやすく、URLの設計もPUT /job_seek
でわかりやすくなる。
記事の引用の引用になるけれど、以下の言葉は確かに後の自分に怒りたくなるのだろうと思った。
自分の作ったコントローラの状態を悔やむのは決まって、作ったコントローラの数が少なすぎた時です。多くの処理を任せようとしすぎてしまうんです。
Full Stack Radioのインタビュー
ここで書いたからには、忘れないようにしたいし、コントローラーに変更を加える際にもう一度読み直したい。
before_actionのifオプションとskip_before_actionで親コントローラーのbefore_actionを無効にする
before_actionの使い方
before_actionを使うと、各アクション実行前に、before_actionで指定したメソッドが実行される。
before_action :メソッド
この使い方は知っていたけど、今回はifオプションを使うと便利だったので、ifオプションについて残しておきたい。
ifオプション
今回以下のようにbefore_actionで認証系にメソッドが使われているが、特定のコントローラだけ無効にしたい時にifオプションが使えた。
class SampleController < ApplicationController before_action :require_login_for_api end
以下のように条件チェック用のメソッドをSampleControllerで用意する。
class ApplicationController < ActionController::Base before_action :require_login_for_api, if: :check? def check? true end end
そして継承先のコントローラーでもfalseになるメソッドを用意する。
class SampleController < ApplicationController def check? false end end
こうすることで、SampleControllerだけ親コントローラーのrequire_login_for_apiを無効にすることができた。
skip_before_action
先ほどのifオプションでも親コントローラのbefore_actionを無効にできるが、この場合CIを通した時に tracerouteのエラーが発生した。
そこで、skip_before_action
のコールバックを使うともっとスッキリ書くことができ、CIを通すことができた。
こちらも自分のために残しておきたい。
class SampleController < ApplicationController skip_before_action :require_login_for_api end
しかし、セキュリティに穴を開けるなどであれば使う時はしっかり考えたい。
railsテストのデバッグ方法をメモ
railsテストのデバッグ方法
railsのsystemテストでハマってしまい、その時に使ったデバッグ方法を残しておく。
binding.pry
binding.pry
をコード中に書くと、コード実行時にbinding.pryの場所で止めてくれるgem。
ブレークポイントとして機能する。
以下のように、確認したい処理の手前に書く。
def update binding.pry if @user.update(user_params) 成功時の処理 else 失敗時の処理 end end
すると、以下のように処理が止まる。
18: def update 19: binding.pry => 20: if @user.update(user_params) 21: 成功時の処理 22: else 23: 失敗時の処理 24: end 25: end [1] pry(#<Controller>)>
ここで例えば以下のように@user.update(user_params)
を実行した時の処理がわかる。
実際にupdateが成功しているのか失敗しているのかわからない時などに使える。
[1] pry(#<Controller>)> @user.update(user_params) => false
他にもこの時点のuser_paramsの内容なども確認できる。
@user.errors.full_messages
以下のように.errors.full_messages
を使うことで先程のupdateでどんなエラーが発生していたかがわかる。
今回はユーザー情報にos:があったのだが、テストデータで用意していたユーザにos:がなかったため、エラーが発生していた。
[1] pry(#<Controller>)> @user.errors.full_messages => [使用しているOSを選択してください。]
これはテストだけでなく、普通に実装している時も使用できる。
binding.pryも普通に実装している時も使えるが、今回のようにテストでエラーが発生した場合もデバッグに使える。
page.save_screenshot('~~~/screenshot.png')
capybaraをインストールすることで使用できる。
テストコード中に書くことで、スクリーンショットを取得できる。 以下の場合、更新ボタンをクリック後のページをスクリーンショットで取得できる。
test 'test' do click_on '更新' page.save_screenshot('~~~/screenshot.png') end
save_and_open_page
こちらもcapybaraをインストールして使う。
こちらもテストコードの中に記述して使用する。
save_and_open_pageを書いた時点の画面の状態をブラウザで確認することができる。
おわりに
p や putsを使ってデバッグも可能だが、更新系の処理を実際に確かめたかったり、画面の状態を確かめたい場合は、これらのメソッドを使って行きたい。
日時をテストするときは、travel_toメソッドを使うと良い
モデルテストで日時をテストするときに、以下のように今日作成されたものかどうかを確認するテストを作成していた。
report = Report.create(title: 'test_title', content: 'test_content') assert_match Time.zone.today.to_s, report.created_at.to_date.to_s
ただ、これだとReport.create
が実施された時間が23:59:59.999
で、assert_match
でテストした時間が24:00:00.000
とかになると、日を跨いでしまい、エラーになる可能性がある。
そこで、travel_to
を使うと、日時を固定できるので便利だった。
travel_to(Date.new(2022, 1, 1)) do report = Report.create(title: 'test_title', content: 'test_content') # <=2022/01/01で固定される assert_equal Date.new(2022, 1, 1).to_s, report.created_at.to_date.to_s end
travel_to do ~ end の中でreportを作成して、assertionで結果を確認すれば、いつテストを行っても、日が固定されているので、日を跨いだらテストにならないといったことは心配なくなる。
【参考】