この記事はMakuake Advent Calendar 2023の24日目の記事です。
概要
10年以上運用しているサービスのPHPとFuelPHPのアップデートを行ったプロジェクトのレポーティングをする。
- アップデート対象
- メインサービス(会社が運用する主要なサービス)のアプリケーション
- FuelPHP利用
- メインサービスが利用している社内ライブラリ(2つ)
- PHP製
- メインサービスの実行環境
- ECSによるコンテナ環境
- PHP-FPM利用
- メインサービス(会社が運用する主要なサービス)のアプリケーション
- アップデート対象バージョン
- PHP7.3からPHP8.1へ
- FuelPHP1.8.2からFuelPHP1.9-developへ
- チーム体制
- 自社エンジニア2名
- プロジェクト進行・調査・実装担当
- 業務委託エンジニア1名
- 調査・実装担当
- 社内リソース調整の都合上、実装作業の主担当は業務委託のエンジニアに依頼
- 調査・実装担当
- 自社エンジニア2名
- 期間
- 2022年5月から2023年4月まで
- 結果的には1ヶ月の遅延をもって2023年4月に完了した
- 遅延理由は、社内エンジニアメンバーがそれぞれのチームに所属しながら並行でアップデートプロジェクトに関わるという負荷の高い体制であったことと、アップデートプロジェクトのためのインフラ構成の検証に時間がかかったことが主な理由であると考えている
- 結果的には1ヶ月の遅延をもって2023年4月に完了した
- 2022年5月から2023年4月まで
背景
PHP7.3は2021年12月にEOLを迎え、セキュリティアップデートが終了しており、アップデートを検討する必要があった。PHPに依存するFuelPHP1.8.2も同様の状況であった。
今回のプロジェクトでは単純にアップデートをすること自体が最適化どうかを検討する必要があった。以前のアップデートプロジェクト時とは異なり、FuelPHPの開発状況が停滞しており、FuelPHPを今後も利用し続けていくことが技術戦略上適切か検討しなくてはならなかった。
※FuelPHPの状況については以前、FuelPHPの2023年3月現在の現況にまとめたのでそちらを参照。
プランニング
事前調査
プロジェクト開始前の事前調査として、以下のような調査を行った。
- PHP7.3からPHP8.1へのマイグレーション情報をwww.php.netを参照してキャッチアップ
- コードベースのPHP互換性調査
- RectorやPHPCompatibilityといったツールを活用したり、ドキュメントの内容からgrepで検索するなどしてPHP7.3からPHP8.1へのアップデートに向けた互換性調査を行った
- 調査項目はPHPマニュアル > 付録に基づいた
- 新機能/新しいクラスとインターフェイス/新しく追加された関数/新しいグローバル定数/下位互換性のない変更点/PHP 8.3.x で推奨されなくなる機能/その他の変更/Windows のサポート
- 調査項目はPHPマニュアル > 付録に基づいた
- RectorやPHPCompatibilityといったツールを活用したり、ドキュメントの内容からgrepで検索するなどしてPHP7.3からPHP8.1へのアップデートに向けた互換性調査を行った
- 実行時エラーの調査
- PHP8.2実行環境を用意し、テストを実行したり、アプリケーションの動作を手動確認するなどして実行時のエラーを洗い出した
- 依存しているパッケージのPHP8.1対応状況の調査
- 既に対応しているもの、アップデートが必要なもの、アップデートできないため何らかの対応が必要なものを洗い出した
- CI・CD関連での調整が必要な箇所の洗い出し
- 実行環境にインストールされている拡張モジュールの確認とアップデート・追加が必要になりそうなものの洗い出し
調査事項に基づいて修正が必要な箇所を洗い出し、プロジェクト全体にかかる工数の見積もりを行った。
モノリシックなアプリケーションのアーキテクチャ戦略
メインサービスで利用されているアプリケーションはいわゆるモノリスであり、複数の開発チームが開発を行っている。
開発組織の戦略としてこのモノリスをどのように刷新していくか?ということがここ数年大きな課題となっている。
メインサービスで使っているPHPやFuelPHPといった技術スタックやそのアーキテクチャは今後どのように刷新されていくか?ということも含めて、アップデートの戦略を定めた。
今回のアップデートの戦略として次のような複数のプランを検討した。
-
- 別フレームワークへのリプレース
- FuelPHPからの脱却し、別のフレームワークへ書き換える
-
- 別言語へのリプレース
- FuelPHPとPHPからの脱却をし、別言語への書き換えを行う。フレームワークの利用も検討。
-
- サービス分割
- モノリスを維持しつつも、一部を別のサービスとして切り出す
- FuelPHPやPHPのアップデートは当面行わず、モノリスを最小化した先で次手を検討する
-
- モジュラーモノリスへのアーキテクチャ変更
- モノリスを維持しつつも、モジュール分割を行う
- サービス分割が前提にあり、モジュール分割後は別サービスへの切り出しを検討する
-
- FuelPHPを継続して利用
- PHPやFuelPHPのアップデート対応を行う
これらはそれぞれメリット・デメリットがあり(詳細は割愛)、比較検討した結果、「FuelPHPを継続して利用」のプランを選択することとした。
選択した理由としては次のようなものがある。
- EOLを迎えたものを長く放置してしまうことによるセキュリティリスクやコンプライアンス面のリスク回避
- モノリスの分割を進めてモノリスが解体できるわけではない(モノリスに留まるコードがある)ことが予期され、いずれにせよFuelPHPをどうするか再度検討する生じるためアップデートを先延ばしにする合理性がない
- 可能な限り最小の社内リソースでアップデートを完了させたい
- モノリスを大きく書き換えたり、分割したりすることに負荷を掛けられない
PHPとFuelPHPのアップデート対象バージョンの選定
FuelPHPを継続利用していくことを決定したため、FuelPHP及びPHPをどのバージョンまでにアップデートするかを調査に基づいて決定した。
プロジェクト計画段階では、PHP7.3からのアップデートバージョンの候補としては、PHP7.4、PHP8.0、PHP8.1があった。(PHP8.2はまだリリースされていなかった。)
PHP7.4は2022年11月にEOLを迎えるため、プロジェクト期間中にEOLとなるため対象外とした。
PHP8.0は2023年11月がEOLであるが、プロジェクト完了後から短命でEOLを迎える見込みになるため同様に対象外とした。
FuelPHP1.8.2は公式のリリース情報によるとPHP7.3までのサポートとなっているため、FuelPHPもPHP8.1をサポートしているバージョンにアップデートする必要があった。しかし、利用していたPHP1.8.2が最新であり、次期バージョンがリリースされていない状況であった。
そこで2つほどプランを検討した。
- FuelPHPをforkしてPHP8.1対応を行う
- FuelPHP1.9-developを利用する
検討の結果、下記の理由からFuelPHP1.9-developを利用することとした。
- コードベースの調査やFuelPHPの開発者への問い合わせなどにより1.9-developはPHP8.1で動作する可能性があった
- リリーススケジュールは未定だが、コミットが不定期で積まれており、開発者の意向も考慮すると今後正式にリリースされる可能性が0ではないように思えた
FuelPHP1.9-developはまだ正式なリリースがされていないが、調査してみると採用する余地があると判断できた。
FuelPHPをforkするよりも1.9-developを利用したほうが開発コストが低く抑えられるとも考えた。
ただし、「フレームワーク本体のテストカバレッジがかなり低いこと」や「リリースされても近い将来リリース予定のPHP8.2対応は期待できない可能性が高い」などといったリスクやデメリットはある。
別のアプローチとして、FuelPHPのコミッターになるというパワープレイも検討したが、FuelPHPのリリースサイクルを早めることにどこまで貢献できるか不確実性が高く、未知数であったため断念した。
# アップデート戦略
方針
アップデートプロジェクトの改修方針して、次のような方針を定めた。
- ビックバンリリースを避ける
- コードフリーズ期間を最小限にする
- 問題が発生したときの切り戻しを手早く行う
- アップデートプロジェクト期間中、開発フローの手順が複雑化させたり、実装上の手間を増やすことを極力避ける
アップデート戦略
方針に基づく上では、PHP7.3からPHP8.1へのアーキテクチャ変更を段階的に行うことが望ましいと考えた。
そのためにはPHP7.3とPHP8.1の両方の環境を並行して運用できる構成を構築する必要があった。
そのような構成の実現のため、5段階のフェーズを設け、段階的なアーキテクチャ変更を達成できるように計画した。
Phase1:プロダクション環境もステージング環境もPHP7.3で運用されている状態
ステージング環境においてPHP7.3とPHP8.1を並行稼働環境を開始できる状態をつくるための準備期間としてのフェーズ。
このフェーズでは次のようなことを行った。
- PHP7.3でもPHP8.1でも動作するようにアプリケーションのコードベースやライブラリのアップデート作業を実施
Phase2:ステージング環境における並行稼働環境の運用開始
ステージング環境のみPHP7.3とPHP8.1の並行稼働環境として運用を開始するフェーズ。
QAの実施や負荷試験のテストを実施し、プロダクション環境を構築する前段階の検証を行った。
特に並行稼働のインフラ構成の仕組みを検証し、プロダクションでの運用で問題が発生しないかを重点的に検証した。
Phase2.5:プロダクション環境における並行稼働環境の運用開始
ステージング環境における並行稼働環境をプロダクション環境にも同様に展開し、運用を開始するフェーズ。
監視や運用開始で生じたバグ対応などを行い、プロダクション環境での運用を安定させ、PHP8.1への完全切り替えができる状態を目指すことを目的としている。
Phase3:ステージング・プロダクションを環境をPHP8.1へ切り替え開始
PHP7.3とPHP8.1の並行稼働状態であるステージング・プロダクション環境をPHP8.1のみ稼働状態に切り替え、運用が安定するか検証していくフェーズ。
Phase2.5の段階である程度は安定していることを予測しているが、PHP8.1のみの運用環境となることでトラフィック量が増えるため、慎重を期してこのフェーズを設けた。
このフェーズでは、PHP7.3環境関連のインフラリソースを残存させておくことで、PHP7.3環境への切り戻しを行うことができるようにしておいた。
Phase3.5:PHP8.1環境への完全切り替え
PHP7.3環境関連の各種インフラリソースや、並行稼働のために残存させていたPHPバージョン分岐のコード等を削除し、PHP8.1環境への完全切り替えを行うフェーズ。
このフェーズでは、PHP7.3環境への切り戻しは基本的に不可能となる。(やろうと思えばできるが、手早い切り戻しはできない。)
実装
改修方針
依存パッケージを除くアプリケーションのソースコードは大きく分けて2つのパターンがあった。
- PHP7.3とPHP8.1の両方のバージョンで正常に動作するように改修できるもの
- PHP7.3とPHP8.1の両方のバージョンで正常に動作するように改修できないもの
前者は単純に改修するだけで良いが、後者はPHPのバージョンで条件分岐を行い、それぞれのバージョンで正常に動作するように改修を行う必要があった。
このような条件分岐をヘルパー関数として定義し、一種のフィーチャートグルのような形で各改修箇所にて利用した。
一方で依存パッケージについては3つのパターンがあった。
- PHP7.3とPHP8.1の両方のバージョンで正常に動作するバージョンにアップデートできるもの
- PHP7.3とPHP8.1の両方のバージョンで正常に動作するバージョンにアップデートできないもの
- PHP7.3とPHP8.1の両方のバージョンで正常に動作するバージョンにアップデートできず、forkしてPHP8.1対応が必要なもの
1つ目のパターンは単純にアップデートするだけで良く、それ以外のパターンはそれぞれの対応が必要となった。
2つ目のパターンはPHP7.3とPHP8.1のそれぞれの環境向けにcomposer.jsonのファイルを用意することで対応した。
この対応により、Phase3.0まではそれぞれのcomposer.jsonファイルには同じ依存パッケージを指定する必要が生じてしまうが、ライブラリ追加は頻繁に発生しなかったため、大きな手間とはならなかった。
3つ目のパターンは2件該当するケースがあった。
1つはruflin/ElasticaというPHPのElasticsearchクライアントライブラリのfork対応であった。
サービスが利用しているElasticsearchのバージョンがかなり古く、PHP8.1対応バージョンのruflin/Elasticaを利用することができなかった。
そのため、ruflin/Elasticaをforkし、PHP8.1対応を行うことで対応した。(アップデートプロジェクト完了から半年頃、Elasticsearchを使っている一部機能がElasticsearchのクライアントライブラリを必要としなくなったため、forkしたリポジトリはお役御免となった。)
もう1つは社内ライブラリのfork対応であった。
社内ライブラリはメインサービスのアプリケーションとはPHP7.3で運用されている別の社内サービスでも利用されていたため、PHP7.3とPHP8.1でそれぞれ動作するように社内ライブラリを運用する必要があった。
社内ライブラリではcomposer.jsonファイルをPHPのバージョン別に分けて、PHPのバージョンで条件分岐するように処理を振り分けるような改修をすることができればforkする必要はなかったが、良いアプローチが思いつかずforkする対応となった。
この対応により、Phase3.0まではfork元とfork先で同じ仕様が保たれるように同期する気をつける手間が生じてしまったが、頻繁に仕様変更が入るようなライブラリではなかったため、あまり大きな手間とはならなかった。
アップデートプロジェクトの完了後は、fork元とfork先はそれぞれ別のものとして運用していくことがサービスの仕様上許容できたため、同期対応はPhase3.5以降は不要となった。
インフラ構築
PHP7.3とPHP8.1の並行稼働環境を構築するにあたり、既存の実行環境に手を加える必要があった。
既存の実行環境はALB+ECSで構成されており、WebサーバーとしてNginxを利用している。
この既存の実行環境を次の要件を満たすように改修した。
- PHP7.3とPHP8.1環境の切り替えを手早く行えること
- PHP7.3とPHP8.1環境でトラフィックを柔軟に振り分けられること
- PHP7.3とPHP8.1環境それぞれログを分けて収集できること(可観測性の担保ができること)
改修のアプローチとして、
- Nginxで振り分け行う方法
- Route53で振り分けを行う方法
- NLBで振り分けを行う方法
などいくつか方法を検討したが、最終的にはCloudFrontのContinuous Deploymentという当時リリースされて間もなかった機能を利用することにした。
cf. Using CloudFront continuous deployment to safely test CDN configuration changes
この機能はCloudFrontのDistributionをPrimaryとStagingの2つに分けてトラフィックを分散することができるという点で要件に合致していた。
トラフィックの分散条件はWeight-basedという重み付けを行う方法を採用した。
この方法は振り分け先に対して全リクエストの0~15%までしか割り振ることができないという制約があるが、最大15%の状態で一定期間トラフィックを受け付けることで切り替え判断に必要な十分なトラフィックが集まると判断した。
最終的な構成は次のようになった。
テスト
QA
PHP7.3とPHP8.1の並行稼働環境を運用する都合上、メインブランチをPHP7.3とPHP8.1の両方で実行するようにコード改修を行ったため、PHP7.3とPHP8.1の両方の実行環境においてQAを実施する必要が生じた。
QAの実施にあたっては、サービス全体の網羅的なテストケースを用意し、UI上でそれを実施していく形を取った。
負荷試験
PHP8.1へのアップデートにより性能劣化がないことを検証するため、k6を使った負荷試験を実施した。
想定するトラフィックのパターンごとにテストを実施したところ、レスポンスタイムはおおよそ25%程度の改善が見られた。
結果
計画的に実施したおかげか大きな問題が起きることもなく(解決が困難な問題は一部あった)、アップデートプロジェクトを完遂することができた。
所感
このプロジェクトは当初自分含めて3名のエンジニアで構成されたチームで走り出したのだが、うち2名とも育休で各々別々のタイミング途中離脱し、他メンバーへの引き継ぎを行うというリレーのようなプロジェクトであった。
そのような体制であってもドキュメンテーションや社内周知等の徹底により、プロジェクトの遂行への影響は最小限に抑えることができたように思う。
複数のチームが触るモノリスのアプリケーションのアップデートは関係者が多く、コミュニケーションコストが高くなりがちだと思うが、適切に計画することで難なくアップデートができることを実感した。
一方で今後の課題と感じる部分も少なくなかった。
テストカバレッジの不十分さ、デッドコードの多さ、古いまま更新されない依存ライブラリ、QA実施の効率性など、アップデートプロジェクトを通して今後日頃の改善が必要だと感じる部分がいくつかあった。
自分は現職では2回目となるアップデートプロジェクトであったが、前回とは組織の構成やアーキテクチャの構成、アプリケーションの状態も異なり、気を付けることが一層増えたように感じた。(これも課題である・・)
次回のアップデートはどうなるか(forked FuelPHPを考えるのか、別の戦略を取るか・・)、FuelPHPの未来はどうなるのか(次のリリースはあるか・・)、不透明感が拭えないが、今回のアップデートプロジェクトの知見を次回にも活かせるよう努めたい。