Slide 1

Slide 1 text

10 年モノのレガシーPHP アプリ ケーションを移植しきるまで の泥臭くも長い軌跡 2024/03/08 PHPerKaigi 2024 Toshimaru Enomoto

Slide 2

Slide 2 text

自己紹介 • としまる( : @toshimaru_e ) • メドピア株式会社 基盤開発グループ エンジニア • グループ・ミッション:「健全なサービス基盤を提供・維持し、medpeer.jp の円滑な事業成長に貢献する。」 • 技術的負債解消マンやっています • 元・PHPer → 現・Rubyist • Ruby も好き、PHP も好き

Slide 3

Slide 3 text

話すこと • 10 年モノのレガシーなPHP アプリケーションをRaills アプリケーショ ンに移植した話をします • 基本的にカッコいい感じの話でなく泥臭めな話が多いと思います

Slide 4

Slide 4 text

話さないこと • 最新のPHP 事情 • 俺が考える最強のイケてるアーキテクチャ • 移植前・移植後のシステム詳細

Slide 5

Slide 5 text

本発表のゴール • 下記のオーディエンスに参考になる話があれば嬉しい • 今後、技術的負債の返済をしていく予定の開発者 • 現在進行系で技術的負債の返済を行っている開発者 • 将来技術的負債を返すことになりそうな開発者 • 本発表を技術的負債返済ストーリーの一事例として、話のネタにして もらえれば

Slide 6

Slide 6 text

CONTENTS WHY なぜ移植するか? HOW RESULT FUTURE どう移植するか? 結果どうなったか? 課題と今後どうするか?

Slide 7

Slide 7 text

How Legacy? 〜そのApp, どれくらいレガシー?〜

Slide 8

Slide 8 text

どれくらいレガシー? ✓ オレオレ・フレームワーク(自 作の独自フレームワーク) ✓ とっくにEOL なPHP バージョン ✓ 標準サポート終了したEC2 サー バー ✓ PEAR/PECL なライブラリ管理 ✓ 自動化されていないテスト・デ プロイ ✓ 担当プロダクトオーナー不在 ✓ 担当エンジニア不在 ✓ 仕様に関するドキュメントはほ ぼ無し(ソースコードが仕様 書) ✓ 無駄な処理が多くパフォーマン スが悪い

Slide 9

Slide 9 text

登場人物・システム • Legacy App: 移植対象となるPHP アプリケーション(主人公)。 • オレオレ・PHP フレームワーク製 • 本発表で「レガシー化する」といったときには、「負債化がひどく進行し、著し くメンテナンスしにくい状態」を指します • New App: 移植先となるRails アプリケーション。 • Ruby on Rails 製(現在の社内標準技術) • New: Legacy App と比べて相対的に新しいという意味 • 移植プロジェクトチーム: Legacy App→New App の移植を担当するチーム。

Slide 10

Slide 10 text

Legacy App New App Reverse Proxy システム・アーキテクチャ

Slide 11

Slide 11 text

WHY? 〜なぜ移植する?〜

Slide 12

Slide 12 text

2007 年〜 医師専用コミュニティサイト「Next Doctors (現 MedPeer )」の運用を開始 2009 年 2014 年 「Next Doctors 」を「MedPeer 」に改称 東証マザーズ市場上場 2015 年 2016 年 共通サービスをGo で一部マイクロサービス化 PHP から Rails への移植作業開始 2017 年 2022 年 社内標準技術はRails で着地 本格的なPHP の移植プロジェクトがスタート 黎明期 発展期 迷走期 安定期 MedPeer の沿革と言語選定の歴史

Slide 13

Slide 13 text

なぜLegacy App は放置された? • 過去にLegacy App の移植プロジェクトは断続的に立ち上がっていた • それらのどれもが結果として局所的な移植として終わってきた • 工数・予算・人的資源の制限で移植しきれなかった

Slide 14

Slide 14 text

なぜ移植するか? • Legacy App のPHP バージョンがEOL • 近年のセキュリティ・インシデント増加を背景として「EOL を迎えたソ フトウェアは基本使わないように!」というお達しが出る (2021 年) • Legacy App のメンテナビリティが厳しい • 社内からPHP 専任のエンジニアは消失(退職 or Ruby エンジニアへ転身) • 臭いものに蓋をし続けて、パンドラの箱状態に

Slide 15

Slide 15 text

なぜ移植するか? • 組織の規模も大きくなり、組織として技術的負債返済にリソース投下 できる状態になった • 2022 年、本格的な Legacy App 移植プロジェクトチーム(3-5 名)が 発足、本格的な移植作業がスタート

Slide 16

Slide 16 text

HOW? 〜どう移植する?〜

Slide 17

Slide 17 text

「どう移植するか?」  CONTENTS 4つの負債解消アプローチから考える Legacy App 討伐戦略 “ソフトウェア開発の三本柱” で足回りを整える 3つの移植方針から考える Legacy App 移植戦略 Input/Output で考える実装方針 フィーチャーフラグを用いたスムーズな移植 01 02 03 04 05

Slide 18

Slide 18 text

4つの負債解消アプローチ から考えるLegacy App 討伐戦略 01

Slide 19

Slide 19 text

4つの負債解消アプローチ 家で例えると… 説明 リファクタリング リノベーション Legacy App をコツコツとリファクタリング マイグレーション お引越し Legacy App → New App へマイグレーション リプレイス 作り直し スクラップ& ビルド Legacy App を全廃棄、New App をフルスクラッチ 削除 解体 取り壊し Legacy App を削除

Slide 20

Slide 20 text

๏ ボトムアップで少しずつ安全 に進められる ① リファクタリング(リノベ戦略) PROS CONS ๏ チマチマと直すので、全てを キレイにするには膨大な時間 がかかる レガシー化する前の健全なシステムであれば有効なアプローチ (※レガシー化した後では「手遅れ」なケースが多い)

Slide 21

Slide 21 text

๏ 機能単位・サービス単位・コ ンポーネント単位で安全に進 められる ๏ 優先度の高い一部分だけを移 植対象にすることが可能 ② マイグレーション(お引越し戦略) PROS CONS ๏ 移植しやすい単位を適切に切る必要があ る ๏ 新旧システムの互換性を考慮する必要アリ ๏ 新旧システムの並行運用期間が必要 ๏ 優先度の低い機能は放置され「塩漬け」 になるリスク 安全性・速度・柔軟性ともにバランスのとれたアプローチ

Slide 22

Slide 22 text

๏ 一撃で負債解消できる ๏ 新旧システムの並行運用期間 は短くできる ③ リプレイス(スクラップ&ビルド戦略) PROS CONS ๏ 必然的にビッグバンリリースとなりリ スク大 ๏ スコープがでかいので時間がとてもか かる ๏ トップダウンの意思決定(経営層の理 解)が必要 成功させれば一撃必殺となるハイリスク・ハイリターンなアプローチ

Slide 23

Slide 23 text

๏ 簡単!早い!(消すだけ) ๏ 安い!(工数かからない) ④ 削除(取り壊し戦略) PROS CONS ๏ 「なるほど完璧な作戦っスね―ッ 不 可能だという点に目をつぶればよぉ 〜」 ๏ 巻き込み事故に注意(必要ないと思っ て消したら実は必要だったケース) これができれば最高なアプローチ (※現実的には全て削除は不可能で、部分的な削除になることが多い)

Slide 24

Slide 24 text

4つの負債解消アプローチ まとめ 速度 安全性 工数 オススメ度 リファクタリング ✕ ◎ ◯ ✕ マイグレーション △ ◯ △ ~ ◯ ◯ リプレイス ◯ ✕ △ △ 削除 ◎ △ ~ ◯ ◎ ◯ レガシー化が進行した後ではあま りに無力! 安全に小さく始めることができる のでまずはコレがオススメ! 比較的小さいサイズの負債であれ ばワンチャン狙うのもアリ? 捨てれる負債はさっさと捨てるの が吉!

Slide 25

Slide 25 text

Legacy App 討伐戦略 • マイグレーション(お引越し戦略) • ストラングラーフィグパターンで少しずつ移植を進めた • 削除(取り壊し戦略) • 移植不要な機能・画面・仕様は削除しつつ移植 • 結果として移植対象の10-15% 程度は削除できた

Slide 26

Slide 26 text

“ソフトウェア開発の三本柱” で足回りを整える 02

Slide 27

Slide 27 text

“ ソフトウェア開発の三本柱” (@t_wada) 27

Slide 28

Slide 28 text

“ ソフトウェア開発の三本柱” で評価 バージョン管理 自動化 テスティング GitHub でソースコード管理 部分的にPHPUnit テストコードが存在 CI/CD が未整備 28

Slide 29

Slide 29 text

開発環境をコンテナ化 • まずは Dockerfile で Legacy App をコンテナ化 • 依存をコンテナ内に閉じ込める • ポータブルな環境の構築 • 開発環境構築プロセスの簡易化 テスト環境でも再利用可能なコンテナの誕生

Slide 30

Slide 30 text

テスティング・CI • GitHub Actions でコンテナ化したLegacy App をビルドし、既存のPHPUnit を実 行するところからスタート • テストケースが不十分とはいえ、CI が回り始めてステータスが になる安心 感はサイコー • 社内標準技術でE2E テスト環境を整備 (RSpec+Capybara+HeadlessChrome) • " 対レガシーコード戦の初期フェーズにおいて、E2E テストで砦を作ることは 非常に有効です" (「品質とスピードに関する16 の質問に答えてみた」 t_wada (和田 卓人))

Slide 31

Slide 31 text

CD ・デプロイ自動化 • Before (旧・デプロイ方式) • 古き良き、サーバーSSH+ デプロイスクリプト実行 • After (新・デプロイ方式) • GitHub Actions からOIDC でAWS 認証を通して、AWS Systems Manager の Run Command でデプロイスクリプトを実行 既存のデプロイスクリプト資産を活かしつつ、デプロイ自動化を達成

Slide 32

Slide 32 text

“ ソフトウェア開発の三本柱” が整った! バージョン管理 自動化 テスティング GitHub でソースコード管理 PHPUnit + E2E テスト GitHub Actions でCI/CD を整備 32

Slide 33

Slide 33 text

Let’s 移植!

Slide 34

Slide 34 text

コードフリーズ宣言 • Legacy App のコードフリーズを宣言 • これ以上技術的負債が膨らまないように入口を塞ぐ • 「変更を加えるならテストを書いてね!」という制約を課す • 例外として Legacy App の弱体化につながる変更(機能削除・コード削除)は OK とする • 上記がきちんと守られるように、移植プロジェクトチームを Legacy App リポジ トリの CODEOWNER に設定 • Legacy App の変更すべてに移植プロジェクトチームのレビューの目が入る

Slide 35

Slide 35 text

3つの移植方針から考える Legacy App 移植戦略 03

Slide 36

Slide 36 text

3つの移植方針 方針 説明 ロジックKeep移植 Legacy App のロジックを維持したまま移植。 仕様Keep移植 Legacy App の仕様を維持したまま移植。 リニューアル Legacy App をゼロからリニューアル。

Slide 37

Slide 37 text

① ロジックKeep 移植 Legacy App のロジックを維持したまま移植 • 同じ言語間・似ているフレームワーク間の移植であれば有効な手段に なりえる • 異なる言語間・フレームワーク間の移植だと無理が出る • 過去の移植プロジェクトはこの方針で移植を実施し、Rails CoC 無 視なコードが書かれ、結果として New App 側に新たな負債を生む ことに…

Slide 38

Slide 38 text

• 仕様は保ったまま、内部構造だけを書き変える • 開発者⇔プロダクトオーナー間の仕様調整作業が発生しないのは◯ • 技術的負債が解消される一方、下記の問題は残る • 前時代的なUI • 仕様負債(不要になった仕様・無駄に複雑な仕様) ② 仕様Keep 移植 Legacy App の仕様を維持したまま移植

Slide 39

Slide 39 text

• 技術的負債の解消に加えて、下記も達成できる • 前時代的なUI の刷新 • 仕様負債の解消 • どうせ移植に工数割くくらいなら、リニューアルするのがベストな選 択! ③ リニューアル Legacy App をゼロからリニューアル

Slide 40

Slide 40 text

• しかし… • 担当プロダクトオーナーが不在→リニューアル後の仕様が決まらな い・決められない • PO がいても「リニューアルするする詐欺」状態で、いつまでたって もリニューアルの話は進まない • リニューアルできるならとっくにリニューアルしてるのだわ… ③ リニューアル Legacy App をゼロからリニューアル

Slide 41

Slide 41 text

移植方針まとめ 仕様 UI ロジック おすすめ度 ロジックKeep移植 そのまま そのまま そのまま △ 仕様Keep移植 そのまま そのまま 変更 ◯ リニューアル 変更 変更 変更 ◯ 多くの場合で、コードを読み解いて 再実装したほうが結果的に速そう 仕様調整の必要がないので、開発者 のみでサクサク進められるのが◯ リニューアルできるのであれば、 さっさとリニューアルするのが吉!

Slide 42

Slide 42 text

Legacy App 移植戦略 • 仕様Keep 移植 • 仕様負債が散見されるものの、仕様変更はせず移植する • 動作確認も Legacy App ⇔ New App で動作比較をすればいいだけで簡単 • 仕様をKeep しつつも明らかに不要なコードベースは移植対象から除外 • どこからも参照されていない画面 • どこから呼ばれていない JavaScript コード • 表示上無意味な HTML/CSS コード

Slide 43

Slide 43 text

Legacy App 移植戦略 〜要望トリアージ編〜 • とはいえ移植をやっていると欲が出てくる • PO 「この機能、実はバグってて… ついでに直せない?」 • 開発者「この複雑な仕様、どう考えても不要じゃね?」 • 出てきた要望は下記のような判断基準でトリアージ • 実装工数を減らすことに繋がる要望は Accept • 実装工数が嵩むことに繋がる要望は Reject

Slide 44

Slide 44 text

Legacy App 移植戦略 〜既存仕様理解編〜 • 基本的には泥臭くソースコードを追って既存仕様を紐解いていく • 画面をいろいろイジったり、ソースコードを改変してみたりしながら既存 仕様の理解を深めていくプロセス • しかし2024 年に生きる我々にはAI という強力な銀の弾がある • レガシーコードをGitHub Copilot に放り投げて解説してもらうだけでも コードリーディングの速度・解像度がグンと上がる • ※ オレオレFW による独自コード・ロジック内に無駄な処理が多かったた め、Copilot による単純なコード変換はあまり使えなかった

Slide 45

Slide 45 text

Let’s 実装!

Slide 46

Slide 46 text

Input/Output で 考える実装方針 04

Slide 47

Slide 47 text

実装方針 • 仕様Keep 移植においては Input/Output が一致 していればOK • Input: リクエストパス, リクエストパラメータ • Output: View (HTML/Text), UI, データ in/out に着目して 仕様を徐々にビルドアップしていく App Input Output

Slide 48

Slide 48 text

実装 Step1 • まずを最小限の正常系をガッチリと固める • [in] リクエストパス ► [out] 静的HTML • 正常系のE2E テストを書いて土台とする • この時点ではDB インタラクションはなく、ダ ミーデータの出力でOK App Request Static HTML In Out

Slide 49

Slide 49 text

実装 Step2 • 次にDB との接合部をロジックに徐々に加えていく • [in] リクエストパス ► [out] 動的HTML • この過程で“ 外せない” 仕様はきちんとテストに 反映させていく • TIPS: バカ正直にロジック読むよりは、DB に流 れるクエリログからロジックをサルベージ・再構 築したほうが速かった App Request Dynamic HTML In Out

Slide 50

Slide 50 text

実装 Step3 • 仕上げに異常系をカバーしていく • [in] リクエストwith 不正なID ► [out]404 ページ • [in] リクエストwith 不正な入力値 ► [out] バリデーションエラー App Invalid Request Error In Out

Slide 51

Slide 51 text

フィーチャーフラグを 用いたスムーズな移植 05

Slide 52

Slide 52 text

フィーチャーフラグを利用して移植 • Legacy App に /feature という移植対象パスがあった場合、下記の ように Reverse Proxy を設定 • /feature → Legacy App • /feature2 → New App • /feature2 のパスは移植用フィーチャーフラグが ON の場合に表示 • リリース時は New App の /feature2 を /feature にして Reverse Proxy を切り替え

Slide 53

Slide 53 text

フィーチャーフラグを利用して移植(開発時) Legacy App New App Reverse Proxy /feature /feature2 - /feature/xxx - /feature/yyy - /feature/zzz - /feature2/xxx - /feature2/yyy - /feature2/zzz ※フィーチャー フラグでアクセス制御

Slide 54

Slide 54 text

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 の 設定切り替え フィーチャーフラグを利用して移植(リリース時)

Slide 55

Slide 55 text

Legacy App New App Reverse Proxy - /feature/xxx - /feature/yyy - /feature/zzz ※フィーチャー フラグでアクセス制御 /feature ✕ nginx.conf で 切り戻し フィーチャーフラグを利用して移植(ロールバック時) /feature - /feature/xxx - /feature/yyy - /feature/zzz 元通り

Slide 56

Slide 56 text

テスト • テスト担当を決めてテスト実施 • PO がいればPO 、いなければ移植対象に関連する運用チームであった りカスタマーサポートチームにテストを依頼 • あるべき仕様がロストしていたとしても、Legacy App ⇔ New App の 差分を確認することで仕様が保たれているかはテスト可能 • TIPS: 自動テスト項目をrspec --format documentation でド キュメント出力し、テスターに渡しておくと、仕様の理解促進やテスト 実施項目の削減にもつながって◯

Slide 57

Slide 57 text

実装→テスト→リリースを繰り返すことN 回…

Slide 58

Slide 58 text

移植DONE!!!

Slide 59

Slide 59 text

RESULT? 〜改善した?〜

Slide 60

Slide 60 text

機能Aの場合

Slide 61

Slide 61 text

機能Aの場合 レスポンス速度が 4〜8倍向上

Slide 62

Slide 62 text

機能Bの場合

Slide 63

Slide 63 text

機能Bの場合 レスポンス速度が 10倍向上

Slide 64

Slide 64 text

やったね!

Slide 65

Slide 65 text

スーパー高速化達成の内実 • 普通に実装しただけ(高速化のためのトリッキーな実装をしたわけで はない) • 無駄なクエリ・N+1 クエリを排除、効率的なクエリを心がける • 各処理において無駄な処理を実行しない • 移植対象のレスポンス速度は、New App で動いている他の機能のレス ポンス速度と比べると同水準のレスポンス速度 • Legacy App が単に遅すぎただけ説

Slide 66

Slide 66 text

Legacy App のコードベースを大幅に弱体化成功

Slide 67

Slide 67 text

Legacy App のコードベースを大幅に弱体化成功 100万行の コードベース削除

Slide 68

Slide 68 text

移植後の機能開発 • コードベースがメンテナンス可能になり、開発チームに引き継ぐこと ができた • 開発者: 社内標準技術でコードを読み書きできるようになった • PO: バグ修正依頼・機能要望を出すことができるようになった • 引き継いだことにより、エンハンス開発も再開された • 早速、移植時点ではレスポンシブデザイン非対応だったページが、 レスポンシブ対応されるという改善が入った

Slide 69

Slide 69 text

HAPPY END? 〜めでたし めでたし?〜

Slide 70

Slide 70 text

NO!!! 残念でした!

Slide 71

Slide 71 text

残課題 • Legacy App に依然として残り続けるPHP 管理画面… • Legacy App のデータベースは今も元気に稼働中! • Go で作られたマイクロサービスはどうする?

Slide 72

Slide 72 text

残課題 • Legacy App に依然として残り続けるPHP 管理画面… • Legacy App のデータベースは今も元気に稼働中! • Go で作られたマイクロサービスはどうする? 俺達の戦いは これからだ!

Slide 73

Slide 73 text

FUTURE 〜今後どうする?〜

Slide 74

Slide 74 text

今後 • 基本方針としてはモノリス化を目指す(モノリシックRails 戦略) • Modular Monolith にするかどうかは検討課題 • 本発表で紹介した移植により、Legacy App は管理画面だけとなり、 ユーザー影響を局所化できたので PHP Upgrade する方針で考えてい る • 最終的には上述のモノリスに統合させる方向で進めたい(時期未 定)

Slide 75

Slide 75 text

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 アーキテクチャの今後

Slide 76

Slide 76 text

本発表の教訓 • <技術的負債返済プロジェクト>が必要になるまで技術的負債を放置 しない! • 技術的負債自体は健全な開発チームにあって当然 • 恒常的な負債返済サイクルが回っていないことが問題 • きちんと上層部に技術的負債の返済を理解してもらい、定常的な負債 返済活動を推進しよう • <エンジニア vs ビジネス> みたいな対立構図にしないことが大事

Slide 77

Slide 77 text

THANK YOU! ご清聴ありがとうございました。

Slide 78

Slide 78 text

その他のTIPS • 非エンジニアにも技術的負債について理解してもらうために、「技術 的負債とは何か」について話す機会をもらって、話をできたのは良 かった • 移植時の実装方針について議論が割れたときのために、実装方針の最 終意思決定者を私一人に決めたのは不毛な議論が減って良かった • Code Review が遅い問題があったが、このへんの知見は別発表にて まとめました: Faster Pull Request Reviews 〜ハイパフォーマンスチー ムへの道〜

Slide 79

Slide 79 text

その他のTIPS • 旧・移植プロジェクトは業務委託メインの開発だったが、新・移植プロ ジェクトは、正社員が中心となりチームを組成した • これにより進捗管理・品質の担保・仕様調整の面でプラスに働いた • あるコードが本当に使われていないかを確認するために GitHub の Org 横断検索を使うのが良かった • クエリ: org:{your-org} NOT is:archived {unused_code} • ※ ただし巨大なCSV ファイルなどは検索対象に入らなかったりするの で注意

Slide 80

Slide 80 text

参考資料 • メドピア公式ブログ・資料 • Golang (Go 言語)を採用して、たった二人で基盤となるAPI ゲートウェイを開発した話 (2015 年) • レガシーな独自フレームワークから脱却してRails へ徐々に移行している話 (2017 年) • メドピアの全力Rails 化の取り組み晒します! (2017 年) • ストラングラー フィグ パターン - Azure Architecture Center • 動作するきれいなコード: SeleniumConf Tokyo 2019 基調講演文字起こし+! - t-wada のブロ グ • カミナシでの技術的負債返済プロジェクトとその決断