概要
FuelPHP1.8.0→1.8.2、PHP5.6→PHP7.3へのバージョンアップ対応をした。 業務でアプリケーションのバージョンアップ対応を行ったので、取り組みをまとめておく。
スコープ
- FuelPHP1.8.0 → FuelPHP1.8.2
- PHP5.6 → PHP7.3
- 対象リポジトリ
- ユーザー側アプリケーション
- 管理側アプリケーション
- パッケージリポジトリ
※ミドルウェアのバージョンとかは割愛 ※OSはAmazon Linux(2ではない)
FuelPHP1.8.0はPHP7.2まで対応しているが、1.8.2は7.3まで対応している。
バージョンアップに取り掛かる2週間くらい前に突如リリースされた。 fuelphp.com - Fuel releases 1.8.2
PHP7.2はアクティブサポートが2019年11月30日、セキュリティサポートが2020年11月30日となっており、 PHP7.3は2020年12月6日、2021年12月6日となっている。
サポートの期間が伸びるためFuelPHP1.8.2がリリースされたのは大変感謝すべき一件だったと思う。
期間
約1ヶ月半
最初の2週間は合宿という形でオフィスから離れた場所で集中的に作業に取り組んだ。同期間中は、コードフリーズ期間として、緊急対応以外のリリースを原則停止した。
チーム構成
- エンジニア6名
なぜバージョンアップしたのか
パフォーマンス要件というよりもセキュリティ対応の意図が大きかった。
プロダクトのセキュリティを担保できないというのは事業にとってのリスクになり得るだろう。
作業
事前準備
- FuelPHP、PHPのバージョンアップ情報(変更点)をキャッチアップ。diff読んだりした。
- ナレッジを探す
- 以前のバージョンアップ対応作業をgithubで漁り、issueやPRなど対応作業等参考になりそうなものを眺めた。
- 以前のバージョンアップ対応で使用したドキュメントで使えそうなものを探したり、教えてもらったりした。
- 調査(ざっくりどのへんの修正必要になってきそうか、外部ライブラリやミドルウェアの対応状況とかの調査)
- アプリケーション側ではどのへんの修正が必要そうか?、外部ライブラリの対応状況は?、ミドルウェアのアップデートは何と何が必要か?...etc
- →作業の見積もりを立てたくて取り込もうとしたが、大まかにしかやらなかった。実際にテストを回したり、動作確認をしていく過程で見積もりが取れていくと思ったので、割り切った。実際、結果として作業はスムーズにいったので時間をかけすぎないで正解だったかもしれない。
- アプリケーション側ではどのへんの修正が必要そうか?、外部ライブラリの対応状況は?、ミドルウェアのアップデートは何と何が必要か?...etc
- アップデート作業方針策定
- アップデートに直接関係ないリファクタリング等はしないという誓いを立てようとした。
- →チーム全体で認識に齟齬はなさそうだったので厳格なルールを定める等は全くしなかった。
- 判断に悩むようなところがあれば都度プロジェクトリーダーとして適宜ハンドリングするようにした。
- アップデートに直接関係ないリファクタリング等はしないという誓いを立てようとした。
- スケジュールを策定
- バージョンアップの開発作業、QA、リリース、ざっくりのスケジュールを立てた。
- 細かいスケジュールよりも都度発生するであろうタスクを柔軟にさばいていくことのほうに意識があったので、github projectsをカンバンを作ってタスク可視化と進捗確認のほうに力を入れた。
- バージョンアップの開発作業、QA、リリース、ざっくりのスケジュールを立てた。
ブランチ運用方針
masterから派生させたバージョンアップ対応用のreleaseブランチを用意した。 コードフリーズ期間は2週間設けたが、フリーズ解消後はmasterブランチでのリリースを可能とするため、masterへの機能改修、追加等があった場合は、releaseブランチへ都度変更をrebaseで取り込むようにした。
進行
FuelPHPもPHPのバージョンアップも作業の進め方としては概ね同じで、
- まずバージョンを上げる
- テスト回してコケたところを直す
- すべて完了したら動作確認テストを行う。動作確認でコケたところも片っ端から直す。※1 ※2
※1 動作確認テストは過去の資産(以前のバージョンアップ対応で使用したテスト項目)をアップデートして利用した。 ※2 FuelPHPのバージョンアップ対応が終わってから一度動作確認を行い、PHPのバージョンアップ対応をしてから再度動作確認を行った。 (FuelPHPの動作確認が終わった段階で一旦リリースを挟んでも良かった気がする。)
といった流れで進行した。
違いはPHPの対応の場合は事前にstg環境とCIの実行環境をphp7.3に対応しておく必要があるくらい。
あとは朝会をやってハマりどころとか進捗の共有等を毎朝行ったり、みんなでランチにいったりした。楽しかった(小並感)。
Unit Testがある程度ちゃんと用意されていたので地獄を見ることはなかったように思う。(テスト大事)
作業完了後に発生したバグはテストで担保できていない部分(ファットなコントローラーとかE2Eがあれば検知そうなところとか)が主で、テストケースが足りないといったところは殆どなかった。
バージョンアップで対応したところ
自分はプロジェクトリーダーのような役割を担っていたので、進捗管理やタスクのハンドリング等を行いつつ、実作業もやるみたいなスタンスで色々やっていたが、FuelとPHPのバージョンアップの開発作業よりもインフラ側の作業の方に時間を使っていた気がする。(開発作業は殆どレビュー中心の作業しかしていないかも、、いくつか対応したもののあるが、、)
FuelPHP
Laravelとかと違い頻繁にアップデートされているFWではないので、PHP7.3対応は本当に万全なのか懐疑的なところ(リリースされて間もないので実績が少ない。)がちょっとあったが、結果として問題ないようだった。
- sessionのライフサイクルの変更により、ログイン、ログアウトが壊れた。
- →影響を受けた箇所を新しいライフサイクルの仕様に合わせて変更した。
- Modelのforgeメソッドでプライマリーキーを引数に渡せなくなったことによるバグ。
- → プライマリーキーを引数に渡している箇所が多い、ニーズがあったので、下方互換のための継承用Modelクラスを作って対応。
- ORMのto_array()の仕様変更
- to_array()で返却される配列の値の順番が保証されるようになった。
- →テストケースでコケている箇所を変更に合わせて対応した。
- Paginationクラスの仕様変更
- query stringを取得したときの値のキャストに変更があった。
- intにキャストされなくなったことにより、ページネーションが壊れた。
- →不要な文字列を渡さないようにキャストして対応した。
- intにキャストされなくなったことにより、ページネーションが壊れた。
- urldecodeしなくなったことによる既存機能への影響
- https://github.com/fuel/core/commit/4614349b243dc48e864a694b017d7356984d9f3c#diff-247dccd657c3e1348e064416f2b1e22bL201
- →get()で値を拾っているところで必要な箇所にはurl_decodeを追加した。
- query stringを取得したときの値のキャストに変更があった。
- Cryptの内部暗号化アルゴリズム変更によるセッション保持への影響
- 何も考えず1.8.0から1.8.2にアプデするとログインセッションが無効になってログアウトされてしまう問題。
- php7.2からsodiumライブラリが標準化されて、FuelPHP1.8.2がそれに対応したため。
- →configで1.8.0の暗号化方式を使えるようにコアを拡張して対応。
Smarty
「マイナーアップデートとはなんだったのか」みたいな変更があってエモい気持ちになるところがあった。
v3.1.30→v3.1.33へのマイナーアップデートで発生した問題が2つほどあった。
- Large template parsing error in smarty 3.1.33 #488
- tplのサイズによっては{leteral}タグを不用意に挟むハックをしないとテンプレートのパースが正しくされないというバグを踏んだ
- date_formatタグの仕様変更によるバグ
- nullを渡されたときはnullが返却されるような仕様だったが、現在日時が返却されるような仕様に変更され、UI上で不適切な値を出力するバグが生じた
- pluginを用意してコアのdate_formatをオーバーライドするような実装をして対応した。
- nullを渡されたときはnullが返却されるような仕様だったが、現在日時が返却されるような仕様に変更され、UI上で不適切な値を出力するバグが生じた
PHP
PHP7系から1年ほど離れていたせいか、割と忘れている機能とか変更とかあって学びがあった。 場当たり的な対応をしてしまっている節がある気がする。より丁寧に対応するにはデータ構造から見直したり、メソッドの外観を調整する必要があったかもしれない。
- 空文字列に空のインデックス演算子を適用するとfatalになるエラー
- php.net - PHP マニュアル 言語リファレンス 型
- php7.1からの下位互換性のない変更
- ソートアルゴリズムの変更によるエラー
- php.net - 同値な要素の並び順
- 並び順に依存していた箇所でバグが出た
- →LaravelCollectionを組み込んでいたのでcollectionを利用してソートアルゴリズムに依存しない形にして対応
- Datetimeのformatにミリ秒を表すvが追加された
- php.net - パラメータ
- →ミリ秒の違いでfailするテストを修正
- 数値ではない値で演算を行うとE_WARNINGが発生
- php.net - 無効な文字列による算術演算の通知
- php7.1からの変更
- 演算必要箇所はintへのキャストで対応
- count()の引数で配列またはオブジェクト以外を適用している箇所でエラー
count(null)でもWarningになる- php.net - Countableインタフェース
- →
issetや!emptyでnullをチェックしたり、該当箇所の引数を配列またはオブジェクトに変更するなどして対応
インフラ
- 既存のPHP5.6環境からPHP7.3環境へ移行するためのchefをかいた。
- インスタンス構築
- ベースとなるインスタンス一台にchefを流して、AMIを取得。AMI起動から複数台を構築。
- stagingとproductionで合計20数台を構築
- ベースとなるインスタンス一台にchefを流して、AMIを取得。AMI起動から複数台を構築。
- jenkins
- デプロイ・リリース作業はjenkinsを使っているので必要に応じてjobの調整等した。
リリース
カナリアリリースで対応した。 PHP5.6のインスタンスがぶら下がっているターゲットグループにPHP7.3のインスタンスを少しずつぶら下げて並行稼働、7.3の台数を増やしつつ、5.6の台数を減らしていった。 全部切り替え終わった段階で完全切替(releaseブランチをmasterにマージ)。
一部本番環境で調査したエラーについては、ロードバランサーのリスナーの設定を活用した。 送信元IPを社内IPで指定して、社内からのアクセスを特定のインスタンスに振り分けて検証する方法を取った。(ダークカナリアリリース?)
並行稼動期間中は度々エラーが発生し、LBからインスタンス切り離したりつけ直したりと障害対応しつつ、安定稼働、完全切替まで2週間を要した。
苦労したところ
リリースのフェーズでテストや動作確認で検知できなかったバグがいくつか発生した。
中には解決が難しいものや対応が厄介なモノ等あったが、何かあったときは切り戻し作業(LBからインスタンスを切り離すだけ)を早急に行いつつ、エラーログの収束、安定稼働に2週間弱ほと格闘した。
自分の目線ではバージョンアップ対応よりもリリース作業のほうが大変だった印象がある。
その他
PHPバージョンアップ関連のブログやスライドを漁ってみた。
バージョンアップ時に対応が必要な箇所はプロダクトによって様々だろうが、基本的な作業の流れの結構似ていると思った。 GameWithさんは環境が酷似しているのですごくエモい気持ちになった。
5年以上PHP5で運用されていたFuelPHPで動くGameWithをPHP7.3にバージョンアップしました! #GameWith #TechWith テストコードが無くてPHP7へのバージョンアップが出来ない?ボットで解決しました! 3ヶ月でphp5.5から7.2にバージョンアップした現在と今後の向き合い方
パフォーマンス改善
CPU・メモリ使用率には大幅改善があったが、レスポンスタイムには大きな変化が見られなかった。 まだちゃんと計測できていない部分があるので調査中。
所感
バージョンアップ対応は人海戦術が有効な部分が大きいと思うので、集中的に取り組むと比較的短期間で終わるのかもしれない。 良い経験になった。
FuelPHP1.8.2が急にリリースされた奇跡があった一方で、合宿終盤でAWSの大規模障害に直面したという稀有な出来事もあった。(stg環境での動作検証に多少の影響があった)