「作って終わらない」開発とは〜Railsアプリを2年半運用して学んだ設計の本質

7分で読める テック

「判断力をつける一番の方法は、自分で設計したシステムを長い間メンテすることだと思う」

Facebookで史上最高クラスのエンジニアと評されるEvan Priestley氏の言葉です。

この言葉を初めて知ったとき、自分はエンジニアになってまだ1年も経っていませんでした。「そうか、自分で設計してメンテしていくことが大事なのか」と漠然と思っただけで、当時はその意味を実感として理解できていませんでした。

それから数年が経ち、自分が設計したシステムを運用し続ける経験をした今、ようやくこの言葉の重さが分かるようになりました。エンジニア9ヶ月目で初めて大規模なシステム設計を担当し、その後2年半にわたって運用してきた中で考えてきたことを残しておきます。

どんなシステムを作ったか

自分が働いていたプロダクトには、最新で正確な情報をユーザーに届けるという機能がありました。機能の実装前は、その情報のリサーチ業務をスタッフが手動で行っていましたが、データ量が増えるにつれてオペレーションの限界が見えてきました。

そこで「人が手動でやっていた情報収集・下処理・データ入力をシステムに置き換える」というプロジェクトが立ち上がりました。自分はエンジニアになって9ヶ月のタイミングで、このシステムの設計リードを担うことになりました。

実現のために、責務を分けた2つのRailsアプリケーションを設計しました。

  • マスタ(生データを蓄積する): 外部APIでデータを収集し、GraphQLエンドポイントで本体に提供する
  • 本体(整形されたデータが保存される): マスタからデータを取得してRakeタスクで下処理・保存する。スタッフが確認する管理画面を持つ

これら2つのシステムのRakeタスクを定時実行させることで、それまで人の手でやっていたオペレーションを自動化するというシステムです。開発期間は6ヶ月でした。

設計時に考えたこと

初めての大規模設計ということもあり、自分なりにいくつかの観点を意識して進めました。

Design Docsで設計意図を残す

GoogleのDesign Docsの観点を参考に、設計ドキュメントを書きました。特に意識したのは、複数の実現手段が考えられるときにそれぞれをドキュメント化して比較検討すること。設計の「なぜ」を言語化しておくことが、後から見たときの理解につながると考えていました。

ユースケース図も合わせて書き、スタッフのオペレーションと実際のシステムの振る舞いが合致しているかをPdMとすり合わせながら進めました。

原則に従う

SOLID原則や凝集度の概念など、ソフトウェア設計の基本思想をインプットしながら実装しました。2つのアプリケーションに責務を分けることもその観点から判断しています。

不確実性への備えを組み込む

未知のことが多いシステムだったので、動かしてみないと分からないことが多い前提で設計しました。具体的には以下を意識しています。

  • 冪等性: ActiveRecordのTransactionを使い、Rakeタスクを再実行しても同じ結果になるようにする
  • 可観測性: エラーを検知する仕組みを入れ、バッチが途中で止まったときも気づけるようにする
  • 個別実行: Rakeタスクに引数を指定して、対象や期間を絞って実行できるようにする

リリース後に起きたこと

設計通りに動き、オペレーションの自動化を実現できました。リリース直後はうまくいっていました。

しかしその後、想定していなかったことが次々と起こりました。

チームの解体

リリースから3ヶ月後、組織再編があり実装を担当したチームが解体されました。自分もこのシステムの運用からは外れ、別のチームへ。設計・実装をした本人が、運用の現場から離れることになりました。

データ量の増加

システムを動かし続けるうちにデータが蓄積されていき、バッチの実行時間が当初の想定を超えるようになりました。依存関係のあるバッチが正常に動かなくなったり、インフラのパフォーマンスが追いつかなくなる場面も出てきました。

運用障害の発生

バッチの実行自体は成功しているのに、処理ロジックに誤りがあってデータの内容が意図したものと異なるという障害が発生しました。Bugsnagでシステムエラーは監視していましたが、バッチ自体は動いているため検知が遅れてしまいました。

設計をふりかえって

2年半の運用を経て、当時の設計を振り返ってみます。

やっておいてよかったこと

設計意図をドキュメントに残していたこと

チームが解体されてからも、設計当初に書いたDesign Docsが機能しました。「あのシステムはなぜこういう構造になっているのか」という問い合わせが来たとき、当時自分が書いたドキュメントを見ることで思い出せたのです。設計の「なぜ」を残しておくことが、後から来る人にも自分にも助けになりました。

責務を分けていたこと

2つのアプリケーションに分けていたことで、障害発生時に「マスタ側の問題か、本体側の問題か」という切り分けがしやすくなりました。片方を止めて調査している間も、もう片方は動き続けられます。責務の分離はメンテナビリティに直結することを実感しました。

冪等性と個別実行を確保していたこと

バッチに冪等性を持たせ、かつ引数で対象を絞って実行できるようにしていたことが、障害調査と復旧に役立ちました。再実行で原因を切り分けたり、修正後にピンポイントで再処理したりできたことで、ユーザーへの影響を最小限にしながら対応を進められました。

足りなかったこと

データの健全性を確認する仕組み

バッチの「実行成功」と「データが正しい」は別の話でした。システムエラーは検知できていても、データの内容が意図したものかどうかを継続的に監視する仕組みがなかったため、データ起因の問題の検知が遅れてしまいました。

データ量の増加を追跡する仕組み

バッチの実行時間がどれくらい増えているか、インフラのスペックと見合っているかを継続的に観測する仕組みを入れておけばよかったと思います。問題が顕在化してから対応するのでなく、予兆を掴める状態にしておくことが大事でした。

リリース後の改善計画

当初から「リリースしてから改善していく」という方針を取っていましたが、どのタイミングで何を改善するかという具体的なアクションプランがありませんでした。運用を始めてから課題が積み上がっていく中で、優先順位をつけながら対応していくのは難しかったです。

学んだこと

この経験を通して、設計力について自分なりの考えが少し整理されました。

設計の意図は「なぜ」ごと残す

コードは残っても、その決断の背景は残りません。なぜそうしたのかを言語化してドキュメントに残しておくことが、長期間の運用においては特に重要です。運用者が変わっても、自分自身が忘れても、ドキュメントが橋渡ししてくれます。

「動く」だけでなく「壊れたとき」も設計する

設計のときに考えるのは、どう動くかだけになりがちです。でも実際の運用では「壊れたときにどう気づくか」「どう調査するか」「どう復旧するか」が問われます。良い設計は、障害発生時に開発者が早期に問題を特定できて、復旧が容易であることにもつながります。

不確実性は減らせないが、対応できるシステムは作れる

設計に3ヶ月かけても、起こることをすべて予測することはできません。チームの解体も、データ増加の速度も、設計時点では正確には分かりませんでした。それでも、原則に従って責務を分け、冪等性を持たせておいたことが実際の問題への対応に役立ちました。不確実性そのものをなくすことはできませんが、不確実な未来が来たときに対応しやすいシステムを作ることはできます。

まとめ

Evan Priestley氏の言葉を冒頭に引用しましたが、「自分で設計したシステムを長い間メンテすること」は、変化の早いIT業界ではなかなかできる経験ではありません。

自分はたまたまそのような機会を得て、設計の「理想」とリリース後の「現実」の両方を経験することができました。

初めての設計は、完璧にはできません。それでいいと思います。原則に従って設計し、意図をドキュメントに残して、壊れたときのことを少し想像しておく。それだけで、後から来る変化にずっと対応しやすくなります。

ぜひ、設計を楽しんでみてください。

質問・リクエストを送る

記事についての質問や、取り上げてほしいテーマがあればお気軽にどうぞ。いただいた質問はブログ記事として回答し、Q&Aページで公開することがあります。