Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

PHPをバージョンアップするための技術と戦略

サーバーサイドエンジニアの田実です!

2023年3月に開催された PHPerKaigi 2023 のパンフレット記事に PHPをバージョンアップするための技術と戦略 というタイトルで寄稿させていただきました*1。技術要素に依らず汎用的な内容で、パンフレット記事だけにしておくのも何だかもったいない気がしたので、ブログ記事にも同じ内容で紹介させていただきます!*2



PHP 7.4が2022年11月末でEOLになり、7系がレガシーと言われるような時代になりました。PHPは毎年古いバージョンがEOL(End Of Life)になるため、PHPのバージョンアップはアプリケーション運用の重要な課題です。

本稿ではPHPをバージョンアップするために必要な技術や戦略を、コードの削除・静的解析から監視・リリースまで幅広く紹介します。EOLではないモダンなPHPアプリケーションがより多く運用されるきっかけになれば幸いです。

なぜバージョンアップするのか

バージョンアップをするための技術を説明する前に、ソフトウェアをバージョンアップする意義やメリットについて紹介します。

はじめに、バージョンアップをすることでソフトウェアとして健全な状態を保つことができます。EOLになったOSSは基本的には保守されませんので、不具合によって開発生産性が低下したり、セキュリティリスクが大きい状態となります。また、リファレンスも保守されないため、古いバージョンを使っているとリファレンスの参照性が悪くなります。一方で、バージョンアップをするとリファレンスの参照性に加えて、より便利な機能が使えるようになり開発者体験や生産性が向上します。PHPなどのミドルウェアにおいては、バージョンアップによって処理系のパフォーマンス向上が見込めるケースもあり、エンドユーザのユーザー体験やコスト面におけるメリットも少なくありません。

また、バージョンアップは変化に強いソフトウェアを作るきっかけになります。バージョンアップの影響範囲はバージョンアップ対象を利用している処理の全てになります。例えばPHPのバージョンアップであればPHPを使っている全てのコードが影響範囲になり、安全にバージョンアップするにはテストコードや静的解析などの回帰テストを全てのコードに対して行う必要があります。バージョンアップは振る舞いを変えずに内部構造を改善するリファクタリングの一種で、変化に強いソフトウェアを作るための試金石とも言えます。変化に強いソフトウェアは開発の質とスピードが高く、ビジネスの競争力としても重要です。

他にも、技術投資による採用効果を期待できたり、最新のソフトウェアを使うことでそのソフトウェアやコミュニティの活性化にも繋がるかもしれません。

このようにバージョンアップをするメリットは多岐にわたり、チームやプロダクトの状況によって違いはあれど、多くのチームにとって優先度の高いタスクの1つではないかと思います。次節から実際にバージョンアップを行うためにやるべき作業について紹介したいと思います。

不要なコードの削除

使っていないコードを保守するのは意味がありません。手動テスト・自動テストや静的解析でチェックをしたものの、実はそのコードは未使用で対応しなくても良かった、ということはよくあります。認知負荷を下げる意味でも、不要になったコードを削除する習慣を個人やチームで意識することは重要です。

削除対象の抽出は機能のクローズなどビジネス駆動で進める方法や、アクセスログなどで機械的に抽出する方法を用います。ビジネス駆動で進める場合は削除対象の機能がわかっているのでトップダウンでコードを削除することができます。利用しているエンドポイントのコントローラーを削除し、そのコントローラーに依存するすべてのモジュールを削除する、といった形で芋づる式で削除していきます。機能クローズ時はエンドポイントへのアクセスの有無をログで確認したり、対象のテーブルが利用されていないことをDBで確認した上でクローズすることになります。

運用期間が長いアプリケーションでは、どの機能や分岐が使われているのかが把握しきれないケースがあります。その場合は、アクセスログとルーティング一覧を突き合わせて、利用されていないエンドポイントを確認・削除する方法が有効です。アクセスログ以外にはOPcacheにキャッシュされているファイルを確認することで、ファイル単位での利用有無を判別することも可能です。このように利用していないコードを機械的にボトムアップで確認していく方法も併せて利用すると良いです。

<?php

// TODO: 本番ではなにかしらの認証機構を入れてください
header("Content-Type: text/csv");

$status = opcache_get_status();
foreach ($status['scripts'] as $s) {
  echo $s['full_path'] . "\t" . $s['hits'] . PHP_EOL;
}

OPcacheの利用状況を確認するAPI例

自動テストの導入と拡充

自動テストを導入することで、新旧バージョンの互換性で壊れてしまうコードを検知できます。後述の静的解析でもある程度は検知できるのですが、値・振る舞いが変わるものに関しては自動テストの方が確実に検知できます。回帰テストとしては、単体テストよりもエンドポイントやバッチ処理に対して一気通貫に行う結合テスト(Featureテスト)を意識すると良いです。結合テストはテストを書くコストや実行コストが高くなりがちですが、正常系だけでも書いておくと比較的安心してバージョンアップに臨めます。結合テストではモックを極力使わないようにすると、DBアクセスなども含めて回帰テストができるのでより安心です。

とはいえ、テストコードを増やしてカバレッジを確保するのは現実的には難しいことがほとんどです。また、カバレッジが十分あるからといって不具合が発生しないわけではなく、自動テストではテストしづらいテストケースがあったり、バージョンアップ時だけ一時的に発生するような不具合も発生する可能性があります。そういう意味でも、基本的には自動テストはベースとして考えて進めると良いと思います。

静的解析の導入

理想的には前述の自動テストを拡充してバージョンアップ時の動作保証をしつつ進めたいのですが、現実的にはテストが少なかったりテストができる仕組みが整っていない、といったケースがあると思います。そういった場合、まずは静的解析を導入するのがオススメです。静的解析はテストを書かずともPHPとして最低限の動作保証や型レベルの検証を行ってくれるため、バージョンアップにおける強力なツールとして活用できます。未使用変数や未定義変数、引数の間違いなど既存の不具合も簡単に洗い出せます。OSSとしては PHPStanPsalm が有名で、PHPerKaigiやPHP Conferenceのトークでよく話題に上がる静的解析ツールです。筆者はPHPStanをよく使っていますが、PHPStanはDockerでも動かせるので対象コードのバージョンに依らず手軽に導入できます。PHPStanに関する記事も多く公開されており拡張機能も充実しているので、静的解析を利用したことがない方にはオススメです。

$ docker run --rm -v /path/to/app:/app \
     ghcr.io/phpstan/phpstan analyse /app/src

Dockerを使ってPHPStanを動かす例

最初はエラーが大量に出ると思いますが、明らかな不具合や修正しやすいものを中心に対応しつつ、優先度が低いものは一時的にエラーを無視して進めると良いです。PHPStanの場合は Baseline の機能を使うことで一時的にエラーを制御するような設定ファイルを自動生成してくれます。この自動生成された設定ファイルを使うことで、チーム内で分担して少しずつ改善することができます。

$ vendor/bin/phpstan analyse src --generate-baseline

baselineとなる設定ファイルを作成するコマンド例

依存ライブラリのバージョンアップ

PHPをバージョンアップするには、依存ライブラリも最低限そのPHPで動くバージョンまで上げる必要があります。PHPと同時に依存ライブラリもバージョンアップすると、影響範囲が大きくなり不具合の予測がしづらくなります。先に各ライブラリをバージョンアップ前のPHPに対応する最新バージョンに上げるなど、手順を工夫することで影響範囲やリスクを抑えることができます。

依存ライブラリのバージョンアップでは各OSSのCHANGELOGやUPGRADEガイドで互換性の無い変更を優先して確認します。各ガイドにしたがって変更を行い、そのライブラリを利用しているモジュールが正常に動くかを自動テストや静的解析でチェックします。HTTPクライアントや外部APIのクライアントなどは自動テストで担保しきれないため、手動テストも必要です。手動テストの際は手順や観点をテキストで簡単にまとめておくと、次回以降のバージョンアップが楽になります。

ログやメトリクスの監視

前述の不要コードの削除やライブラリのバージョンアップでは、リリース後のログやメトリクスの監視が重要です。自動テストや静的解析がどんなに充実していても、それらでカバーしきれない予期せぬ不具合・性能劣化は発生するものです。そのため不具合が発生する前提で、それらを適切に素早く検知できるような仕組みが必要です。

ログ監視やメトリクス監視はAmazon CloudWatch、Datadog、New Relicなどのプラットフォームや、Jenkins、Re:dashなどのOSSが利用できます。適切なログ監視を行うためには、適切なロギングも必要です。例えば、発生した事象と取るべき対応、影響範囲をログメッセージに書いたり、ログレベルやログを出す場所についてチームで認識を合わせていく必要があります。アラート通知も必要な通知を精査しないと、オオカミ少年化して重要な通知が埋もれてしまい、検知が遅れたり見逃してしまう可能性があります。ロギングや監視は一朝一夕で解決する課題ではなく、チームで意識して少しずつ改善していくのが大切です。

リリース直後のAPMのメトリクス変化の例

リリース

ログやメトリクスの監視と併せて、リリース時の切り戻しについても検討する必要があります。監視によって不具合を検知できたとしても、迅速に切り戻しができないと傷口が広がってしまうからです。ライブラリのバージョンアップであれば単純にリバートして再リリースすれば良いかもしれませんが、PHPなどのミドルウェアに関してはアーキテクチャによって取れる方法が変わります。

例えば、本番アプリケーションがDockerコンテナで管理されている場合はバージョンアップ=Dockerfileの修正になるので、基本的にはコードのリバート及び再デプロイで切り戻しが可能です。しかしながら、EC2などのインスタンスがベースになっている場合はコードのリバートだけでは対応できません。例えばphp-fpmをAnsibleなどで管理している場合は、旧バージョンと新バージョンを両方動かしておいて、旧バージョンのphp-fpmを復帰させることで切り戻しができます。バージョンアップ後のインスタンスをまるごと作り直す場合は新旧インスタンスをロードバランサに登録・切り離しすることでリリースや切り戻しができます。また、実際に不具合が発生すると、焦って正常に状況を判断できなくなることがあります。切り戻し手順をあらかじめ整理するなど、精神的に切り戻しがしやすくする工夫も重要です。
カナリアリリースなどの段階を踏むのも有効です。1台だけ先行して新しいバージョンに上げて影響範囲を小さくすると、切り戻しがうまくいかなかったとしても傷口を抑えることができます。各エンジニアの開発環境を先行してバージョンアップして、検証期間を長めに取る方法もあります。新バージョンでのみ動くコードを書いた場合など、開発・本番環境間の差異によるリスクがあるものの、静的解析やテストコードで新旧両方動くような書き方を矯正したりすることでリスクを抑えることができます。

チームへの共有

バージョンアップはチーム戦でもあるので、少しでも変化に強いソフトウェアを作れるように、これらの知見をチーム内で積極的に共有していくと良いです。自動テストや静的解析で便利なモジュールを書いたり、ライブラリのアップグレード・大規模なリファクタリングをする場合は積極的にチームに共有・相談しましょう。自分の当たり前は他の人にとっての知見ですので、コミュニケーションの一環としてチームに知見や技術をどんどん貯めていきましょう。知見の共有はチーム全体にバフをかける重要なエンジニアリング能力の1つです。

また、共有は同じ内容であっても定期的に何回も行うと良いです。共有されたことは時間が経てば忘れますし、それが習慣や文化になるまでは長い時間を要します。何回も説明したり意識できるようにWikiやブログにドキュメンテーションするのも良い戦略です。

メンタル的TIPS

監視やリリース手順・切り戻し手順を十分準備して影響を最小限にしたとしても、やはり不具合を起こすと精神的に疲弊します。PHPをバージョンアップしなくても今の時点でアプリケーションは動いているので、ビジネス的に意味を見出しづらくなることもあります。新機能開発などと違ってテストコードの整備・不要コードの削除などのタスクはとても地味ですし、ユーザやビジネスサイドからの感謝を得られにくく、モチベーションの維持が難しいです。しかしながら、バージョンアップ自体はプロダクトやチームによってタイミングの違いはあれど、基本的には必ずやっていくべきことです。上述したようにバージョンアップによる直接的・間接的なメリットも多くあるので、1石2鳥3鳥の効果を狙っていくとモチベーションが維持しやすいです。また、バージョンアップはビジネス理解からアプリケーション実装・インフラ設計・リスク管理まで、アプリケーション実装の総合格闘技といっても過言ではありません。バージョンアップから得られる知見というのはとても大きく、人から聞いたり勉強するだけでは得られない貴重な経験です。ドメイン知識や技術課題をより深く理解するきっかけにもなります。やり遂げるのはとても大変ですが、ぜひバージョンアップという総合格闘技にチャレンジしていってほしいと思います。

まとめ

PHPをバージョンアップするための技術と戦略について紹介しました。今回紹介した技術はバージョンアップに対するものというよりは、変化に強いソフトウェアを作るための技術です。日々の開発との違いは、その影響範囲の大きさやタスクの切り出しの難易度の違いでしかありません。紹介した内容が一般的すぎて拍子抜けした方もいらっしゃるかもしれません。しかしながら、変化に強いソフトウェアを作るのに必要なのは何か特別な能力ではなく、普通の技術を普通に適用していくような日々の小さい改善の積み重ねをしていくことです。大手術にならないよう、問題を放置せず少しずつリスクを取って日々のケアをしていくことを個人としてもチームとしても意識する必要があります。

PHPerKaigiやPHP Conferenceではバージョンアップに関するトークが多く発表されています。今回紹介した方法以外にも様々なアプローチがありますので、PHPerKaigiや他のカンファレンスのトークを聞いて、技術を知り、技術を楽しみ、技術を活用していってほしいと思います。

*1:家庭の事情で今回は登壇ではなくパンフレット記事に寄稿させていただく形での参加となりました。こういった形で参加できるのは良いですね!

*2:ヤプリでもPHPやLaravelのバージョンアップを行っており、バージョンアップでやったことやハマりどころなどの具体例は別記事で共有しようと思います!