oneshot task に規約と仕組みを導入する
Cookpad TechConf 2018 の突発 LT で話した内容をもう少しちゃんと書いてみます。
アドホックなタスクの必要性
日々、サービスを運用しているとアドホックなデータの変更や処理の再実行などが必要になります。
典型的には以下のようなケースです。
- データマイグレーション
- サポート対応
- 不具合対応
- その他もろもろ
ここではこのようなアドホックなタスクを oneshot task と呼ぶことにします。
oneshot task をスクリプトとして書く
oneshot task をスクリプトとして書くといくつかのメリットが得られます。
似たようなことをする方法はいくつかありますが、それらの方法と比べたときの主なメリットは次のようなものです。
vs. 管理画面
管理画面をポチポチすれば対応できるものによっても場合によってはスクリプトを書く価値があります。
大量のデータを処理する必要があるときにはスクリプトを書いたほうが簡単確実ですし、スクリプトであればトランザクションなども使えるのでアトミックな更新をすることができます。 また、事前条件・事後条件をチェックした上で必要に応じてトランザクションをロールバックするなどより安全に配慮した処理を行なうこともできます。
vs. 手オペ
ここで手オペと呼んでいるのは本番環境で bin/rails console
するとか DB に直接 SQL を投げるとかそういったものです。
サービスの最初期など場合によっては便利な手オペですが、ミスを起こしやすくリカバリしにくいので基本的にはスクリプトにしておくべきです。
スクリプトにすることによって、テストを書くことができるようになり、PR にしてレビューできるようになり、Git に残るので過去の変更内容を見返すことができるようになります。
考慮すべきこと
スクリプト化するメリットを書きましたが、スクリプトにすることによっていくつか考えることが生じます。
ファイルをどこに置くのか
Rails には oneshot task の置き場所に関する規約はないので、どこに置くかを決めておく必要があります。
いつまでも残ったら邪魔じゃないのか
一時的に使用するスクリプトをリポジトリに入れることによって他のコードへの依存が発生します。 将来、依存されているコードを変更したくなったときに依存している箇所を書き換えてよいのかどうか逐一判断する必要が生じます。
ファイル名・タスク名の命名とかファイル作成とかが面倒
単純にファイルを作るのが面倒という話です。
稀によくあるタスクだと名前が衝突したりするのでぶつからない名前を考えるのが地味に面倒だったりします。
規約と仕組みをつくる
日々、スクリプトを書く必要が生じる中でいちいち上のようなことを考えるのは面倒なので規約と仕組みで解決します。
規約
置く場所を決める
決めの問題なのでどこでも良いんですが、Rails で oneshot task を書くとなると rake task が一般的だと思うので lib/tasks/oneshot/
に入れることにします。
そうすることによって、置く場所をいちいち考える必要がなくなるとともに、このディレクトリにあるファイルは(実行後であれば)消しても良いとわかるようになります。
命名規則を決める
ファイル名・タスク名の命名にも規約をつくり、規約によってファイル名・タスク名にタイムスタンプをつけるようにして YYYYMMDD_foo_bar.rake
というファイル名と oneshot:foo_bar_YYYYMMDD
というタスク名にします。
そうすることによって、命名がぶつかることを防止するとともに、タイムスタンプを元に消して良いかどうか判断しやすくなったり rm lib/tasks/oneshot/2017*
というような一括削除が行えるようになります。
仕組みをつくる
規約をつくったので、その規約にのっとってファイルをつくっていけば良いんですが、規約通りにファイルをつくるというのは明らかに人間よりもコンピュータが得意な仕事です。
そこで規約通りにファイルを作成するための仕組みをつくります。
幸い、Rails には generator の仕組みがあって、普段よく使っている bin/rails g hogehoge
を自分でつくれるのでこの仕組に乗っかって bin/rails g oneshot FooBar
できるようにします。
generator については以下のページによくまとまっています。
使いまわしたいので gem にした
リンク先を読んでもらうとわかるのですが、generator の仕組みは非常にシンプルで generator をつくるのは全然難しくありません。
ただ、複数のプロジェクトで同じような generator を何度も書くのは面倒なので、使いまわせるように切り出して oneshot_task_generator という gem に切り出しました。
使い方
$ bin/rake generate oneshot FooBar
という感じで実行すると lib/tasks/oneshot/20180205_foo_bar.rake
というファイルができます。中身はこんな感じ。
# Skeleton for this file was generated by `bin/rails generate oneshot FooBar` # For copy and paste: `bin/rake oneshot:foo_bar_20180205` namespace :oneshot do desc '' task foo_bar_20180205: :environment do end end
基本的には規約通りのディレクトリに規約通りのテンプレを吐き出すだけのシンプルなものです。
カスタマイズ
gem にする前に運用していたバージョンでは oneshot でよく使うコード(e.g. トランザクション)をテンプレに埋め込んでいました。
そういった簡単なカスタマイズもできるようにしています。
# config/initializers/ohesnot_task_generator.rb OneshotGenerator.configure do |config| config.body = <<-BODY ActiveRecord::Base.transaction do # Write transactional code here end BODY end
# Skeleton for this file was generated by `bin/rails generate oneshot FooBar` # For copy and paste: `bin/rake oneshot:foo_bar_20180208` namespace :oneshot do desc '' task foo_bar_20180208: :environment do ActiveRecord::Base.transaction do # Write transactional code here end end end
おわりに
自分が欲しいものを規約と仕組みに落とし込んで、しばらく運用してみたところ便利だったので gem にしたというお話でした。
技術的にも思想的にも別に大したことはしてないんだけど、こういう地味に便利になるみたいなのが結構好きです。
使ってみて「ここをこうして欲しい!」とか「英語がクソ」みたいなのがあれば issue なり PR なりいただければと思います。