行動すれば次の現実

ほどよくモダンなシステム開発を目指しています。メインテーマは生産性、Ruby、Javascriptです。

スムーズに新規開発するために必要なこと(要件定義〜画面設計)

今まで様々なシステムを新規開発してきましたが、毎回思うのは「スムーズに新規開発するにはどうしたら良いのか」ということです。

「前回よりも今回」という目標を持って、この問いに取り組んできましたが、ある程度答えが明確になってきたので、今回アウトプットすることにしました。

新規開発に苦戦している方の一助になれば幸いです。

まずは顧客の「要望」を「要件」に落とし込む

まずは顧客の頭の中にある要望を吸い上げて、要件に落とし込む作業を行います。

「顧客がどのような目的でシステムを作りたいのか」を一人称視点で理解する必要があります。

前提として、顧客と最低限会話できるレベルの業務知識を持っておく必要があります。業務知識といっても概要レベルで構いません。細かな部分は顧客とのヒアリングの中でイメージできれば良いので、ここでは大まかな業務知識を持ち合わせておけば良いです。

業務知識を持ち合わせていないと、顧客が抱えている本質的な課題を捉えることが出来ません。さもなくば顧客から挙げられた具体的な要望を、そのまま要件として捉えてシステムに反映させてしまいます。顧客はシステム開発のプロではないので、要件に矛盾が生じたり、システムとしての一貫性が欠けてしまい、カオスなシステム出来上がってしまいます。

顧客の要望を一人称視点で理解することで、初めて適切な要件に落とし込むことが出来るのです。 顧客から挙げられた要望を、グルーピングなどの作業を通じて抽象化を行います。顧客がどのような課題に悩んでいて、どのように改善したいのか。そのためにはどのようなシステムを作ればよいのかを考えていきます。

これらを整理すると、顧客の要望から要件に落とし込む手順を以下の通りになります。

  1. 顧客と同レベルの業務知識を持ち合わせる
  2. 顧客の要望をヒアリングする(具体レベルの内容であることが多い)
  3. 顧客の要望からグルーピングなどの抽象化作業を行い、要件に落とし込む

ワイヤーフレームで顧客と認識を合わせよう

要件の成果物としてワイヤーフレームを作成します。 要件に落とし込んだ内容をワイヤーフレーム通じて画面という形に具体化します。

実際にワイヤーフレームを作成してみると、抽象的だった要件が段々と具体化されていくことに気付くかと思います。具体化する作業の中で様々な疑問が生じてきます。 それらの疑問を「課題管理表」というシートに全て記載して顧客とのミーティングの中で解決させていきます。

一通り作成すると要件がブラッシュアップされ、より実装イメージが湧いてきます。 ワイヤーフレームを顧客にレビューしてもらい、フィードバックを得るという作業を繰り返すことで、さらにブラッシュアップされていきます。

完成物イメージを通じて、顧客と要件の齟齬を無くすことで、相互のイメージ不一致、いわゆる「顧客が本当に必要だったもの」のリスクを潰すことができます。

ワイヤーフレームを作成にはfigmaなどのツールを用いることをおすすめします。 figmaを使うと部品の共通化などが行えるので、顧客のフィードバックを反映するのも比較的容易です。 また、画面遷移の機能も備わっていますので、実物をイメージしてもらいやすいのもメリットの一つです。

Heroku Pipelineでデプロイ作業がかなりラクになった

今まで同じコードベースを複数のアプリで別管理していたのですが、さすがに片手で数えられないアプリ数にもなると管理が辛くなってきたので、Heroku Pipelineを導入してみました。

実際に使用してみると、痒いところに手が届く素晴らしい機能だと感じたので紹介したいと思います。

ステージごとに「複数」のアプリを載せられる

Heroku PipelineではReview App、Staging、Productionの3つのステージが存在します。

Review AppにはPR単位の使い切りの一時的なアプリを載せて、StagingとProductionにはそれぞれ該当のアプリを登録します。

StagingとProductionそれぞれアプリは1つのみ載せられると勝手に思い込んでいましたが、複数のアプリを載せられるという仕様なのは「さすがHeroku」だと感じました。

例えばパッケージソフトを扱うシステムの場合、Staging環境に「開発用アプリ」「デモ用アプリ」、Productiion環境に「A社用アプリ」「B社用アプリ」のように複数のアプリを登録することが可能です。

「開発用アプリ」のスラグを一括で「A社用アプリ」「B社用アプリ」にプロモートすることができます。これによりリリース作業の煩わしさがなくなり、とても管理が楽になりました。

また、Herokuではプロモートやデプロイ時に実行される「リリースフェーズ」に特定のコマンドを発行することができます。

Procfileに以下のように「release」の定義をすることで、今までコマンドで直接叩いていたマイグレーションも自動化できるようになりました。 マイグレーションをし忘れるというリスクもなくなり至れり尽くせりです。

Procfile

release: bin/rails db:migrate
web: bundle exec puma -C config/puma.rb

自動テストがHeroku CIで簡単に実行できる

有料にはなりますが、Heroku CIを利用することでRSpecやRubocopなどのテスト環境を簡単に構築することができます。

CIツールというと有名所だとCircle CIなどがあります。Circle CIの場合、Docker上にテスト環境を構築してテストを実行する必要があります。

シンプルにRSpecとRubocopを実行したいだけでも、それなりに複雑なDockerの定義が必要になります。 (例えばimageを定義したり、dbを構築したり、各種ライブラリをインストールしたりなど)

Heroku CIの場合、Herokuのdyno上で実行されるため、Dockerの定義が必要ありません。そのため、設定ファイルはかなりシンプルな形式になります。

app.json

{
  "environments": {
    "test": {
      "addons": [
        "heroku-postgresql"
      ],
      "scripts": {
        "test": "bundle exec rubocop app && bundle exec rspec spec"
      }
    }
  }
}

ただし、複雑な自動テストをしたい場合にはHeroku CIは向いていないかもしれません。

例えば、数回に1回何らかの理由で失敗してしまうテストを抽出して、再テストするような自動化を構築する場合、これをHeroku CI再現させるのは難しいように感じました。 (そもそもそのようなテストがある事自体が問題なのですが。。)

Circle CIではworkflowという機能を使って、テストjobと再テストjobをコントロールして失敗したテストを再テストするということが可能ですが、Heroku CIでは一筋縄では実現できなそうでした。(もしかすると方法があるのかもしれませんが)

とはいえほとんどのケースはHeroku CIで実現できますので、一考の価値はあるかと思います。

Railsで簡単にPDFが出力できるGroverを導入してみた(Herokuへのデプロイも)

この記事では、Groverを使ってPDFを出力する際の導入方法や注意点を記載しております。また、Herokuへのデプロイについても説明します。

Groverとは?

GroverとはGoogle PuppeteerとChromiumを使ってHTMLをPDFや画像ファイル変換するGemです。

RailsでGroverを使うと、HTML(ERB)を実装するのと同じ感覚で、簡単にPDF出力ロジックを実装することが出来ます。

github.com

Google Puppeteerとは?

スクリプトを介してヘッドレストブラウザ(GUIを持たないブラウザ)や通常のブラウザを操作することができるフレームワークです。主にブラウザの自動テストなどに使用されます。類似の代表的なフレームワークにSeleniumなどがあります。

developer.chrome.com

Chromiumとは?

Chromeのベースとなるブラウザエンジンです。Chromeとの違いは自動アップデートやクラッシュレポートなどの機能が搭載されていない点です。

Grover、Puppeteer、Chromiumの関係

Grover、Puppeteer、Chromiumの関係について整理します。

  • ① GroverはPuppeteerとChromiumを元にHTMLを描画します。
    • 実際にはPuppeteerがChromiumを使用してブラウザを操作してHTMLを描画します。
  • ② Groverは描画されたHTMLを元に画像ファイル(PDF、PNG、JPEG)を生成します。

Grover、Puppeteer、Chromiumの関係

実装イメージ

インストール

Gemfileにgroverを追加します。

Gemfile

gem 'grover'

設定ファイル

initializersにGroverの共通定義を設定します。 実際の出力処理で個別に上書きすることも可能です。

config/initializers/grover.rb

Grover.configure do |config|
  config.options = {
    format: 'A4',
    margin: {
      top: '30px',
      right: '20px',
      left: '20px'
    },
    cache: false,
    display_url: <%= ENV.fetch("GROVER_DISPLAY_URL") {'localhost:3000' } %>
  }
end

display_urlには自ホストのアドレスを入力してください。これは出力対象のHTMLでCSSファイルを読み込んでいる場合に必要になります。デプロイを想定して環境変数などする必要があります。

HTMLファイル内に直接スタイルを定義している場合、display_urlは不要です。

PDF出力コントローラーの実装例

  1. ActionController::Baseに定義されているrender_to_stringを使用して、指定したtemplateからHTML構文を生成します。
  2. HTML構文をGroverに渡して、to_pdfメソッドでPDFを生成します。
  3. send_dataでPDFファイルをクライアントに返します。
def show
  html = self.class.new.render_to_string(template: 'sample/index')
  pdf = Grover.new(html).to_pdf
  send_data(pdf, filename: 'sample.pdf', type: 'application/pdf', disposition: 'inline')
end

Herokuで使う場合の注意点

Herokuにデプロイする場合はいくつか注意点があります。

buildpackの追加

Herokuで使用するためにはNode.jsとpuppeteerのビルドパックを追加する必要があります。

Node.jsビルドパックの追加

heroku buildpacks:add heroku/nodejs --index=1

puppeteer-heroku-buildpackの追加

heroku buildpacks:add jontewks/puppeteer --index=2

puppeteerのビルドパックの実態はpuppeteer-heroku-buildpackです。このビルドパックにはHerokuサーバー上でpuppeteerを使用するために必要なライブラリ群が含まれています。

しかし、このビルドパックを導入するとSlug Size(Herokuの容量)をかなり消費することになります。HerokuのSlug Sizeは最大500MBまでなのですが、puppeteerを導入すると余裕で400MBは超えてしまうでしょう。

その原因はpuppeteer-heroku-buildpackに含まれているxdg-utilsというライブラリの影響が大きいと思います。このライブラリだけで344MB消費しています。

そのため、puppeteer-heroku-buildpackのGithubではxdg-utilsを除いたタグ(22.0.0-no-xdg-utils)も作成されているようです。xdg-utilsを除くことによる影響は100%ないとは保証できないとのことですので、導入は自己責任でお願いします。

ちなみに私の環境では特に問題なかったので22.0.0-no-xdg-utilsをビルドしています。

heroku buildpacks:add https://github.com/jontewks/puppeteer-heroku-buildpack#22.0.0-no-xdg-utils --index=2

GROVER_NO_SANDBOXの設定

GROVER_NO_SANDBOX=trueという環境変数を設定する必要があります。

no_sandboxはChromiumに対して適用されるようです。

そもそもChromiumのサンドボックスとはタブ毎に独立したプロセスでアプリケーションを稼働する仕組みをさします。デフォルトではこれが有効になっていますのが、Herokuで利用するためには無効にする必要があります。

heroku config:set GROVER_NO_SANDBOX=true

日本語フォントの追加

Herokuは日本語フォントに対応しておらず、そのまま出力しようとすると文字化けしてしまいます。

そのため、日本語フォントをHerokuに読み込ませる必要があります。

文字情報技術促進協議会のサイトからIPAフォントをダウンロードして、任意のttfファイルをプロジェクトのルートに配置します。

IPAexゴシックを使用する例

.fonts/
  - ipaexg.ttf

終わりに

PDF出力用のGemはいくつかありますが、 Groverを使用するとHTMLを実装する感覚でPDFレイアウトを組むことができるので保守性の観点でもおすすめです。

Groverのデメリットは処理が重くなりやすい点だと思います。シンプルなレイアウトであれば数秒で作成されますが、複数ページなどを出力する場合は処理時間の問題が発生します。

Herokuには30秒でタイムアウトしてしまうという仕様がありますので、要件によってはバックグラウンド(非同期処理)でPDFを作成して、完了したらクライアントにダウンロードさせるなどの考慮が必要になるかと思います。

Sidekiqでスタータスチェックや進捗管理ができるsidekiq-statusの使い方

Sidekiqのステータスチェックや進捗状況管理に便利なsidekiq-statusの使い方について説明します。

https://github.com/utgarda/sidekiq-status

sidekiq-statusとは

sidekiq-statusを使うとジョブのスタータスや進捗状況を簡単に把握することができます。

sidekiq-statusはSidekiqのジョブに関する追跡情報をredis上に保存します。 クライアントからはjob_idを用いて簡単にそれらの情報を取得することができます。

例えば、重たい非同期処理(ジョブ)を画面から実行して、画面上にステータスや進捗情報を表示したい場合などに役立ちます。

sidekiq-statusの導入

sidekiq-statusのインストール

Gemfile

gem 'sidekiq-status'

sidekiq-statusの設定を追加

config/initializers/sidekiq.rb

Sidekiq.configure_server do |config|
  # 下記を追記
  Sidekiq::Status.configure_server_middleware config, expiration: 30.minutes
  Sidekiq::Status.configure_client_middleware config, expiration: 30.minutes
end

Sidekiq.configure_client do |config|
  # 下記を追記
  Sidekiq::Status.configure_client_middleware config, expiration: 30.minutes
end

expirationにはステータスをredisに保持する期間を指定します。デフォルトでは30分です。

expirationはジョブクラスごとに上書きすることも可能です。ジョブの実行時間よりも長い時間を指定する必要があります。

ジョブクラスにSidekiq::Status::Workerをinclude

ステータス管理したいジョブクラスにSidekiq::Status::Workerをincludeすることでステータスを確認することができます。

class SampleJob
  include Sidekiq::Job
  include Sidekiq::Status::Worker # 追記します

  def perform(*args)
  end
end

ジョブステータス用の管理画面を追加

sidekiq-statusではsidekiqの管理画面にスタータス情報を表示する拡張機能を提供しています。

routes.rbに下記の記載をするとstatusesタブが新たに追加されます。

config/routes.rb

require 'sidekiq/web'
require 'sidekiq-status/web' # 追記する

statusesタブ

sidekiq-statusで出来ること

スタータスの把握

Sidekiq::Status::statusを使用することでスタータスを把握することが出来ます。

job_id = SampleJob.perform_async
status = Sidekiq::Status::status(job_id)

# statusは下記のいずれか
# queued,working,retrying,complete,failed,interrupted

# スタータス毎のメソッドも用意されている
Sidekiq::Status::queued? job_id
Sidekiq::Status::working? job_id
Sidekiq::Status::retrying?  job_id
Sidekiq::Status::complete?  job_id
Sidekiq::Status::failed? job_id
Sidekiq::Status::interrupted? job_id

注意点

statusにnilやfalseが返却されることがあります。 これはexpirationを超過している場合や、何らかの理由でSidekiqやredisの情報が消失された場合などに発生します。

この場合、画面上では失敗やエラーとして表現するなどの工夫が必要になります。

進捗管理

以下の様に進捗率を設定したり、自前の状態を保持することが可能です。

class SampleJob
  include Sidekiq::Job
  include Sidekiq::Status::Worker

  def perform(*args)
    total 100 # 進捗率の最大値

    # storeを使うと任意の状態(hoge_statusとする)を保持することができる
    store hoge_status: 'start'
    # retrieveでhoge_statusの状態を取得できる
    progress = retrieve :hoge_status
    p progress

    # 5秒待機(なんらかの処理を想定)
    sleep 5

    # 進捗率を50%に変更
    at 50, 'complete import'
    # hoge_statusの状態をimportedに変更
    store hoge_status: 'imported'

    # 5秒待機(なんらかの処理を想定)
    sleep 5

    # 進捗率を100%に変更
    at 100, 'finish'
    # hoge_statusの状態をfinishに変更
    store hoge_status: 'finish sample'
  end
end

クライアントからステータスを確認するには以下のメソッドを用います。

job_id = SampleJob.perform_async
Sidekiq::Status::get job_id, :hoge_status #=> 'start'
Sidekiq::Status::at job_id #=> 0

# 10秒後
Sidekiq::Status::total job_id #=> 100
Sidekiq::Status::message job_id #=> "finish"
Sidekiq::Status::get job_id, :hoge_status #=> 'finish sample'

管理画面からもステータスや進捗状況が確認できます。

管理画面から見た進捗率

Sidekiqの引数とエラーリトライ時の挙動について

Sidekiqを使用するために知っておいてほしい、引数とエラーリトライ時の挙動についてまとめました。

引数のベストプラクティス

Sidekiqのperform_asyncの引数をJSON形式でRedisに永続化します。

例えばモデルオブジェクト自体を引数に渡してしまった場合、デフォルトではto_sにより<Quote:0x0000000006e57288>のようなオブジェクトハッシュ値として扱われるので、正しく復元できない場合があります。

仮に、to_sをカスタマイズして正しくJSONに復元出来たとしても、perform_asyncに渡す前と後でモデルオブジェクトの内容が変わった場合に意図しない挙動を招くことがあるので避けるべきです。引数としてはstring, integer, float, boolean, nil, array, hashの型でなければなりません。

# 悪い例
quote = Quote.find(quote_id)
SomeJob.perform_async(quote)

# 良い例
SomeJob.perform_async(quote_id)

リトライと冪等性

SidekiqではJobの冪等性(何度実行しても同じ結果になる)を意識して設計する必要があります。

Sidekiqはエラー時にリトライする機能が備わっています。エラーが発生した場合に、一定間隔おきに再施行(リトライ)を繰り返す仕組みになっています。

デフォルトでは25回リトライします。これを考慮して設計しなければ思わぬ不具合を引き起こす恐れがあります。

例えばクレジットカードの決済を実行してユーザーに課金処理を行い、完了メールを送るJobがあるとします。

課金処理が成功後に、完了メールを送信する処理でエラーが発生したとします。この場合リトライが発生して再度、課金処理が走ってしまうと二重課金が発生してしまいます。(デフォルトのリトライ回数だと最大で26回課金されてしまう)

このような場合を考慮して、どこまで処理が完了しているのかステータス管理するなどして、冪等性、完全性を保証させる必要があります。

もし冪等性の考慮が難しいなどの理由で意図的にリトライをさせたくない場合、以下のオプションでリトライを無効にすることが出来ます。

class SomeJob
  include Sidekiq::Job

  # リトライを無効にする
  sidekiq_options retry: 0
  # sidekiq_options retry: falseでも無効にできるが後述の違いがある

  def perform(*args)
    # Do something
  end
end

sidekiq_options retry: 0とretry: falseの違い

retry: 0 の場合

エラー時はデッド状態になり画面から再試行可能

retry: false の場合

エラー時は画面から再試行不可能

画面からの再試行もさせたくないなどの理由がなければretry: 0にしておくのが良いかと思います。

リトライ時の挙動について

例えばsidekiq_options retry: 2とした場合、以下のようにステータス管理されてJobがリトライされます。

  1. Jobが実行されてエラーが発生する
  2. 再試行(1回目)状態になり一定時間後にキューに登録される
  3. 1回目のリトライJobが実行されてエラーが発生する
  4. 再試行(2回目)状態になり一定時間後にキューに登録される
  5. 2回目のリトライJobが実行されてエラーが発生する
  6. デッド状態になる(画面から任意で再試行可能)

参考

https://github.com/mperham/sidekiq/wiki/Best-Practices

Sidekiqの導入手順【令和版】

SidekiqをRailsに導入する手順をまとめました。

Gemのインストール

Gemfile

# sidekiq本体(必須)
gem 'sidekiq'
# sidekiqのモニタリング画面で使用
gem 'sinatra', require: false
# redisのkeyに任意のprefixを付与させることができる
gem 'redis-namespace'

Sidekiqジョブの作成

rails console

rails g sidekiq:job sample

実行するとsample_job.rbが作成されます

app/sidekiq/sample_job.rb

class SampleJob
  include Sidekiq::Job

  def perform(*args)
    # Do something
  end
end

ちなみにインクルードされているSidekiq::Jobは元々はSidekiq::Workerというモジュール名でした。

Workerという言葉の定義が誤解を招くため、SidekiqのOSSプロジェクトではWorkerをJobにリネームするよう動いています。

現状はどちらの名前でも動きますが、ver7では完全移行となるようですので、Sidekiq::Workerを使用している場合は早めにSidekiq::Jobに変更しておくことをおすすめします。

Rename Sidekiq::Worker to Sidekiq::Job · Discussion #4971 · mperham/sidekiq · GitHub

redisの設定

sidekiqはredisを使用してメッセージングしていますので、開発環境にインストールする必要があります。

redisのインストール方法は各プラットフォームによりますので割愛させていただきます。redisがインストールできたら以下の設定ファイルを作成します。

config/initializers/sidekiq.rb

Sidekiq.configure_server do |config|
  # redisの設定
  config.redis = {url: 'redis://localhost:6379', namespace: "my_app_sidekiq_#{Rails.env}"}
end

Sidekiq.configure_client do |config|
  # redisの設定
  config.redis = {url: 'redis://localhost:6379', namespace: "my_app_sidekiq_#{Rails.env}"}
end

「redis-namespace」のgemによってnamespace: "my_app_sidekiq_#{Rails.env}"のようにnamespaceを個別に割り振ることができます。 同じredisサーバーを複数の環境で共有する場合などに管理がラクになります。

url: 'redis://localhost:6379'の部分は環境によって変わる部分ですので環境変数などで切り替えられるようにしておくと良いでしょう。

動作確認

rails console上で実際にJobを起動させてみましょう

rails c
> SampleJob.perform_async(1)
=> "51190418577b3dcafd192368"

sidekiqプロセスのログには以下のようにstartとdoneのログが出力されれOKです。

2022-10-19T05:27:06.508Z pid=1 tid=dnp class=SampleJob jid=51190418577b3dcafd192368 INFO: start
2022-10-19T05:27:06.685Z pid=1 tid=dnp class=SampleJob jid=51190418577b3dcafd192368 elapsed=0.177 INFO: done

モニタリングUIについて

sidekiqではJobの状態をモニタリングできる画面を提供してくれています。

routesファイルに記載の上、指定のURLにアクセスすることで画面を起動することが出来ます。

config/routes.rb

require 'sidekiq/web'

Rails.application.routes.draw do
  root "pages#index"
  # 下記を追記
  mount Sidekiq::Web => "/sidekiq"
end

/sidekiqにアクセスするとモニタリング画面が確認できます。

モニタリングUI

また、sidekiqにはキューという概念があり、キューごとに優先度を振ることが出来ます。 例えば優先度が2のキューは1のキューよりも2倍の頻度でチェックされるようになります。

Jobごとに優先度を変えたい場合は複数のキューをSidekiqに登録しておいて、Jobごとに割り振ると良いでしょう。

Advanced Options · mperham/sidekiq Wiki · GitHub

Rails7(esbuild+stimulus)にdatepickerライブラリのflatpickrをインストールする

Rails7+esbuild+stimulusの構成でflatpickrを使えるところまでを説明します。

flatpickrのインストール

yarn add flatpickr

app/assets/stylesheets/application.scss

// 下記を記載
@import 'flatpickr/dist/flatpickr';

app/javascript/application.js

// 日本語化するため下記を記載
import "flatpickr/dist/l10n/ja.js"

実装イメージ

app/javascript/controllers/hello_controller.js

import { Controller } from "@hotwired/stimulus"
import flatpickr from "flatpickr";

export default class extends Controller {
  static targets = [ "inputDate" ]

  connect() {
    flatpickr(this.inputDateTarget, {
      locale: 'ja',
      dateFormat: 'Y/m/d(D)',
    });
  }
}

view

<div data-controller="hello">
  <input data-hello-target="inputDate" type="text">
</div>

画面確認

flatpickr