こうすれば怖くない!レガシーシステムの置き換え作業

Yusuke Shinyama
Product Run
Published in
18 min readJun 20, 2024
Photo by AXP Photography on Unsplash

歴史とは何か? それは過去と現在との、終わることのない対話である。
— E. H. カー

どうも、Tanzu Labsエンジニアの Yusukeです。 今回は我々が最近おこなったアプリケーション・モダナイゼーションの取り組みについてご紹介したいと思います。

「モダナイゼーション (近代化、modernization)」とは、いわゆるレガシーなシステムから 技術的な負債を取り除き、クリーン・効率的かつ、安全で信頼性のあるシステムに刷新する作業のことです。 と、言葉で書くと短いのですが、これは実際には大変な作業です。

最初に、レガシーシステムにあるあるな問題として:

  • もとの開発スタッフが残っていない。
  • 設計文書もほとんど存在しておらず、誰も把握していない複雑怪奇な仕様がある。
  • 正式な動作検証の枠組み (テスト) がないため、うかつに変更できない。
  • にもかかわらず、ビジネス上重要な位置を占めており、うかつに停止もできない。

といったことがあげられます。

このような難問に対する、伝統的なリプレイスの手法は以下のようなものでした:

  • もとのコードを読み、仕様を復元する。
  • すべて再実装し、一気にリプレイスする。

しかしこれは非常に困難かつハイリスクな試みです。これに対して、私たち Tanzu Labs の提案するモダナイゼーションの手法は次のとおりです:

  • レガシーなシステムを改修せず、新規に モダンなシステムを開発する。このときレガシーなコードをほとんど参照せず、ビジネス的な要求から新たに仕様を書き起こす。
  • 現行のシステムを稼動させつつ、新システムに 段階的に 移行する。
  • これにともない、新システム開発・維持のための チームおよび文化 も同時に熟成させる。

そんなこと本当にできるの? と思われるかもしれません。 今回は、エンジニアとしての視点から、私たちが実際にこれをいかに達成したかを具体例をまじえてご説明します。

最初におことわりしておきますが、一口に「レガシーシステム」といっても千差万別であり、画一的な方法が適用できるわけではありません。 上に挙げた手法が比較的簡単にいくケースもあれば、そうではないケースもあります。 以下で説明することは私が経験したモダナイゼーションのひとつの例にすぎず、 システムのプラットフォームとして一般的なWebアプリを想定しています。システムによっては、異なるアプローチが適していることもあります。

Strangler Figパターンとは?

Photo by David Clode on Unsplash

私たちが今回とったアプローチは “Strangler Figパターン” [1] と呼ばれる手法です。まず、レガシーアプリの機能の一部のみを新たに実装し、これをレガシーアプリと並列に稼働させます。このモダンアプリの機能および仕様の切り出しは、私たちがSWIFTと呼んでいる方法を使ってビジネス的な要求をもとに行います。SWIFTについては、過去の記事「Labs流 アプリケーションモダナイゼーション」を参照してください。

2つのアプリの並列稼働を実現する仕組みは、アプリケーションロードバランサー (以下、ALB) を使います。これはレガシーとモダンの機能をURLによって分岐させ、モダンアプリで実装されている機能はモダンアプリ側を優先して使うように設定します。

モダナイゼーション初期の状態 (一部の機能のみが実装されている)

SWIFTは継続的なプロセスであり、機能の切り出し → 実装 → 評価を繰り返し行っていきます。これを続けるとモダンアプリの機能は徐々に増えていき、いずれはレガシーアプリの機能のほぼすべてをカバーするようになります。こうなるとレガシーアプリの機能はもはやほとんど使われなくなり、事実上レガシーアプリがモダンアプリで置き換わった状態になります。このような手法を、“Strangler (締め殺し)” をおこなう樹木になぞらえて Strangler Fig パターンと呼びます。

モダナイゼーション成熟期の状態 (ほとんどの機能が実装されている)

ただし Strangler Figパターンはつねに使用可能というわけではありません。これが使えるためには、以下の2つの条件が必要です:

  1. 改修したい機能が、ユーザとの直接インタラクションをおこなう、いわばアプリの「表面」近くに配置されていること。
  2. 改修したい機能 (エンドポイント) が、ALBの単純なURLマッチングによって識別できること。

置き換え対象の機能がアプリの内部深くに埋め込まれている場合は、まずレガシーアプリの一部を改修し、モダンな部分との切り替えが可能な「引き込み線」を作成する必要があります。これは “Branch by Abstraction” パターン [2] と呼ばれますが、今回は解説は省略します。

改修したい部分がアプリ内部に埋め込まれている場合 (Branch by Abstractionパターン)

ブラックボックスとしての「アプリ」

Photo by Zaeem Nawaz on Unsplash

さて、このようにしてモダンアプリの実装を開始するわけですが、そもそも必要な仕様がはっきりとわかっていない状態で、どうやってアプリが「正しく」実装されていることを確認できるのでしょうか?

そのためには、実装対象のアプリをブラックボックスとして考える必要があります。アプリ内部の動作は無視して、外部から観測可能な入出力だけに着目してみると、究極的にはWebアプリがやっているのは:

  1. ユーザから、なんらかの入力 (HTTPリクエスト) を受け取る。
  2. ユーザに適切な応答 (HTTPレスポンス) を返す。
  3. 同時に、データベース (以下、DB) になんらかの書き込みをおこなう。

ということだけです。(ここでは単体で完結していて、外部システムとの連携がないアプリを想定しています。) 逆にいえば、もしレガシーアプリが受け取るすべての入力に対して、モダンアプリもまったく同じ出力を返せば、それは「同等の機能をもつアプリ」といえることになります。これがE2E (end-to-end) テストを使った動作検証のアイデアです。

ブラックボックスとしてのWebアプリ

E2Eテストの実装

Photo by Nguyen Thu Hoai on Unsplash

ここで、私たちのプロジェクトにおけるE2Eテストの仕組みについて説明しましょう。まず、本番環境でレガシーアプリに来たHTTPリクエストとレスポンスの対を記録しておきます。私たちの場合、これは GoReplay [3] というツールを使って行いました。GoReplayは「非侵襲的」なツールで、ネットワークの盗聴機能を使うため、既存のアプリに対して並列に設置するだけでよく、(プロキシなどを挿入するのとは違って) 環境に変更を加える必要がありません。次にレガシーアプリが動いている本番環境とは個別に「E2E環境」と呼ばれるものを作成し、ここでレガシーアプリとモダンアプリを並列に動かします。GoReplayには記録したHTTPリクエストの再生機能もあるので、これを使ってレガシーアプリとモダンアプリに本番そっくりのリクエストを送ります。具体的なE2Eテストの手順は以下のとおりです:

  1. ある時点での本番環境DBのスナップショットを用意し、これをレガシーアプリ用とモダンアプリ用のDBとして複製する。
  2. GoReplayで取得した本番環境のHTTPリクエスト (数時間〜数日分) を、レガシーアプリとモダンアプリに対して再生する。このとき、各アプリが返すHTTPレスポンスも同時に取得する。
  3. レガシーアプリとモダンアプリのレスポンスを比較する。
  4. すべてのリクエストを送った時点で、レガシーアプリ用とモダンアプリ用の各DBテーブルを比較する。
本番そっくりの状況をE2E環境で再現し、レガシーとモダンを比較する

HTTPリクエストとレスポンスの比較、およびDBテーブルの比較については独自の比較ツールを作成しました。HTTPリクエストとレスポンスについては、特定のヘッダおよびボディのみを抜き出し、これらが完全一致していれば「一致」、そうでなければ「不一致」として扱っています。DBテーブルについても同様で、両方のテーブルから特定のカラムのみを抜き出し、これらが完全一致している行を「一致」として扱いました。DBテーブルの比較については、レガシーアプリの出力に対してモダンアプリの出力が「不足している部分」と「余計な部分」が存在するので、一致率を算出する際には注意が必要です。

レガシーとモダンの出力比較

DBテーブルの比較ツールは基本的に「テーブル行に対するdiff」を行っていると考えてよいでしょう。その出力は、たとえば以下のようになります:

レガシーとモダンのDBテーブルの差異

参考までに、GoReplayで記録したHTTPリクエストとレスポンスのサンプルも紹介しておきます。 “1”で始まる行がHTTPリクエスト (ヘッダ+ボディ) であり、“3”で始まる行がHTTPレスポンス (ヘッダ+ボディ) です。各リクエストとレスポンスには一意なIDが付与されており、これをみることで「どのリクエストに対するレスポンスか」がわかるようになっています。

GoReplayによるリクエストとレスポンスの記録

このようにしてE2Eテストを開発中のモダンアプリに対して行っていきます。最初は5分間分 (アプリが5分間のあいだに受け取る量) のリクエストから始めました。各E2Eテストごとに、レガシーとモダンとの差異を検証し、新しい仕様をモダンアプリに追加していきます。モダンアプリの実装が進むにつれてリクエスト量を増やしていき、最終的には24時間分のリクエストに対して、レスポンス一致率99.96%、テーブル一致率99.98%を達成するところまできました。残った不一致はビジネス的に影響がないことが判明したため、この時点でリリースを決定しました。

E2Eテスト結果の変遷

実際の開発業務

Photo by Annie Spratt on Unsplash

さて、モダナイゼーションのおおざっぱな枠組みは説明しましたが、実際の開発業務はどうだったのでしょうか? SWIFTの最初のフェーズである分析 (Event Storm, Boris, SNAP-E) が完了したあと、エンゲージメントのほとんどの期間 (70%程度) はシステムの実装に費やしました。これは私たち Tanzu Labsの伝統であるXP (エクストリームプログラミング) の原則にのっとって行いました。具体的には:

  1. SWIFTの分析にもとづいて、PM (プロダクトマネージャ) が仕様をストーリーとして書き出す。
  2. チーム全員でIPMを行い、Dev (エンジニア) がストーリーの実装をおこなう。
  3. E2Eテストの結果にもとづいて、PMが不足した仕様を洗い出し、ストリーとして追加していく。

これらに加えて、毎日のスタンドアップや、ペアプログラミング、テスト駆動開発、CI/CDによる徹底的な自動化の追求など、私たちTanzu Labsがこれまで実施してきた文化を継承しました。

なお E2Eテストの計画と実施、データの検証等はすべて私たちのチームのPMが行ってくれたのですが、エンジニアからみると彼らの働きは「英雄的」だったということを記しておきます。当のPM陣にとってもこの部分は楽しかったようです。通常の新規開発では PMは「何をつくるべきか」で悩むのですが、モダナイゼーションではこの目標が明確に定まっており、しかも本番環境との差異が客観的に数値で確認できるため、「真の Build-Measure-Learnのサイクルが実現できた (PM談)」とのことです。このあたりはモダナイゼーション開発の醍醐味と言えるかもしれません。

また、実装にあたってはモダンアプリのコードをクリーンに保つための工夫も行いました。そのひとつが “Anti-Corruption Layer” (ACL) [4] と呼ばれる方法です。今回のシステムでは、レガシーアプリとモダンアプリが本番環境で同一のDBにアクセスするようになっています。しかし、コードと同様に、レガシーアプリのDBも長年にわたる追加・改築が繰り返されてきており、わかりにくい構造になっていました。具体的には:

  • 理解しにくい (あるいは本来の意味と異なった使われ方をしている) カラム名
  • 一貫しない型宣言 (あるテーブルでは INTEGER型のカラムが別のテーブルでは VARCHAR型になっているなど)
  • もはや使われていない、あるいは用途が不明なテーブル

などです。通常、アプリのロジックはデータベースの構造に依存します。そのためレガシーアプリのDBにアクセスしようとすると、モダンアプリのコードも必然的にレガシーなDB構造に「引きずられる」ことになってしまいます。そこで、モダンアプリとDBの間にACL層を導入し、これにレガシーDBの特殊な構造をすべて吸収させるようにしました。こうすることでモダンアプリは「クリーンな」DB構造を想定して設計することができ、レガシーDBの「レガシーさ」をモダンアプリに持ち込まない (“汚染”させない) ようにできます。

レガシーDBの「レガシーさ」を吸収するAnti-Corruption Layer (ACL)

モダンアプリには他にもCI/CDパイプラインからのSlackへの通知機能や、Testcontainersを使った本格的な統合テスト、Gatlingを使った自動負荷テスト、レスポンスタイムの監視など、多くのノウハウを詰め込んでいます。おかげで、お客様から「将来にわたって社内の模範となるコードベースができた」という評価をいただくことができました。

そしていよいよリリース

Photo by Jens Herrndorff on Unsplash

モダンアプリの実装を開始してから約1ヶ月ほどたち、E2Eテストの結果が満足いくものになったので、いよいよモダンアプリを本番環境にリリースすることになりました。繰り返しますが、これはまだモダナイゼーションの初期段階であり、レガシーアプリの特定のエンドポイントだけを置き換えるものです。また、最初から全リクエストを対象にするのではなく、「特定のエンドポイントにくるHTTPリクエストのうち、1%だけをモダンアプリに振り分ける」という形でリリースしました。これはALBの設定を調整することによっておこなうことができます。

リリース後のALB比率の変遷

今回は実際に稼働している環境を変更するため、あらかじめリリースの詳細な手順や、万が一うまくいかなくなったときの対処方法などをチーム内で取り決めておきました。そのためもあってか、実際のリリース初日はとくに大きな事故もなく、ALBの設定を変更するだけの作業なのですぐに完了しました。その後、徐々にモダンアプリへのリクエスト比率を上げていき、2週間後には50%のリクエストをモダンアプリで処理できるようになりました。この間、毎回のリリースごとにPMが抜き打ちでDBチェックをおこない、監視サービスにおけるメトリクスなども収集し、異常が起きていないことを確認しました。

SWIFTの枠組みでは、リリース作業が一区切りついたあとに、ふたたび分析と実装をおこない、この「リリース → 運用→ 機能拡張」のサイクルを繰り返します。およそ3ヶ月にわたるエンゲージメントの中で、私たちのチームは3回の大きな機能拡張をおこない、そのうち2回目までをリリースすることができました。この時点でモダンアプリはアプリが受け取る (分量ベースで) 9割近くのリクエストを処理できるようになっており、ビジネス的に重要なロジックを十分にカバーできました。最後にチームで技術的な決定の理由を記した文書 (Architectural Decision Records, ADR) や各種ノウハウを記した「クックブック」を作成し、エンゲージメントは終了しました。

おわりに — エンゲージメントを振り返って

Photo by Federico Respini on Unsplash

いかがだったでしょうか? この記事では、おもにエンジニアの観点から、できるだけ具体的なモダナイゼーション・プロジェクトの例を説明しました。約3ヶ月間のエンゲージメントを振り返っての感想は「楽しかった!」の一言につきます。実際に稼働しているミッションクリティカルなシステムを改修するという非常に貴重な体験をさせていただきましたし、そのシステムが扱っているビジネスドメイン固有の複雑さ・歴史的な奥深さをうかがうこともできました。これは新規開発案件ではなかなかできない体験です。

個人的に一番大きかった学びは、レガシーなシステムの内部を掘り下げていくにつれ、レガシーなシステムに対する一種の「共感」のようなものを得ることができた、という点です。多くの人は、レガシーなシステムを「なんとなく悪いもの」のようなイメージでとらえることが多いように思うのですが (自分もその一人でした)、そもそもレガシーなシステムは今から数十年も前のエンジニアが試行錯誤のすえに開発してきたものです。彼らは時間的・技術的な制約の中で彼らなりのベストをつくしたはずであり、意図して「悪い設計」にしようとしたわけではありません。そう考えると、今となっては理不尽に見える設計上のいろいろな決定も「ここはこういう事情があったんだろうなあ…」などと思いを馳せることができます。この体験が今回のモダナイゼーションプロジェクトにおける最大の収穫でした。

ちなみに、Tanzu Labsにおけるモダナイゼーションのアプローチで個人的に気に入っている部分を挙げておきます。それは、このアプローチが「万能を謳っていない」ということです。私たちのエンゲージメントは「これをやれば一発解決」というようなサービスではありませんし、おそらく万人向けでもありません。XPという人によって好みが分かれる方法にもとづいており、SWIFT自体も、どちらかといえば「設計手法」というよりは「チーム作業におけるひとつの流儀」に近いと感じます。ただ公式どおりに作業していけばよいというものではなく、最終的には人 (チーム) と “Do What Works” の精神がものをいうのだ、ということをあらためて認識しました。

最後になりましたが、本エンゲージメントを可能にしてくださったステークホルダーおよびチームの皆様に感謝いたします。

参考文献

--

--

Yusuke Shinyama
Product Run

Passionate programmer and teacher, or something else.