上級者向け

第8回: 壊さない WordPress — Git / ステージング / マイグレーション戦略

第8回: 壊さない WordPress — Git / ステージング / マイグレーション戦略

「プラグインを更新したら、管理画面が真っ白になった」── そんな朝、迎えたことはありませんか?

WordPress は世界中で動いているぶん、運用事故も世界中で毎日起きています。コア更新で表示崩壊、データベース置換のミスでシリアライズが破壊、本番だけ手で直してステージングと食い違う ── ほとんどは「触り方の設計」が無いことが原因です。

本記事は、その「触り方」を体系化します。対象は、自分や顧客の WordPress を任されている中級〜上級の開発者・ディレクター・運用責任者の方々です。大企業 DevOps の理想論ではなく、個人事業や小規模チームでも回せる現実的な運用フローを軸に進めます。

「壊さない WordPress」運用の三本柱

「壊さない WordPress」を支える3原則 ── 触らない / 戻せる / 再現できる
「壊さない WordPress」を支える3原則 ── 触らない / 戻せる / 再現できる

「壊さない」を考える前に、「壊れる」を定義しておきます。

運用現場で「サイトが壊れた」と呼ばれる現象には、性質の異なる 3 種類があります。原因も対処も違うので、まずは分けて見ます。

「壊れる」とは何か

壊れ方原因例検出のしやすさ
改ざん外部からの侵入・マルウェア・SEO スパム注入★★
更新事故プラグイン / コア / PHP バージョン更新の副作用★★★★
構成ドリフト本番だけ手で直し、ステージング・Git と乖離

改ざんは第6回で扱いました。本記事の主役は 更新事故構成ドリフト です。

特に「構成ドリフト」は厄介で、気づかないうちに進行します。ある日突然、デプロイが通らなくなる。ステージングで動いた修正が本番で動かない。そんな形で表面化したときには、原因の特定だけで数日溶けることもあります。

3 つの原則 — 触らない / 戻せる / 再現できる

「壊さない WordPress」を支える原則は、次の 3 つに整理できます。

  • 触らない ── 本番の管理画面と SSH で直接編集しない。すべての変更は再現可能な形で記述する
  • 戻せる ── どの変更も 1 操作で 1 つ前の状態に戻せる構造を、変更前に用意する
  • 再現できる ── ローカル / ステージング / 本番のどこでも、同じ手順で同じ結果になる

この 3 つは独立ではなく、互いに支え合っています。「触らない」を徹底するから差分が記録される(戻せる)。差分があるから同じ手順を再実行できる(再現できる)。逆に「ちょっとだけ本番を触る」が始まると、3 原則は同時に崩れます。

WEBさん
WEBさん

これは Infrastructure as Code / GitOps と呼ばれる考え方を WordPress に当てはめたものです。難しそうに聞こえますが、要するに「本番に直接ログインして手で直すのをやめる」という運用習慣の話でもあります。

個人事業〜中小組織の現実的な運用範囲

とはいえ、すべての WordPress サイトに本格的な CI/CD パイプラインや Bedrock 構成が必要なわけではありません。月数千 PV の個人ブログに Kubernetes は要りません。本記事では「規模に応じて選べる」軽量版と本格版を両方提示する形を取ります。

運用レベルの目安

  • Level 1(個人事業): 子テーマ + 手動バックアップ + ステージング 1 環境
  • Level 2(小規模制作会社): Git でテーマ管理 + 自動バックアップ + 手動デプロイ
  • Level 3(中規模事業者): Bedrock + CI/CD + 復元テスト定期実施
  • Level 4(大規模 / 業務基幹): 上記 + マルチリージョン + RTO/RPO 定義

本記事は主に Level 2〜3 を狙い、Level 1 でも実践できるエッセンスを抽出します。

第7回(構築)との対比

第7回は「WordPress を Web アプリとして組む」構築側の話でした。CPT、Role、REST、外部 API ── 複雑な要件を WordPress で受ける設計です。

第8回はその裏面、「複雑になった WordPress を、長期的に壊さず運用する仕組み」です。組み立てる力と、壊さない力。両方が揃ったとき、WordPress は業務システムとして使えるツールになります。

子テーマ / mu-plugin / カスタムプラグインの使い分け

拡張ポイント 4 種の責務と寿命
拡張ポイント 4 種の責務と寿命

「壊さない WordPress」の入口は、コードをどこに置くかの設計です。

WordPress には拡張ポイントが複数あります。それぞれ寿命とテスト戦略が違うので、用途に応じて置き場所を選ぶ必要があります。

親テーマと子テーマの責務分離

もっとも基本の分離は、親テーマ(購入テーマ・配布テーマ)と子テーマです。

親テーマはアップデートで上書きされます。ここに修正を書くと、次のアップデートで消えます。子テーマは上書きされない領域として用意します。

ただし、子テーマに何でも書くと今度は子テーマの functions.php が肥大化します。1000 行を超えた子テーマは、もはや「テーマ」ではなくミニアプリです。

いつ mu-plugin で、いつカスタムプラグインか

判断基準を表で整理します。

置き場所向いている用途無効化の可否寿命の目安
子テーマ見た目・テンプレート差し替えテーマ切替で可サイト寿命と同等
mu-pluginサイト全体に必須の機能・設定不可(常時有効)サイト寿命と同等
カスタムプラグイン独立した機能ブロック機能の必要な期間
code snippets 系プラグイン一時的な実験短期(検証後は移動)

判断のコツは 2 軸あります。「これは見た目か、機能か」。「テーマを変えたときに残したいか、消えてほしいか」。この 2 つで分類すると、置き場所はほぼ自動的に決まります。

  • カスタム投稿タイプ(CPT)の登録 → mu-plugin(テーマ非依存)
  • ショートコードの定義 → カスタムプラグイン
  • single-product.php の差し替え → 子テーマ
  • 管理画面のメニュー非表示 → mu-plugin
  • 独自 REST エンドポイント → カスタムプラグイン

functions.php の肥大化問題

子テーマの functions.php に CPT 登録、REST 拡張、Cron、外部 API 連携を全部書く ── これをやると、テーマを切り替えた瞬間にサイトの機能が半分消えます。

「テーマに直書きしない」原則を破った結果です。リデザインのたびに痛い目を見ます。

判断基準

「テーマを変えたら消えてもよいか?」を必ず自問する。消えてはいけないものは、テーマではなく mu-plugin かカスタムプラグインに置く。これだけで、将来のリデザイン時の事故が大幅に減ります。

各レイヤーの寿命とテスト戦略

レイヤーごとに、変更頻度とテストの粒度を変えます。

  • 子テーマ: 視覚回帰テスト中心(スクリーンショット比較)
  • mu-plugin: ステージングでの統合テスト(機能が動くか)
  • カスタムプラグイン: PHPUnit などのユニットテスト + ステージング統合

個人事業レベルではユニットテストまでは現実的でないことも多いです。最低限「ステージングで触ってから本番に上げる」を運用ルールとして固定するところから始めます。

設定 (wp_options) / コード / コンテンツの分離哲学

WordPress の 3 層と同期方向 ── コード / DB スキーマ / DB データ
WordPress の 3 層と同期方向 ── コード / DB スキーマ / DB データ

WordPress の状態は、実は 3 種類に分かれます。

この 3 つを混ぜたまま運用すると、ステージングと本番の同期がほぼ確実に事故ります。まずは「何と何が別物なのか」を分けて見ます。

WordPress の 3 つの状態

具体例同期の向き
コードテーマ・プラグイン・mu-pluginローカル → 本番
DB スキーマテーブル定義・カスタムテーブルマイグレーションスクリプト経由
DB データ記事・wp_options・wp_postmeta本番 → ステージング(逆は禁忌)

重要なのは 同期の向きが層によって違う ことです。コードは「左から右」、データは「右から左」、スキーマは「中間のマイグレーション」を経由します。

これを意識しないと、本番の最新コンテンツをステージングのコピーで上書きしてしまう事故が起きます。

環境依存設定をどこに置くか

環境ごとに値が違うもの ── サイト URL、API キー、メール送信先、Stripe シークレット、reCAPTCHA キーなど。これらを wp_options に保存するのは危険です。

ステージングと本番の DB を同期したとき、本番の API キーがステージングに、あるいは逆にステージングのキーが本番に上書きされる事故が起きます。

この種の値は wp-config.php か環境変数 に置くのが正解です。コードから読むときも、できれば定数化しておきます。

<?php
// wp-config.php
// 環境別に値を切り替える例

if (getenv('WP_ENV') === 'production') {
    define('WP_HOME', 'https://example.com');
    define('WP_SITEURL', 'https://example.com');
    define('MY_API_KEY', getenv('PROD_API_KEY'));
} elseif (getenv('WP_ENV') === 'staging') {
    define('WP_HOME', 'https://staging.example.com');
    define('WP_SITEURL', 'https://staging.example.com');
    define('MY_API_KEY', getenv('STAGING_API_KEY'));
} else {
    define('WP_HOME', 'https://web-navigator.blog/webnavi-blog');
    define('WP_SITEURL', 'https://web-navigator.blog/webnavi-blog');
    define('MY_API_KEY', getenv('LOCAL_API_KEY'));
}
PHP

WP_HOME / WP_SITEURL を定数で固定する利点

WP_HOME と WP_SITEURL を wp-config.php に書くと、wp_options の siteurl / home の値より優先されます。これで以下のメリットが生まれます。

  • 本番 DB をステージングに持ってきても URL が自動で切り替わる(定数が勝つため)
  • 管理画面の「設定 → 一般」から誤って URL を変更されない(グレーアウト表示)
  • 環境ごとの URL が wp-config.php を見るだけで把握できる

dotenv / Composer での環境分離

もう一歩進めるなら、vlucas/phpdotenv を Composer で導入し、.env ファイルで環境変数を管理する形が定番です。これは後述する Bedrock の標準構成でもあります。

# .env (本番)
WP_ENV=production
DB_NAME=prod_wp
DB_USER=prod_user
DB_PASSWORD=********
DB_HOST=localhost

WP_HOME=https://example.com
WP_SITEURL=${WP_HOME}/wp

# .env はGitに含めないこと
Bash

注意

.env ファイルは Git にコミットしないようにします。.gitignore に追加し、リポジトリに残った履歴がないかも事前に確認します。代わりに .env.example(キー名だけ書いて値は空)を共有用にコミットする運用が一般的です。

Git で WordPress テーマを管理する正攻法

Git に入れるもの / 入れないものの境界線
Git に入れるもの / 入れないものの境界線

WordPress を Git で管理しようとして、最初につまずくのは「何を入れて、何を入れないか」です。

コア丸ごと入れると 50MB 超になります。アップロード画像を入れるとリポジトリが肥大化します。wp-config.php を入れると秘密情報が漏れます。それぞれ理由があって、入れないのが正解です。

何を Git に入れるか / 入れないか

標準 WordPress の場合、リポジトリには以下の最小構成が現実的です。

対象Git に入れる理由
wp-content/themes/自作テーマ入れる自前のコード
wp-content/mu-plugins/入れる自前のコード
wp-content/plugins/自作プラグイン入れる自前のコード
配布プラグイン本体入れない(Composer 管理推奨)サイズ / ライセンス
WordPress コア入れないサイズ / アップデート競合
wp-content/uploads/入れないサイズ / 別途バックアップ
wp-config.php入れない(テンプレのみ)秘密情報
.env入れない秘密情報

.gitignore の実例

そのままコピーして使えるテンプレートを置いておきます。

# WordPress core
/wp-admin/
/wp-includes/
/wp-*.php
/index.php
/license.txt
/readme.html
/xmlrpc.php

# Uploads
wp-content/uploads/

# Cache
wp-content/cache/
wp-content/upgrade/

# Plugins (自作以外は基本除外)
wp-content/plugins/*
!wp-content/plugins/my-custom-plugin/

# Configuration
wp-config.php
.env
.env.local

# OS / Editor
.DS_Store
.idea/
.vscode/
*.swp
*.log

node_modules/
vendor/
Bash

アップロードファイル(wp-content/uploads/)の管理

uploads は Git に入れず、別経路で同期します。選択肢は概ね 3 つです。

  • rsync / SFTP で手動同期(小規模)
  • S3 / R2 などのオブジェクトストレージにオフロード(中規模以上)
  • 本番をマスターとし、ステージング・ローカルは差分取得のみ

個人事業レベルでは「本番 uploads をマスターにして、ローカルには必要なときだけ rsync で取得」が最も現実的です。

ブランチ戦略

WordPress プロジェクトに、複雑なブランチ戦略は要りません。GitHub Flow ベース(main + feature ブランチ)で十分です。

  • main: 本番デプロイ済み。常に動作する状態を維持
  • develop(任意): ステージング自動デプロイ用
  • feature/*: 機能ごとの作業ブランチ。PR で main にマージ
  • hotfix/*: 本番障害時の緊急修正。main から切って main へ戻す

コミットメッセージ規約

Conventional Commits をベースに、WordPress プロジェクト向けに簡素化したものを推奨します。

# 形式: <type>(<scope>): <subject>

feat(theme): single.php に著者ボックスを追加
fix(plugin): カスタム CPT の rewrite ルール衝突を修正
chore(deps): WP コアを 6.7.1 に更新
refactor(mu): functions.php を mu-plugin に分割
docs(readme): デプロイ手順を更新
style(theme): エディタ CSS のインデント調整
Bash

scope に theme / plugin / mu / config / db あたりを置くと、後から log を眺めたときに「どの層に何があったか」が一目で分かります。後述するロールバック判断にも効きます。

ステージング → 本番 デプロイ自動化

Local → Staging → Production の 3 段フロー
Local → Staging → Production の 3 段フロー

「本番直接編集」をやめると同時に必要になるのが、ステージング環境です。

目指す流れは「ローカルで触る → ステージングで確認 → 本番に反映」の 3 段です。

3 段構成の意味

  • ローカル: 書く・壊す・実験する場所。失敗してよい
  • ステージング: 本番に近い環境で確認する場所。本番データの複製を持つ
  • 本番: ユーザーが触る場所。手で直さない

ローカルとステージングを兼ねている運用もよく見ます。ですが、ローカルは PHP / MySQL バージョンや HTTPS の有無、メール送信、画像オフロードなどが本番と違います。本番に近い「ステージング」を 1 つ持っておくと、事故が大幅に減ります。

ローカル開発環境の選択肢

ツール特徴向いている人
Local (by Flywheel)GUI 中心 / 一発起動 / Live Link個人事業 / 制作会社
wp-env公式 / Docker ベース / CLI 中心プラグイン / テーマ開発者
Docker(自前 compose)完全制御 / 本番 OS と揃えやすい中規模以上 / 業務利用
DDEVDocker ラッパー / 設定が薄い複数プロジェクト管理

当ブログは Local by Flywheel で開発しています。GUI の手軽さと、本番反映時に Magic Sync が使える点が魅力です。ただ、CI/CD と組み合わせるなら Docker / DDEV / wp-env の方が再現性が高くなります。

WEBさん
WEBさん

私は Local + Git + 手動デプロイで何年も運用してきました。個人事業の規模ならこれで十分回ります。ただ、案件が複数並んだり、複数人で触るようになった瞬間、Docker / CI/CD への移行を考え始めてもよい時期です。

デプロイの 5 つのレベル

デプロイ自動化は段階的に進めます。いきなり最終形を目指すと挫折しがちです。現状に応じて 1 段ずつ上がる形を推奨します。

  • Lv1 手動 SFTP: ファイルを 1 つずつアップ。ミスが多く、何を変えたか追えない
  • Lv2 Git pull: 本番サーバーで git pull。最低限の差分管理は成立
  • Lv3 SSH デプロイスクリプト: ローカルから 1 コマンドで本番更新
  • Lv4 CI/CD 自動デプロイ: main ブランチへの push をトリガに自動デプロイ
  • Lv5 承認フロー付き自動デプロイ: ステージング自動 + 本番は承認ボタンで実行

個人事業なら Lv2〜Lv3、小規模制作会社なら Lv3〜Lv4、業務システム要件なら Lv5 を目指す目安です。

同期ツールの比較

ツール対象向き注意点
Local Magic Syncファイル + DB双方向対応ホスト限定
WP Migrate (Lite/Pro)DB 中心双方向URL 置換が安全
UpdraftPlus Migrateバックアップ単位一方向差分転送は弱め
rsync + wp db export分離管理スクリプト次第CLI に慣れる必要

「DB だけ持ってくる」「ファイルだけ持ってくる」を分けられるツールを選ぶと、事故時の影響範囲を絞れます。

本番反映前のレビューフロー

個人作業でも、自分の目でもう一度通すレビューを挟むと事故率が下がります。Pull Request のセルフレビューでよいので、次のチェックリストを置いておくと習慣化しやすくなります。

本番反映前チェック

  • 変更したファイル一覧を git diff で確認
  • DB マイグレーションが必要か確認
  • 環境依存設定(API キー等)が変わっていないか
  • ステージングで該当ページを実際に開いて確認
  • キャッシュクリアの要否
  • ロールバック手順を 1 行で言えるか

DB マイグレーション戦略

スキーマバージョン管理の流れ
スキーマバージョン管理の流れ

WordPress のもっとも厄介な部分のひとつは、DB スキーマ管理の標準的な仕組みがないことです。Laravel の php artisan migrate や Rails の rails db:migrate に相当するものがありません。

そのため、カスタムテーブルを追加したり、データ構造を変更したりする運用は自前で考える必要があります。

WordPress のスキーマ管理がない問題

標準 WordPress は、コアアップデート時の内部処理(wp_upgrade → dbDelta)でテーブル定義を更新します。ですが、これは「WordPress コア専用」の仕組みです。自作プラグインのテーブル更新は自前でやる必要があります。

そこで多くの WordPress プラグインが採用しているのが、「プラグイン側で DB バージョン番号を持ち、起動時に現在の DB バージョンと比較して、必要なら upgrade を走らせる」パターンです。

dbDelta + バージョン管理パターン

<?php
/**
 * カスタムテーブルのマイグレーション例
 * mu-plugins/my-schema.php に置くと、毎回 admin_init で走る
 */

const MY_DB_VERSION = '1.2.0';
const MY_DB_OPTION_KEY = 'my_db_version';

add_action('admin_init', function () {
    $current = get_option(MY_DB_OPTION_KEY, '0.0.0');
    if (version_compare($current, MY_DB_VERSION, '<')) {
        my_run_migration($current, MY_DB_VERSION);
        update_option(MY_DB_OPTION_KEY, MY_DB_VERSION);
    }
});

function my_run_migration($from, $to) {
    global $wpdb;
    require_once ABSPATH . 'wp-admin/includes/upgrade.php';

    $charset_collate = $wpdb->get_charset_collate();
    $table = $wpdb->prefix . 'my_events';

    $sql = "CREATE TABLE {$table} (
        id BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        user_id BIGINT(20) UNSIGNED NOT NULL,
        event_type VARCHAR(60) NOT NULL,
        payload LONGTEXT NULL,
        created_at DATETIME NOT NULL,
        PRIMARY KEY (id),
        KEY user_id (user_id),
        KEY created_at (created_at)
    ) {$charset_collate};";

    dbDelta($sql);
}
PHP

dbDelta の落とし穴

  • カラム名は半角スペース 2 つで区切る(1 つだと無視されることがある)
  • KEY 句のスペースもシビア。Codex のサンプル通りに揃える
  • dbDelta は ALTER 系を「比較して必要分だけ」打つよう設計されている。基本は何度実行しても安全だが、KEY 句の表記揺れで重複インデックスが増える事例もあるため、本番反映前にステージングで挙動確認しておく
  • カラム削除(DROP COLUMN)はやってくれない。削除は別 SQL で明示

データマイグレーションスクリプト

スキーマだけでなく「データ自体を変換する」マイグレーションもあります。例えば「postmeta に文字列で保存していた金額を、税抜・税込の 2 つに分割する」といったケースです。

WordPress には専用ツールがないので、wp-cli のカスタムコマンドとして書くのが扱いやすい形です。実行ログが残る、ドライランが書ける、本番でも CLI で叩けるという利点があります。

<?php
/**
 * 例: postmeta '_price' を '_price_excl' '_price_incl' に分割する
 * wp my-tools split-price --dry-run
 */

if (defined('WP_CLI') && WP_CLI) {
    WP_CLI::add_command('my-tools split-price', function ($args, $assoc) {
        $dry = !empty($assoc['dry-run']);
        $posts = get_posts([
            'post_type'      => 'product',
            'posts_per_page' => -1,
            'meta_key'       => '_price',
            'fields'         => 'ids',
        ]);
        foreach ($posts as $id) {
            $raw = get_post_meta($id, '_price', true);
            $excl = round((float)$raw / 1.1, 2);
            $incl = (float)$raw;
            if ($dry) {
                WP_CLI::log("DRY {$id}: excl={$excl} incl={$incl}");
            } else {
                update_post_meta($id, '_price_excl', $excl);
                update_post_meta($id, '_price_incl', $incl);
            }
        }
        WP_CLI::success(count($posts) . ' posts processed');
    });
}
PHP

ポイントは --dry-run を必ず実装することです。本番で叩く前にドライランで「何件が、どう変わるか」を出力させて目視確認します。

search-replace の安全な使い方

第5回でも触れた話の延長です。URL の置換や本番→ステージング同期で使う wp search-replace は、ドメインによっては事故が起きやすい操作です。

# 安全な search-replace の手順

# 1. まずドライランで件数を確認
wp search-replace 'https://example.com' 'https://staging.example.com' --dry-run

# 2. シリアライズ対応 + 全テーブル + ドライランで詳細
wp search-replace 'https://example.com' 'https://staging.example.com' --all-tables --dry-run --report-changed-only

# 3. バックアップを取ってから本番実行
wp db export backup-before-replace-$(date +%Y%m%d-%H%M).sql

wp search-replace 'https://example.com' 'https://staging.example.com' --all-tables --report-changed-only
Bash

逆向き同期の禁忌

本番 DB をステージングに持ってくる(本番 → ステージング)は安全な方向です。逆(ステージング → 本番)は 本番の最新コンテンツを上書きする可能性が極めて高いため、原則として避けます。どうしても必要な場合は「ステージング DB の特定テーブルだけ抽出して、本番にマージ」の形に限定します。

Composer-based WordPress (Bedrock / Roots)

標準 WordPress vs Bedrock ── ディレクトリ構造の違い
標準 WordPress vs Bedrock ── ディレクトリ構造の違い

WordPress を「Composer で管理する PHP プロジェクト」として扱う構成として、Roots プロジェクトが提供する Bedrock があります。標準 WordPress とはディレクトリ構造から違うので、移行する価値があるかは慎重に考える必要があります。

標準 WordPress との違い

項目標準 WPBedrock
コアの場所ドキュメントルート直下web/wp/ 配下
wp-content/wp-content//web/app/
設定wp-config.phpconfig/ + .env
プラグイン管理管理画面 / ZIPComposer (wpackagist)
コア更新管理画面 / wp-clicomposer update

Composer でプラグイン管理する利点

  • プラグインのバージョンが composer.lock で固定される
  • 環境ごとに異なる開発専用プラグイン(Query Monitor 等)を require-dev で分離可能
  • プラグインの追加・削除・更新がコミット履歴に残る
  • 本番サーバーに管理画面から ZIP をアップする運用を排除できる
{
  "require": {
    "php": ">=8.1",
    "roots/wordpress": "6.7.1",
    "wpackagist-plugin/wordpress-seo": "^23.0",
    "wpackagist-plugin/wp-mail-smtp": "^4.0"
  },
  "require-dev": {
    "wpackagist-plugin/query-monitor": "^3.16",
    "wpackagist-plugin/debug-bar": "^1.1"
  },
  "extra": {
    "wordpress-install-dir": "web/wp",
    "installer-paths": {
      "web/app/plugins/{$name}/": ["type:wordpress-plugin"],
      "web/app/themes/{$name}/": ["type:wordpress-theme"]
    }
  }
}
JSON

既存サイトを Bedrock に移行する価値はあるか

結論から書きます。既存の安定稼働サイトをわざわざ Bedrock に移行するメリットは、多くの場合あまり大きくありません。

移行にはディレクトリ構造の変更、URL リライト、プラグイン互換性の検証、ホスティング設定の見直しが必要です。その手間に見合うリターンは「ゼロから新規構築する」ケースの方が大きくなります。

  • 新規構築なら Bedrock を検討する価値が大きい
  • 既存サイトは「標準 WP + Composer のみ部分導入」が落としどころ
  • ホスティングが Composer / SSH に対応しているか事前確認(共有レンタルは厳しい場合あり)
WEBさん
WEBさん

私自身、標準 WP で構築した既存サイトを Bedrock に丸ごと移行したことはありません。新規案件で「将来 5 年以上動かす」とわかっているものは Bedrock 化を検討する、くらいの温度感が現実的だと考えています。

CI/CD パイプライン構築 (GitHub Actions 実例)

GitHub Actions による CI/CD パイプライン
GitHub Actions による CI/CD パイプライン

WordPress プロジェクトに CI/CD はオーバーキルだと思われがちです。ですが、Lint と構文チェックだけでも自動化しておくと「push したら本番が落ちる」事故を 1 段階防げます。GitHub Actions を例にミニマム構成を示します。

CI/CD で何を自動化するか

  • Lint(PHP_CodeSniffer / WPCS): コーディング規約に違反したコードをマージ前にブロック
  • 静的解析(PHPStan / Psalm): 未定義変数、型不整合をブロック
  • 構文チェックphp -l): 構文エラーをブロック(Lv1 として最も簡単)
  • ユニットテスト(PHPUnit + WP Test Suite): 機能が壊れていないか確認
  • デプロイ(rsync / SSH): main マージ時に本番反映

GitHub Actions ワークフロー例

# .github/workflows/ci.yml
name: WP CI

on:
  pull_request:
    branches: [main, develop]
  push:
    branches: [main]

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.2'
          tools: composer, phpcs
      - name: Composer install
        run: composer install --no-progress --prefer-dist
      - name: PHP Syntax check
        run: find ./wp-content/themes ./wp-content/mu-plugins -name "*.php" -print0 | xargs -0 -n1 php -l
      - name: PHPCS (WordPress Coding Standards)
        run: ./vendor/bin/phpcs --standard=WordPress wp-content/themes wp-content/mu-plugins

  deploy:
    needs: lint
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: rsync to production
        uses: burnett01/rsync-deployments@7.0.0
        with:
          switches: -avzr --delete --exclude='.git'
          path: wp-content/
          remote_path: /var/www/example.com/wp-content/
          remote_host: ${{ secrets.PROD_HOST }}
          remote_user: ${{ secrets.PROD_USER }}
          remote_key: ${{ secrets.PROD_SSH_KEY }}
YAML

secrets 管理

SSH 鍵、API キー、DB パスワードなどは GitHub Actions の Repository Secrets に保存します。コードに直書きしない、ログに出力しない、これだけは習慣として徹底します。

  • 本番 SSH 鍵は secrets.PROD_SSH_KEY として登録(Ed25519 推奨)
  • 該当鍵は本番サーバー側で authorized_keyscommand="..." 制限を付ける
  • 定期的にローテーション(半年〜1 年に 1 回)

失敗時のロールバック

自動デプロイには「失敗時に戻せる」前提を組み込みます。rsync で --delete を使うと意図しないファイル消失が起きる可能性があるため、本番側に世代バックアップを残す形が安全です。

#!/bin/bash
# サーバー側 pre-deploy フック例
# /home/deploy/pre-deploy.sh
set -e

TIMESTAMP=$(date +%Y%m%d-%H%M%S)
BACKUP_DIR=/var/backups/wp/${TIMESTAMP}

mkdir -p ${BACKUP_DIR}
rsync -a /var/www/example.com/wp-content/ ${BACKUP_DIR}/wp-content/

wp --path=/var/www/example.com db export ${BACKUP_DIR}/db.sql --add-drop-table

# 14 日より古いバックアップは削除
find /var/backups/wp -maxdepth 1 -mtime +14 -exec rm -rf {} +
Bash

バックアップ / Disaster Recovery 戦略

3-2-1 バックアップルール
3-2-1 バックアップルール

「バックアップをとっています」という言葉だけで安心しているプロジェクトは、ほぼ確実に復元したことがありません。

バックアップは取ること以上に「復元できることを定期的に確認すること」がセットで意味を持ちます。

「とっています」では足りない 3 つの理由

  • 同じサーバー上だけにある ── サーバーが壊れたら一緒に消える
  • 復元したことがない ── いざ復元するとパスや所有権の問題で動かない
  • バックアップが古い ── 最後に取れたのが半年前で気づいていない

3-2-1 ルール

バックアップ設計の古典的なフレームワークが 3-2-1 ルールです。

  • 3 コピー: 本番 + バックアップ 2 つ
  • 2 つの異なるメディア: 例えば「サーバーディスク + オブジェクトストレージ」
  • 1 つはオフサイト: 物理的に離れた場所に置く

個人事業レベルでは、UpdraftPlus などのプラグインで Google Drive / Dropbox / S3 / R2 に自動転送する形で 3-2-1 をかなりの精度で満たせます。

DB / ファイル / 設定の組み合わせ

復元に必要な要素は最低でも 3 つです。どれか 1 つ欠けると復元が完成しません。

対象推奨頻度保管期間サイズ目安
DB ダンプ日次30 日小〜中
uploads週次(差分)30〜90 日
テーマ / プラグインGit で常時永続
wp-config.php / .env変更時永続極小

復元テスト (3 ヶ月に 1 回)

3 ヶ月に 1 回程度、別環境にバックアップを実際に復元してみる「復元ドリル」を実施します。発見できる問題の例は次の通りです。

  • バックアップサイズが上限に達して途中で打ち切られていた
  • uploads/ が含まれていなかった(設定ミス)
  • 復元後にパーマリンクが効かず管理画面が 404(.htaccess を別途用意する必要あり)
  • 復元後にメール送信が本番に飛んでしまう(SMTP 設定の差し替え忘れ)

個人事業の現実解

  • UpdraftPlus 無料版 + Google Drive(DB 日次 / ファイル週次)
  • サーバー側のホスト提供スナップショット(Xserver / Wpx / ConoHa など)
  • テーマ / mu-plugin / 自作プラグインは Git(GitHub プライベートリポジトリ)
  • 四半期に 1 回、Local に本番をコピーして「動くこと」を確認

「触る前に safety net を張る」運用フロー

ここまでの仕組みがあっても、最後の事故率を下げるのは「触る瞬間の作法」です。本番に触る前に safety net (安全網) を張ってから手を動かす習慣を、チェックリストとして固定化します。

本番作業前の 8 項目チェックリスト

本番作業前チェック

  • 1. 直近のバックアップが取れていることを確認(時刻を見る)
  • 2. 同じ作業をステージングで一度通しておく
  • 3. 変更ファイル一覧を git diff で書き出す
  • 4. 影響範囲を 1 行で言語化(どのページが壊れたら困るか)
  • 5. ロールバック手順を 1 行で書く(戻し方を作業前に確定)
  • 6. 作業時間帯はアクセスが少ない時間を選ぶ
  • 7. 死活監視(UptimeRobot 等)のステータスを開いておく
  • 8. 作業ログを残す(何時何分に何をしたか)

「本番直接編集」の禁止

WordPress 管理画面の「外観 → テーマファイルエディター」「プラグイン → プラグインファイルエディター」を、運用上は実質無効化します。wp-config.php に以下を追加すると、UI 自体が消えます。

<?php
// wp-config.php に追加
// テーマ / プラグインの編集をブラウザから不可にする
define('DISALLOW_FILE_EDIT', true);

// プラグイン / テーマの追加・更新もブラウザ経由を不可にする
// (Composer / CI 経由のみに統一する場合)
define('DISALLOW_FILE_MODS', true);
PHP

DISALLOW_FILE_MODS は「管理画面からプラグイン更新もできなくする」設定です。CI/CD で更新する運用に揃えた場合のみ有効化を検討します。

観測の自動化

触る前の safety net とセットで「触った後に何かが壊れたら気づく仕組み」も必要です。最低限の組み合わせ例を挙げます。

  • 死活監視: UptimeRobot / Better Uptime / HetrixTools(1〜5 分間隔で外形監視)
  • UX 監視: Microsoft Clarity(ユーザー操作の異常を録画で確認)
  • パフォーマンス: Search Console / PageSpeed Insights(週次〜月次で確認)
  • エラーログ: WP_DEBUG_LOG を本番でも有効化し、定期的に確認

障害発生時の初動 10 分

障害を見つけた瞬間に「何をするか」を事前に決めておきます。10 分以内にやることリストです。

  • 0-2 分: 障害の事実確認(実際に開いてみる、複数端末で確認)
  • 2-4 分: 直近の変更を特定(git log / プラグイン更新履歴)
  • 4-7 分: ロールバック判断(戻すか調査優先か)
  • 7-10 分: 実行(ロールバックか暫定対処か)

「調査してから戻すか決める」のではなく「まず戻して、ステージングで原因を追う」の方が事業影響を最小化できる場面が多いです。

WEBさん
WEBさん

私の経験上、本番障害でいちばんやってはいけないのは「焦って本番でデバッグする」ことです。本番は復旧優先、原因究明はステージングで。これだけ覚えておいてください。

障害発生時のロールバック手順

障害種別ごとのロールバック判断フロー
障害種別ごとのロールバック判断フロー

ロールバックは「戻せる前提」を作っておかないと、いざというときに戻せません。状況別に「どの層を、どこまで戻すか」を整理します。

状況別ロールバック戦略

状況戻すもの手段
プラグイン更新で WSOD該当プラグインのみWP Rollback / 旧版 ZIP 差替
テーマ更新で表示崩壊テーマgit revert / バックアップから復元
独自コード反映で 500直前のデプロイgit revert + 再デプロイ
DB マイグレーションで壊れたDB 全体直前ダンプから restore
改ざんを発見第6回の手順 + 完全復元クリーン環境に再構築

プラグイン更新で崩れた場合

まず該当プラグインのみを 1 段階前の版に戻します。WP Rollback プラグインを入れておくと、管理画面から旧バージョンに戻すボタンが追加されて便利です。

# wp-cli で旧版に戻す例
wp plugin install some-plugin --version=4.2.1 --force

# 全体のプラグインバージョンを確認
wp plugin list

# 一旦無効化したい場合 (SSH 経由)
wp plugin deactivate some-plugin
Bash

DB マイグレーションで壊れた場合

DB を伴う変更は最も戻しにくい部類です。マイグレーション実行直前のダンプを必ず取り、壊れたら全体をリストアします。

# マイグレーション直前のダンプ
wp db export pre-migrate-$(date +%Y%m%d-%H%M).sql

# 壊れたら全体リストア
wp db reset --yes
wp db import pre-migrate-XXXXXX.sql

# my_db_version の戻しも忘れずに
wp option update my_db_version '1.1.0'
Bash

改ざんを発見した場合

改ざんは「ロールバック」ではなく「クリーンな環境に再構築」が原則です。理由とフローは第6回で詳しく扱いましたので、本記事では要点だけ確認します。

  • 侵入経路の特定が先(ログ保全)
  • 「壊れたファイルだけ削除」は再発の温床
  • クリーンな WP コア + 自前テーマ + 信頼できる時点のバックアップ DB で再構築
  • 全管理ユーザーの再発行 + パスワードリセット強制

「戻せない変更」を見極める

ロールバックには限界があります。次のものは「戻すと別の問題が起きる」変更です。事前に判別する習慣を持ちます。

  • 新しい投稿・コメント・注文データ(戻すと顧客データを失う)
  • 外部サービスとの不可逆連携(メール送信済み、決済確定など)
  • マイグレーション後のスキーマ変更(旧版コードが新スキーマに対応しない)

判断のコツ

「戻したとき、新しく入った顧客データはどうなるか」を先に考えると、ロールバックすべきか前進修正すべきかの判断が早くなります。コード起因の問題はロールバックが速い、データ起因の問題は前進修正の方が安全、というのが目安です。

シリーズ完結 — 7 + 8 で完成する「複雑 WP を長期運用する」設計

ここまで、上級者シリーズ第8回として「壊さない WordPress」の運用設計を 11 個のテーマで見てきました。シリーズの締めくくりとして、第7回(構築)と第8回(運用)を組み合わせたときに何が完成するのか、そして読者のみなさんが次に何を学ぶとよいのかを書き残します。

第7回 (設計) と第8回 (運用) のセット

第7回では「WordPress を Web アプリとして組み立てる設計」を扱いました。CPT、Role、REST、外部 API、複雑な要件を WordPress で受けるための構造設計です。第8回はその逆側、「組み上げた WordPress を、長期にわたって壊さず動かす運用設計」を扱いました。

テーマ問い
第7回構築設計どう組み立てるか
第8回運用設計どう壊さず保つか

この 2 回は対になる関係です。組み立てる力だけだと「最初は動くが、半年後に誰も触れない」サイトになります。運用する力だけだと「壊さないが、要件に合わせて伸ばせない」サイトになります。両方が揃ったとき、WordPress は 業務システムとして長期で使える土台 になります。

全 8 回の俯瞰

上級者シリーズの全 8 回を振り返ります。

  • 第1回: WordPress を「中規模 Web アプリ」として扱う発想
  • 第2回: テーマ / プラグインの境界線設計
  • 第3回: パフォーマンスチューニング (キャッシュ層と DB の話)
  • 第4回: REST API と外部連携
  • 第5回: DB と wp_options の構造設計
  • 第6回: 改ざんと復旧 (フォレンジック視点)
  • 第7回: WordPress を Web アプリとして組む
  • 第8回 (本記事): 壊さない WordPress / Git・ステージング・マイグレーション戦略

第1回〜第6回が「個別の技術領域」、第7回・第8回が「それを束ねる設計と運用」という位置づけです。

読者への提言 — 3 つの実装ステップ

本記事をここまで読んでくださった方が、月曜の朝から実行できる最小ステップを 3 つに絞ります。

  • Step 1: wp-config.php に DISALLOW_FILE_EDIT を追加し、本番直接編集を物理的に塞ぐ
  • Step 2: 自作テーマと mu-plugin を Git 管理にする (.gitignore は本記事のものを流用)
  • Step 3: 月末カレンダーに「復元ドリル」の予定を入れる (年 4 回でよい)

この 3 つだけで、本番事故の発生確率はかなり下がります。CI/CD や Bedrock はその後でも遅くありません。

次に学ぶべきこと

上級者シリーズの「外側」として学ぶ価値が高い領域を、最後に推薦として残します。

  • Web セキュリティ全般: WordPress に閉じず OWASP Top 10 / CSP / SameSite Cookie / SSRF など
  • Docker と Linux 運用: WordPress を超えてサーバーレイヤーを自分で触れること
  • PHP 標準ライブラリ / モダン PHP: WordPress を「PHP プロジェクト」として扱えるようになる
  • ストリーミング処理 / キュー: Action Scheduler / BullMQ など、重い処理を WP の外に逃がす設計
  • 監視・可観測性 (Observability): ログ / メトリクス / トレースの基本

WordPress を深く理解した次のステップは、ほとんどの場合「WordPress の外」にあります。WordPress の中にとどまり続けるよりも、CMS としての WordPress を相対化できる視点を持つ方が、結果として WordPress を上手く使えるようになります。

WEBさん
WEBさん

私自身、ここで書いてきたことは、過去の本番障害で何度も叱られながら身につけてきたことです。同じ事故を踏まなくて済むように、この 8 回が誰かの遠回りを減らせれば、書いた意味がありました。

最後に

上級者シリーズ全 8 回を通読いただき、本当にありがとうございました。「組み立てる WordPress」(第1〜7回) と「壊さない WordPress」(第8回) が一本の線でつながったとき、複雑な要件を抱えた WordPress プロジェクトも長期にわたって維持できるようになります。

本シリーズは「WordPress で複雑なものを作れるが、壊さず動かす自信がない」という方に向けて書き始めました。読了後にこの記事に書かれた内容のうち 1 つでも明日からの運用に取り入れていただければ、書き手としてはそれ以上に嬉しいことはありません。

本記事や本シリーズへの疑問点・改善提案がありましたら、お問い合わせフォームよりお気軽にお寄せください。みなさんの WordPress 運用が、これからも壊れず続いていくことを願っています。

記事の手順で解決しなかったら

本記事は自力解決を想定した内容ですが、症状によっては専門家への相談が早道です。状況に応じて下記窓口をご利用ください。

🚨 緊急復旧 (24h)

「いま、サイトが落ちている」「真っ白で何も操作できない」

緊急復旧の窓口 →

🛠️ WordPress 復旧

フォレンジック対応 — 原因特定から再発防止まで一括対応

復旧サービス →

⚙️ 保守プラン

壊れる前に防ぐ — 月額の更新監視・改ざん検知

保守プラン →