格闘の記録 〜13年前のWordPressを復旧〜
以前、10年以上放置したWordPressから何が出てきたかを書いた。
今回は復旧作業そのものの記録である。手順というより、想定と実際がどこでずれたかの記録になっている。
作業前に自分で引き継ぎ文書を書いた。症状、原因、環境の制約、対処手順。そこに書いた前提が3つ、事実と違っていた。
なお私は電気回路の出身で、Webサーバの運用は本業ではない。
前提1: 症状の記載が古かった
文書にはこう書いていた。
Your PHP installation appears to be missing the MySQL extension
実際の応答はこうだった。
HTTP/1.1 500 Internal Server Error
(本文 0 バイト)
エラーメッセージが出なくなっている。
引き継ぎ文書の「現在の症状」は、書いた時点のスナップショットである。作業再開時には自分で取り直す。
前提2: ファイルの移動が完了していなかった
文書の記載。
WordPress本体一式をルートから公開フォルダへ移動
実際にはルートに残っていた。
~/web/
├── wp-admin/ 残っている
├── wp-includes/ 残っている
├── wp-content/ 319MB
└── mixture/ 公開フォルダ
├── wp-config.php ○
├── .htaccess ○
├── wp-*.php ○
├── wp-admin/ 無い
├── wp-includes/ 無い
└── wp-content/ 無い
ブラウザのFTP画面でディレクトリを個別にコピーしていて、サイズの大きい3つの手前で作業が止まっていた。
wp-settings.php はあるが wp-includes/load.php が無い。require が失敗してfatal errorになり、0バイトの500になる。前提1の症状変化はこれが原因だった。
移動が完了していなかったため、ルート側の wp-content 319MBはそのまま残っていた。移動として完遂されていた場合、失敗時に残らなかった可能性がある。
数千ファイルの転送はブラウザのFTP画面では現実的でない。この時点でCLIに切り替えた。
前提3: PHPバージョンの下限はドメイン単位だった
文書の記載。
PHPバージョン 7.4(CGI版)。7.4が下限で、これ以下には戻せない
これは管理画面のプルダウンを見て書いたものである。実際、7.4より下の選択肢は表示されない。
同一アカウント上の他ドメインを実測した結果。
| ドメイン | PHP | 状態 |
|---|---|---|
| 復旧対象 | 7.4.33 | 停止中 |
| 別サイトA | 5.4.45 | 正常稼働 |
| 別サイトB | 5.3.29 | 正常稼働 |
| 別サイトC | 8.3.33 | 正常稼働 |
PHPバージョンはドメイン単位で設定されており、同一アカウントに5.3と8.3が同居していた。「7.4が下限」は対象ドメインの選択肢についての記述であり、アカウント全体の制約ではない。一度上げると戻せない仕様が、そのドメインにだけ適用されている。
共有ホスティングの制約は、アカウント単位・ドメイン単位・ディレクトリ単位で異なる。どのスコープの話かを確認する必要がある。
同じDBで片方だけ接続できなかった理由
復旧対象と別サイトAは同じデータベースを使っている。認証情報も同じである。片方だけ接続できない。
原因はPHPバージョンだった。PHP 5.4の ext/mysql は旧式の認証プロトコルに対応しているが、PHP 7のmysqlndは対応していない。
根本にあるのはこれである。
mysqldump: Got error: 2049: Connection using old (pre-4.1.1)
authentication protocol refused
DBユーザーのパスワードが old_password 形式で保存されていた。
同じ認証情報で mysql コマンドは接続できていた。mysql クライアントは secure_auth がOFF、mysqldump はONという違いによる。エラーメッセージはクライアントのオプションを指しているが、原因はサーバ側に保存されているハッシュの形式である。
パスワードの再設定では解決しない
引き継ぎ文書に書いていた対処法。
PHP 7.1以降では
old_password形式が使えない。 対処: 管理画面でパスワードを再設定してnative_password形式に更新する
このサーバでは効かない。確認は1行で済む。
SELECT LENGTH(PASSWORD('probe')), LENGTH(OLD_PASSWORD('probe'));
-- 16, 16
新形式なら41バイト、旧形式なら16バイトになる。両方16だった。
SHOW VARIABLES LIKE 'old_passwords';
-- old_passwords = ON
このMySQL 5.1は、新しく設定したパスワードも旧形式で保存する。管理画面でリセットしてもPHP 7.4からは接続できない。
この時点で「パスワードを直す」という選択肢が消え、DBサーバの移行が必要になった。当初の計画には無かった工程である。
パスワードリセット系の対処法は、サーバがどの形式で保存するかを確認しないと空振りする。SELECT LENGTH(PASSWORD('任意の文字列')) で判別できる。
影響範囲の確認
DBを変更するにあたり、他サイトへの影響を確認した。
7サイトすべての wp-config.php でDBユーザー名が同じ文字列だった。同一ユーザーを共有しているように見える。
平文を残さずに比較するため、md5の先頭で突き合わせた。
| DB | パスワード(md5先頭) | DBホスト | 使用サイト |
|---|---|---|---|
| A | 7fe38c58… |
mysql517 | 5サイト |
| B | eec6d786… |
mysql025 | 2サイト |
| C | 21b4ea10… |
mysql132 | 1サイト |
DBごとに別だった。ユーザー名の文字列が一致しているだけで、DBホストも異なるため実体は別ユーザーである。
影響範囲は7サイトから5サイトに縮小した。
比較のみが目的なら平文を出す必要はない。値を知る必要がなく、同一かどうかだけ判定すればよい場面は多い。
サーバのshellで使えるコマンド
CLIに移行して最初に環境を確認した。
| コマンド | 有無 |
|---|---|
tar gzip unzip rsync find |
あり |
mysql mysqldump |
あり(5.6.51) |
php(CLI) |
なし |
mktemp |
なし |
/dev/stderr |
使用不可 |
php CLIが無いためWP-CLIは使えない。方針を決める前に確認しておく情報である。
mktemp が無いのでテンポラリファイルはパスを固定して作成する。2>>/dev/stderr は失敗してパイプごと終了するため、ファイルに出力する。
共有ホスティングのshellは標準的なLinux環境とは異なる。作業前に command -v で棚卸ししておく。
移行先はMySQL 8.4、クライアントは5.6
old_passwords の解決として新規DBを作成したところ、MySQL 8.4が割り当てられた。PHP 7.4のmysqlndは対応している。
一方、サーバ上の mysql クライアントは5.6.51である。8.4のデフォルト認証方式に対応していない可能性があった。
対策として、手元のMacに新しいクライアントを入れ、SSHポートフォワード経由でインポートできる経路を用意した。結果的には5.6のクライアントでも接続できたため使わなかった。
移行時はサーバ側だけでなくクライアント側のバージョンも確認する。
wp-content/db.php ドロップイン
コア入れ替え前に wp-content/ を確認したところ、db.php が存在した。
WordPressのドロップインで、2011年製のキャッシュプラグインがDB接続層を独自クラスに差し替えていた。
$GLOBALS['wpdb'] = new dbrc_wpdb( DB_USER, DB_PASSWORD, DB_NAME, DB_HOST );
コアを更新しても、これが残っていると単独で動作しなくなる。PHP 8で削除された定数定義の書き方も使われていた。
wp-content/ 直下の db.php object-cache.php advanced-cache.php は、プラグイン一覧に表示されないが優先的に読み込まれる。プラグインを全停止しても症状が消えない場合は、これらを確認する。
リネームして無効化した。クエリキャッシュのみの機能であり、可逆である。
日本語版パッケージの翻訳ファイル
引き継ぎ文書の記載。
rm -rf wordpress/wp-content # 既存のテーマ・プラグインを守るため
新しいコアの wp-content で上書きするとテーマ・プラグイン・アップロード画像が消えるため、この判断自体は正しい。
ただし日本語版パッケージでは翻訳ファイルが wp-content/languages/ に含まれる。
wordpress/wp-content/languages/ja.mo 413KB
wordpress/wp-content/languages/admin-ja.mo
wordpress/wp-content/languages/ja-*.json
これを削除すると4.9時代の古い .mo が残り、管理画面の表示が一部英語になる。
languages/ のみを別に切り出し、既存のディレクトリへマージした。
「wp-content = ユーザーデータ」は英語版パッケージの前提である。作業前に unzip -l で中身を確認する。
WordPressの自動更新が先に走っていた
コアを5.9に置き換えた後、upgrade.php でDBスキーマを移行する予定だった。
実行前に db_version を確認したところ、すでに5.9が要求する値になっていた。
generator タグを確認すると、配置したのは5.9で、動作しているのは5.9.16だった。ファイルの更新時刻は配置の5分後である。
WordPressのバックグラウンド自動更新が、初回アクセス時のWP-Cron経由で実行され、コアの自己更新、upgrade.php?step=upgrade_db によるスキーマ移行、完了通知メールの送信までを行っていた。
WordPress 3.7以降、マイナーリリースの自動更新は既定で有効である。古いバージョンを配置しても、初回アクセスでパッチが適用される。
5.9には既知の脆弱性があるため、5.9.16まで上がったこと自体は望ましい。ただし復旧作業中は、自分の操作と自動処理の結果が混在する。判別できたのはファイルのタイムスタンプのみだった。
実際の作業手順
引き継ぎ文書に書いた計画。
1. バックアップ
2. コアを差し替え
3. upgrade.php でスキーマ移行
4. 最新版へ更新
実際の手順。
1. バックアップ(ファイル + DB)
2. 新規DB(MySQL 8.4)を作成し29テーブルを移行 ← 追加
3. wp-config.php を新DBへ向ける ← 追加
4. wp-content をルートから公開フォルダへコピー ← 追加
5. db.php ドロップインを無効化 ← 追加
6. コアを配置 + 翻訳をマージ
7. プラグインを全停止した状態で upgrade.php ← 変更
8. 表示確認 → プラグインを1つずつ復帰
9. 最新版へ更新
5工程増えた。うち3つは、引き継ぎ文書の前提が事実と違ったことに起因する。
3つとも、確認方法まで書いていれば防げた種類の誤りだった。「7.4が下限」ではなく「管理画面のプルダウンに7.4より下が表示されない」と書いてあれば、スコープの取り違えに気づける。引き継ぎ文書には結論だけでなく、その根拠を併記する。
次の記事では、この作業で最も時間を使った部分を書く。判定が4回誤っていた件である。
参考
- MySQL リファレンスマニュアル:
old_passwords/secure_auth - WordPress: バックグラウンド自動更新の設定について
- WordPress: ドロップイン(
db.php/object-cache.php/advanced-cache.php) - WordPress: 過去バージョンのリリース一覧
この文章は原案・ディレクション:九頭龍、作文:AIで書いています。
