この記事はMakuake Advent Calendar 2023の24日目の記事です。
10年以上運用しているサービスのPHPとFuelPHPのアップデートを行ったプロジェクトのレポーティングをする。
PHP7.3は2021年12月にEOLを迎え、セキュリティアップデートが終了しており、アップデートを検討する必要があった。PHPに依存するFuelPHP1.8.2も同様の状況であった。
今回のプロジェクトでは単純にアップデートをすること自体が最適化どうかを検討する必要があった。以前のアップデートプロジェクト時とは異なり、FuelPHPの開発状況が停滞しており、FuelPHPを今後も利用し続けていくことが技術戦略上適切か検討しなくてはならなかった。
※FuelPHPの状況については以前、FuelPHPの2023年3月現在の現況にまとめたのでそちらを参照。
プロジェクト開始前の事前調査として、以下のような調査を行った。
調査事項に基づいて修正が必要な箇所を洗い出し、プロジェクト全体にかかる工数の見積もりを行った。
メインサービスで利用されているアプリケーションはいわゆるモノリスであり、複数の開発チームが開発を行っている。
開発組織の戦略としてこのモノリスをどのように刷新していくか?ということがここ数年大きな課題となっている。
メインサービスで使っているPHPやFuelPHPといった技術スタックやそのアーキテクチャは今後どのように刷新されていくか?ということも含めて、アップデートの戦略を定めた。
今回のアップデートの戦略として次のような複数のプランを検討した。
これらはそれぞれメリット・デメリットがあり(詳細は割愛)、比較検討した結果、「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つほどプランを検討した。
検討の結果、下記の理由からFuelPHP1.9-developを利用することとした。
FuelPHP1.9-developはまだ正式なリリースがされていないが、調査してみると採用する余地があると判断できた。
FuelPHPをforkするよりも1.9-developを利用したほうが開発コストが低く抑えられるとも考えた。
ただし、「フレームワーク本体のテストカバレッジがかなり低いこと」や「リリースされても近い将来リリース予定のPHP8.2対応は期待できない可能性が高い」などといったリスクやデメリットはある。
別のアプローチとして、FuelPHPのコミッターになるというパワープレイも検討したが、FuelPHPのリリースサイクルを早めることにどこまで貢献できるか不確実性が高く、未知数であったため断念した。
アップデートプロジェクトの改修方針して、次のような方針を定めた。
方針に基づく上では、PHP7.3からPHP8.1へのアーキテクチャ変更を段階的に行うことが望ましいと考えた。
そのためにはPHP7.3とPHP8.1の両方の環境を並行して運用できる構成を構築する必要があった。
そのような構成の実現のため、5段階のフェーズを設け、段階的なアーキテクチャ変更を達成できるように計画した。
ステージング環境においてPHP7.3とPHP8.1を並行稼働環境を開始できる状態をつくるための準備期間としてのフェーズ。
このフェーズでは次のようなことを行った。
ステージング環境のみPHP7.3とPHP8.1の並行稼働環境として運用を開始するフェーズ。
QAの実施や負荷試験のテストを実施し、プロダクション環境を構築する前段階の検証を行った。
特に並行稼働のインフラ構成の仕組みを検証し、プロダクションでの運用で問題が発生しないかを重点的に検証した。
ステージング環境における並行稼働環境をプロダクション環境にも同様に展開し、運用を開始するフェーズ。
監視や運用開始で生じたバグ対応などを行い、プロダクション環境での運用を安定させ、PHP8.1への完全切り替えができる状態を目指すことを目的としている。
PHP7.3とPHP8.1の並行稼働状態であるステージング・プロダクション環境をPHP8.1のみ稼働状態に切り替え、運用が安定するか検証していくフェーズ。
Phase2.5の段階である程度は安定していることを予測しているが、PHP8.1のみの運用環境となることでトラフィック量が増えるため、慎重を期してこのフェーズを設けた。
このフェーズでは、PHP7.3環境関連のインフラリソースを残存させておくことで、PHP7.3環境への切り戻しを行うことができるようにしておいた。
PHP7.3環境関連の各種インフラリソースや、並行稼働のために残存させていたPHPバージョン分岐のコード等を削除し、PHP8.1環境への完全切り替えを行うフェーズ。
このフェーズでは、PHP7.3環境への切り戻しは基本的に不可能となる。(やろうと思えばできるが、手早い切り戻しはできない。)
依存パッケージを除くアプリケーションのソースコードは大きく分けて2つのパターンがあった。
前者は単純に改修するだけで良いが、後者はPHPのバージョンで条件分岐を行い、それぞれのバージョンで正常に動作するように改修を行う必要があった。
// コード例
if (version_compare(PHP_VERSION, '7.4.0') < 0){
// 7.4.0未満のコード
}
if (version_compare(PHP_VERSION, '8.1.0') >= 0) {
// 8.1.0対応コード
}
このような条件分岐をヘルパー関数として定義し、一種のフィーチャートグルのような形で各改修箇所にて利用した。
一方で依存パッケージについては3つのパターンがあった。
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を利用している。
この既存の実行環境を次の要件を満たすように改修した。
改修のアプローチとして、
などいくつか方法を検討したが、最終的にはCloudFrontのContinuous Deploymentという当時リリースされて間もなかった機能を利用することにした。
cf. Using CloudFront continuous deployment to safely test CDN configuration changes
この機能はCloudFrontのDistributionをPrimaryとStagingの2つに分けてトラフィックを分散することができるという点で要件に合致していた。
トラフィックの分散条件はWeight-basedという重み付けを行う方法を採用した。
この方法は振り分け先に対して全リクエストの0~15%までしか割り振ることができないという制約があるが、最大15%の状態で一定期間トラフィックを受け付けることで切り替え判断に必要な十分なトラフィックが集まると判断した。
最終的な構成は次のようになった。
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の未来はどうなるのか(次のリリースはあるか・・)、不透明感が拭えないが、今回のアップデートプロジェクトの知見を次回にも活かせるよう努めたい。