7,600行のコードを安全にこの世から抹消した話
qsonaです。今回はコードを安全にこの世から抹消していく の実践編ということで、7,600行のコードを削減してデプロイした話を書きます。
経緯
「ランキング」のマイクロサービスを作った話 でも登場した、ランキングサービスでの話です。APIは一度リニューアルし最新はv2でした。
その後、新しい形のランキングを実装することになり、同時に、いままであった形式のランキングはなくなり、リプレイスされることになりました。そのため、新規の実装をするとともに、新しくバージョンを上げてv3のAPIを新設することにしました。
コアなロジックやデータストアにアクセスするコードについては、大部分を使い回すことができ、無事に本番リリースを行いました。
一方で、古いランキングはもう開催しないため、v2以下のAPIやそれに紐づくコードは実質的に不要になりました。そのコードを今回削除していくことにしました。
事前準備
ビジネスサイドと話し、機能を切れる範囲やタイミングを確定
FiNCアプリは様々なお客様に対して提供されています。基本的には新しいランキングが始まるとともに古いランキングは開催されなくなりますが、一部例外がありました。ビジネスサイドと話し、手順を踏んで新しいランキングに移行することにしました。
影響範囲の特定
まず、APIを使っている元を調べました。今回は、どのマイクロサービス/クライアントがランキングサービスを利用しているかまでは、自分で把握できており、以下の4つでした。
- iOS/Androidの旧アプリ
- BFF (Backends for Frontends) サーバ
- Aiちゃん(チャットボット)
- メインサービス
削除の告知
Aiちゃんのサービスは別チームが開発している(といっても物理的な席は5メートルしか離れていませんが)ので、そのチームのエンジニアに、APIを削除したい旨を連絡しました。
そもそも今回のケースでは、古いAPIは呼び出されたとしても404や空配列などしか返さなくなるため、基本的に呼んでも意味がないAPIになっています。その旨を説明し、呼び出しの削除に協力いただきました。
呼び出し元の削除
メインサービスからの呼び出しもすでに不要なものでした。これは自分が所属するチームが管理しているので、単に呼び出しの削除をしました。
BFF (GraphQLで作られている) も、自分が管理しているので同じく呼び出しの削除をしました。
どのAPIが呼び出されているのかを確認
誰がサービスを使っているかは把握していましたが、厳密にどのAPIが呼び出されているかは自信がなかったので、今回は NewRelic を用いて具体的に確認しました。
基本的に古いクライアントが叩いているAPIは、サポートを打ち切っていない限り消せません。
ただし、例えばindexとshowのAPIがあるときに、indexが空配列しか叩かないのであれば、showは叩かれることはないかもしれません。今回は機能が消えているので、本当に入り口のAPIのエンドポイントをいくつか残して、あとは消して問題なさそうと思っていました。実際にNewRelicを確認してそれが正しいことがわかりました。
実際にコードを消す
コードを消していくときに留意したことを書きます。
良い粒度でコミットする
あるまとまりで消し、全てテストが通っている状態で git commitします。後で何かあった時のrevertや、ミスったときに戻るのを楽にするためです。
とにかくビッグバン的な手法を避け、順序立てて、しかし大胆に漏れなく進めていきましょう。
依存元から順番に消す
当たり前ではありますが、重要です。依存先から消すと、その状態ではテストが通らないので結局元を消し切るまでコミットできず、ビッグバン的な手法になってしまいます。依存元から消していけば、消す途中の状態でも動かないコードがないので、コミットを重ねながら順にすすめることができます。
ここまでの手順では、サービス自体の依存元である、ランキングサービスへのAPI呼び出しを消してきました。そして現在、依存されていないAPIエンドポイントが消せる状態になりました。
ここからは、サービス内で依存関係の順番に消していきます。
なお、言語やフレームワークに依存するような内容は特段ありませんが、ランキングサービスはRuby on Railsで書かれているため、用語や構成はそれに寄っています。
ルーティングを消す
まずは不要と判明しているルーティングを消します。API呼び出しをしているテストコードがある場合は、同時に消す必要があります。
コントローラを消す
エンドポイントを消せているのはメソッドごと消していきます。旧クライアントなどの事情で消せていないものは、固定で空配列や404を返すなどしていきます。
シリアライザを消す
jsonのシリアライザとしてactive_model_serializersを使っており、シリアライザのファイルは例えば V2::RankingSerializer のようにAPIバージョンに合わせてバージョニングしています。シリアライザはコントローラーから使われていますが、コントローラーを上で消したので、これも消していきます。
一部残っているエンドポイントも、固定で404や空配列を返しているだけなので、シリアライザは利用していません。v1, v2のシリアライザは全て消せました。
サービスクラスを消す
コントローラーからは、サービスまたはモデルが直接呼ばれています。サービスはモデルを呼んでいるので、先に消しやすいのはサービスです。
モデルクラスを消す
同じく、テストも一緒に消していきます。
無事、コードをすべて消せました。少し手戻ったりしましたが、ここまでの作業時間は2時間くらいです。
デプロイする
Staging環境にリリースして数日置く
「不要なコードの削除」という行為の性質上、影響範囲を見切るのが難しい、という問題があります。特に、元々他サービスから呼び出されていたAPIを消す、というところに不安材料があります。もちろん、何も影響していないことを期待しているのですが。
とはいえ、緊急度も全く高くないので、QAチームにがっつりリグレッションテストを依頼するほどでもないと判断しました。
幸い、QA等で社内でStaging環境のアプリが利用されることは多いので、とりあえずStagingに置いてみて、その期間にエラーが起きていないかを確認しました。これにより、大きな問題が起こる確率を下げることが出来ると思います。
もっとも、何日放置すればどれくらいの確率で防げるというようなものを定量的に測ることはできていません。機能追加と違いリリースを急ぐこともないので、今回は1週間様子をみました。
本番デプロイ後の監視
今回のケースでは、以下のような特徴がありました。
- リリース後に深刻な問題が起こる確率は低い。
- 事前に考えうる全ての影響範囲を網羅してテストするのは、非常に難しい。
- 問題が起きてもすぐにコードを戻してリリースできる。
したがって、事前の確認にそこまで力を入れず、かわりに本番リリース後の監視を少し丁寧に行いました。
まとめと感想
今回は、不要になってからかなり早い段階(1ヶ月程度)でコードを消すことで、非常に短い実作業時間で行うことができました。
作業は、常に依存元から消していくことで、段階的に行いました。影響範囲を見切るのが難しいため、リリース後の監視に重点を置きました。
7,600行の削減を行った感想としては、やはりコードの量が少ないことはそれだけで良いことが多い、というものです。新しく開発に入るメンバーにも「このコードは使われていないです」などと説明しなくてすみますし、開発速度にかなり直接影響してくる感覚があります。また、消せるのが分かってからすぐに消したことで、消すための負担も少なくて済みました。
最後に、消すコードが生まれるというのは、ビジネスの成長の証だと感じています。ランキングも次の形へステップアップしたということで、コードを消すという行為にも非常に前向きになれる理由はそこなのかなと思いました。
ビジネスの成長を実感しつつ、さらにFiNCのサービスを前に進めていって下さる方、FiNCではエンジニアを絶賛募集していますので、ぜひカジュアルにご連絡ください。お待ちしております。
追伸
先日、ぎんざRuby会議01に参加・発表してきましたが、その中で、「コードを消す」という関連するテーマで非常に興味深い発表があったので、紹介したいと思います。
ActiveSupport::Multibyte::Unicode::UnicodeDatabase を消したかった (Fumiaki MATSUSHIMA)
Ruby on Railsで最も大きい 1MB超のdatファイルを消すため、障害を1つずつ取り除いて順に消していくストーリーに、聴いていて引き込まれました。最後はRailsの新バージョンが出れば消せるという状態まで進んでいます。Railsほど世界中で使われるライブラリのサイズが1MB減ることの影響を考えると、非常に素敵な貢献だなと感じました。