Upgrade to Pro — share decks privately, control downloads, hide ads and more …

10年モノのレガシーPHPアプリケーションを移植しきるまでの泥臭くも長い軌跡 / legacy-php-app-migration

10年モノのレガシーPHPアプリケーションを移植しきるまでの泥臭くも長い軌跡 / legacy-php-app-migration

toshimaru

March 08, 2024
Tweet

More Decks by toshimaru

Other Decks in Technology

Transcript

  1. 自己紹介 • としまる( : @toshimaru_e ) • メドピア株式会社 基盤開発グループ エンジニア

    • グループ・ミッション:「健全なサービス基盤を提供・維持し、medpeer.jp の円滑な事業成長に貢献する。」 • 技術的負債解消マンやっています • 元・PHPer → 現・Rubyist • Ruby も好き、PHP も好き
  2. どれくらいレガシー? ✓ オレオレ・フレームワーク(自 作の独自フレームワーク) ✓ とっくにEOL なPHP バージョン ✓ 標準サポート終了したEC2

    サー バー ✓ PEAR/PECL なライブラリ管理 ✓ 自動化されていないテスト・デ プロイ ✓ 担当プロダクトオーナー不在 ✓ 担当エンジニア不在 ✓ 仕様に関するドキュメントはほ ぼ無し(ソースコードが仕様 書) ✓ 無駄な処理が多くパフォーマン スが悪い
  3. 登場人物・システム • Legacy App: 移植対象となるPHP アプリケーション(主人公)。 • オレオレ・PHP フレームワーク製 •

    本発表で「レガシー化する」といったときには、「負債化がひどく進行し、著し くメンテナンスしにくい状態」を指します • New App: 移植先となるRails アプリケーション。 • Ruby on Rails 製(現在の社内標準技術) • New: Legacy App と比べて相対的に新しいという意味 • 移植プロジェクトチーム: Legacy App→New App の移植を担当するチーム。
  4. 2007 年〜 医師専用コミュニティサイト「Next Doctors (現 MedPeer )」の運用を開始 2009 年 2014

    年 「Next Doctors 」を「MedPeer 」に改称 東証マザーズ市場上場 2015 年 2016 年 共通サービスをGo で一部マイクロサービス化 PHP から Rails への移植作業開始 2017 年 2022 年 社内標準技術はRails で着地 本格的なPHP の移植プロジェクトがスタート 黎明期 発展期 迷走期 安定期 MedPeer の沿革と言語選定の歴史
  5. なぜ移植するか? • Legacy App のPHP バージョンがEOL • 近年のセキュリティ・インシデント増加を背景として「EOL を迎えたソ フトウェアは基本使わないように!」というお達しが出る

    (2021 年) • Legacy App のメンテナビリティが厳しい • 社内からPHP 専任のエンジニアは消失(退職 or Ruby エンジニアへ転身) • 臭いものに蓋をし続けて、パンドラの箱状態に
  6. 4つの負債解消アプローチ 家で例えると… 説明 リファクタリング リノベーション Legacy App をコツコツとリファクタリング マイグレーション お引越し

    Legacy App → New App へマイグレーション リプレイス 作り直し スクラップ& ビルド Legacy App を全廃棄、New App をフルスクラッチ 削除 解体 取り壊し Legacy App を削除
  7. ๏ ボトムアップで少しずつ安全 に進められる ① リファクタリング(リノベ戦略) PROS CONS ๏ チマチマと直すので、全てを キレイにするには膨大な時間

    がかかる レガシー化する前の健全なシステムであれば有効なアプローチ (※レガシー化した後では「手遅れ」なケースが多い)
  8. ๏ 機能単位・サービス単位・コ ンポーネント単位で安全に進 められる ๏ 優先度の高い一部分だけを移 植対象にすることが可能 ② マイグレーション(お引越し戦略) PROS

    CONS ๏ 移植しやすい単位を適切に切る必要があ る ๏ 新旧システムの互換性を考慮する必要アリ ๏ 新旧システムの並行運用期間が必要 ๏ 優先度の低い機能は放置され「塩漬け」 になるリスク 安全性・速度・柔軟性ともにバランスのとれたアプローチ
  9. ๏ 一撃で負債解消できる ๏ 新旧システムの並行運用期間 は短くできる ③ リプレイス(スクラップ&ビルド戦略) PROS CONS ๏

    必然的にビッグバンリリースとなりリ スク大 ๏ スコープがでかいので時間がとてもか かる ๏ トップダウンの意思決定(経営層の理 解)が必要 成功させれば一撃必殺となるハイリスク・ハイリターンなアプローチ
  10. ๏ 簡単!早い!(消すだけ) ๏ 安い!(工数かからない) ④ 削除(取り壊し戦略) PROS CONS ๏ 「なるほど完璧な作戦っスね―ッ

    不 可能だという点に目をつぶればよぉ 〜」 ๏ 巻き込み事故に注意(必要ないと思っ て消したら実は必要だったケース) これができれば最高なアプローチ (※現実的には全て削除は不可能で、部分的な削除になることが多い)
  11. 4つの負債解消アプローチ まとめ 速度 安全性 工数 オススメ度 リファクタリング ✕ ◎ ◯

    ✕ マイグレーション △ ◯ △ ~ ◯ ◯ リプレイス ◯ ✕ △ △ 削除 ◎ △ ~ ◯ ◎ ◯ レガシー化が進行した後ではあま りに無力! 安全に小さく始めることができる のでまずはコレがオススメ! 比較的小さいサイズの負債であれ ばワンチャン狙うのもアリ? 捨てれる負債はさっさと捨てるの が吉!
  12. Legacy App 討伐戦略 • マイグレーション(お引越し戦略) • ストラングラーフィグパターンで少しずつ移植を進めた • 削除(取り壊し戦略) •

    移植不要な機能・画面・仕様は削除しつつ移植 • 結果として移植対象の10-15% 程度は削除できた
  13. 開発環境をコンテナ化 • まずは Dockerfile で Legacy App をコンテナ化 • 依存をコンテナ内に閉じ込める

    • ポータブルな環境の構築 • 開発環境構築プロセスの簡易化 テスト環境でも再利用可能なコンテナの誕生
  14. テスティング・CI • GitHub Actions でコンテナ化したLegacy App をビルドし、既存のPHPUnit を実 行するところからスタート •

    テストケースが不十分とはいえ、CI が回り始めてステータスが になる安心 感はサイコー • 社内標準技術でE2E テスト環境を整備 (RSpec+Capybara+HeadlessChrome) • " 対レガシーコード戦の初期フェーズにおいて、E2E テストで砦を作ることは 非常に有効です" (「品質とスピードに関する16 の質問に答えてみた」 t_wada (和田 卓人))
  15. CD ・デプロイ自動化 • Before (旧・デプロイ方式) • 古き良き、サーバーSSH+ デプロイスクリプト実行 • After

    (新・デプロイ方式) • GitHub Actions からOIDC でAWS 認証を通して、AWS Systems Manager の Run Command でデプロイスクリプトを実行 既存のデプロイスクリプト資産を活かしつつ、デプロイ自動化を達成
  16. コードフリーズ宣言 • Legacy App のコードフリーズを宣言 • これ以上技術的負債が膨らまないように入口を塞ぐ • 「変更を加えるならテストを書いてね!」という制約を課す •

    例外として Legacy App の弱体化につながる変更(機能削除・コード削除)は OK とする • 上記がきちんと守られるように、移植プロジェクトチームを Legacy App リポジ トリの CODEOWNER に設定 • Legacy App の変更すべてに移植プロジェクトチームのレビューの目が入る
  17. 3つの移植方針 方針 説明 ロジックKeep移植 Legacy App のロジックを維持したまま移植。 仕様Keep移植 Legacy App

    の仕様を維持したまま移植。 リニューアル Legacy App をゼロからリニューアル。
  18. ① ロジックKeep 移植 Legacy App のロジックを維持したまま移植 • 同じ言語間・似ているフレームワーク間の移植であれば有効な手段に なりえる •

    異なる言語間・フレームワーク間の移植だと無理が出る • 過去の移植プロジェクトはこの方針で移植を実施し、Rails CoC 無 視なコードが書かれ、結果として New App 側に新たな負債を生む ことに…
  19. 移植方針まとめ 仕様 UI ロジック おすすめ度 ロジックKeep移植 そのまま そのまま そのまま △

    仕様Keep移植 そのまま そのまま 変更 ◯ リニューアル 変更 変更 変更 ◯ 多くの場合で、コードを読み解いて 再実装したほうが結果的に速そう 仕様調整の必要がないので、開発者 のみでサクサク進められるのが◯ リニューアルできるのであれば、 さっさとリニューアルするのが吉!
  20. Legacy App 移植戦略 • 仕様Keep 移植 • 仕様負債が散見されるものの、仕様変更はせず移植する • 動作確認も

    Legacy App ⇔ New App で動作比較をすればいいだけで簡単 • 仕様をKeep しつつも明らかに不要なコードベースは移植対象から除外 • どこからも参照されていない画面 • どこから呼ばれていない JavaScript コード • 表示上無意味な HTML/CSS コード
  21. Legacy App 移植戦略 〜要望トリアージ編〜 • とはいえ移植をやっていると欲が出てくる • PO 「この機能、実はバグってて… ついでに直せない?」

    • 開発者「この複雑な仕様、どう考えても不要じゃね?」 • 出てきた要望は下記のような判断基準でトリアージ • 実装工数を減らすことに繋がる要望は Accept • 実装工数が嵩むことに繋がる要望は Reject
  22. Legacy App 移植戦略 〜既存仕様理解編〜 • 基本的には泥臭くソースコードを追って既存仕様を紐解いていく • 画面をいろいろイジったり、ソースコードを改変してみたりしながら既存 仕様の理解を深めていくプロセス •

    しかし2024 年に生きる我々にはAI という強力な銀の弾がある • レガシーコードをGitHub Copilot に放り投げて解説してもらうだけでも コードリーディングの速度・解像度がグンと上がる • ※ オレオレFW による独自コード・ロジック内に無駄な処理が多かったた め、Copilot による単純なコード変換はあまり使えなかった
  23. 実装方針 • 仕様Keep 移植においては Input/Output が一致 していればOK • Input: リクエストパス,

    リクエストパラメータ • Output: View (HTML/Text), UI, データ in/out に着目して 仕様を徐々にビルドアップしていく App Input Output
  24. 実装 Step1 • まずを最小限の正常系をガッチリと固める • [in] リクエストパス ► [out] 静的HTML

    • 正常系のE2E テストを書いて土台とする • この時点ではDB インタラクションはなく、ダ ミーデータの出力でOK App Request Static HTML In Out
  25. 実装 Step2 • 次にDB との接合部をロジックに徐々に加えていく • [in] リクエストパス ► [out]

    動的HTML • この過程で“ 外せない” 仕様はきちんとテストに 反映させていく • TIPS: バカ正直にロジック読むよりは、DB に流 れるクエリログからロジックをサルベージ・再構 築したほうが速かった App Request Dynamic HTML In Out
  26. 実装 Step3 • 仕上げに異常系をカバーしていく • [in] リクエストwith 不正なID ► [out]404

    ページ • [in] リクエストwith 不正な入力値 ► [out] バリデーションエラー App Invalid Request Error In Out
  27. フィーチャーフラグを利用して移植 • Legacy App に /feature という移植対象パスがあった場合、下記の ように Reverse Proxy

    を設定 • /feature → Legacy App • /feature2 → New App • /feature2 のパスは移植用フィーチャーフラグが ON の場合に表示 • リリース時は New App の /feature2 を /feature にして Reverse Proxy を切り替え
  28. フィーチャーフラグを利用して移植(開発時) Legacy App New App Reverse Proxy /feature /feature2 -

    /feature/xxx - /feature/yyy - /feature/zzz - /feature2/xxx - /feature2/yyy - /feature2/zzz ※フィーチャー フラグでアクセス制御
  29. Legacy App New App Reverse Proxy /feature - /feature/xxx -

    /feature/yyy - /feature/zzz - /feature/xxx - /feature/yyy - /feature/zzz ※フィーチャー フラグでアクセス制御 /feature2 → /feature ✕ nginx.conf で Proxy先スイッチ routes.rb の 設定切り替え フィーチャーフラグを利用して移植(リリース時)
  30. Legacy App New App Reverse Proxy - /feature/xxx - /feature/yyy

    - /feature/zzz ※フィーチャー フラグでアクセス制御 /feature ✕ nginx.conf で 切り戻し フィーチャーフラグを利用して移植(ロールバック時) /feature - /feature/xxx - /feature/yyy - /feature/zzz 元通り
  31. テスト • テスト担当を決めてテスト実施 • PO がいればPO 、いなければ移植対象に関連する運用チームであった りカスタマーサポートチームにテストを依頼 • あるべき仕様がロストしていたとしても、Legacy

    App ⇔ New App の 差分を確認することで仕様が保たれているかはテスト可能 • TIPS: 自動テスト項目をrspec --format documentation でド キュメント出力し、テスターに渡しておくと、仕様の理解促進やテスト 実施項目の削減にもつながって◯
  32. スーパー高速化達成の内実 • 普通に実装しただけ(高速化のためのトリッキーな実装をしたわけで はない) • 無駄なクエリ・N+1 クエリを排除、効率的なクエリを心がける • 各処理において無駄な処理を実行しない •

    移植対象のレスポンス速度は、New App で動いている他の機能のレス ポンス速度と比べると同水準のレスポンス速度 • Legacy App が単に遅すぎただけ説
  33. 移植後の機能開発 • コードベースがメンテナンス可能になり、開発チームに引き継ぐこと ができた • 開発者: 社内標準技術でコードを読み書きできるようになった • PO: バグ修正依頼・機能要望を出すことができるようになった

    • 引き継いだことにより、エンハンス開発も再開された • 早速、移植時点ではレスポンシブデザイン非対応だったページが、 レスポンシブ対応されるという改善が入った
  34. 残課題 • Legacy App に依然として残り続けるPHP 管理画面… • Legacy App のデータベースは今も元気に稼働中!

    • Go で作られたマイクロサービスはどうする? 俺達の戦いは これからだ!
  35. 今後 • 基本方針としてはモノリス化を目指す(モノリシックRails 戦略) • Modular Monolith にするかどうかは検討課題 • 本発表で紹介した移植により、Legacy

    App は管理画面だけとなり、 ユーザー影響を局所化できたので PHP Upgrade する方針で考えてい る • 最終的には上述のモノリスに統合させる方向で進めたい(時期未 定)
  36. 2007 年〜 医師専用コミュニティサイト「Next Doctors (現 MedPeer )」の運用を開始 2009 年 2014

    年 「Next Doctors 」を「MedPeer 」に改称 東証マザーズ市場上場 2015 年 2016 年 共通サービスをGo で一部マイクロサービス化 PHP から Rails への移植作業開始 2017 年 2023 年 社内標準技術はRails で着地 エンドユーザー側のPHP の移植が完了 202X 年 20XX 年 PHP/Go をRails に統合 すべてのサービス群をRuby on Rails モノリス化 黎明期 発展期 迷走期 安定期 モノリス期 ✕ ✕ MedPeer アーキテクチャの今後
  37. 参考資料 • メドピア公式ブログ・資料 • Golang (Go 言語)を採用して、たった二人で基盤となるAPI ゲートウェイを開発した話 (2015 年)

    • レガシーな独自フレームワークから脱却してRails へ徐々に移行している話 (2017 年) • メドピアの全力Rails 化の取り組み晒します! (2017 年) • ストラングラー フィグ パターン - Azure Architecture Center • 動作するきれいなコード: SeleniumConf Tokyo 2019 基調講演文字起こし+! - t-wada のブロ グ • カミナシでの技術的負債返済プロジェクトとその決断