コンテンツにスキップ

WordPress VPS デプロイメントガイド (Ansible + Cloudflare Tunnel)

概要

この文書では、WebArena Indigo の VPS(Ubuntu)へ WordPress 環境をデプロイし、Cloudflare Tunnel 経由で公開するための手順を説明します。

構成

  • ホスト: WebArena Indigo VPS (Ubuntu 24.04 推奨)
  • 公開方式: Cloudflare Tunnel (cloudflared)
  • 実行基盤: Docker Compose (WordPress, MariaDB, Socat)
  • プロキシ: Vendure Server (Fly.io) が WordPress の GraphQL エンドポイントをプロキシ
  • 自動化: Ansible + Justfile

環境情報

環境 IP アドレス 公開ドメイン
Staging 116.80.84.4 https://cms-staging.ritsubi-platform.com
Production 116.80.84.2 https://cms.ritsubi-platform.com

前提条件

必要なツール

  • Ansible: mise exec ansible -- ... 経由で使用可能
  • Just: タスクランナー
  • SSH 接続: 手元で通常の ssh ubuntu@<ip> が通る OpenSSH 設定があること。 just wp-*-vps / Ansible はその設定を正として利用し、必要時は ~/.ssh/agent-environment を自動読込する

VPS SSH 接続情報

環境 IP アドレス SSH ユーザー デプロイパス
Staging 116.80.84.4 ubuntu /opt/wordpress
Production 116.80.84.2 ubuntu /opt/wordpress

Ansible インベントリ: infra/ansible/inventory/{staging,production}.yml

ステップ 1: インフラ構成の定義 (Terraform)

Cloudflare Tunnel および DNS レコードは Terraform で管理されています。

  • 設定ファイル: infra/terraform/cloudflare-apps/ritsubi-platform.com/tunnels.tf

[!NOTE] 本リポジトリの Ansible は /etc/systemd/system/cloudflared.service を直接管理し、WordPress VPS では cloudflared tunnel --protocol http2 run --token ... を既定にしています。Production で QUIC の timeout: no recent network activity が断続的な 502 を起こしたため、CMS 用 Tunnel は HTTP/2 を正とします。

新しいトンネルを作成した場合は、生成された tunnel_token を AWS Secrets Manager に登録する必要があります。

ステップ 2: 環境変数の設定 (AWS Secrets Manager)

WordPress 運用に必要な変数を AWS Secrets Manager に設定します。

環境 SSM パス SECRETS_CONFIG
Staging b2b-ecommerce/staging/wp staging_wp
Production b2b-ecommerce/prod/wp production_wp

主要な変数:

  • WORDPRESS_DB_PASSWORD: データベースパスワード
  • WP_ADMIN_USER / WP_ADMIN_PASSWORD: 初期管理者情報
  • CLOUDFLARE_TUNNEL_TOKEN: Cloudflare Tunnel の接続トークン(後述)
  • STOREFRONT_URL: 連携先フロントエンドの URL(server-to-server で叩く canonical origin。production は https://order.ritsubi-platform.com
  • STOREFRONT_PUBLIC_URL: WP 管理画面の preview / パーマリンクで エンドユーザーに露出させる公開 alias。未設定なら STOREFRONT_URL を流用。 production は https://medical.ritsubi.co.jp を設定する
  • VENDURE_BASE_URL: Vendure API の URL
  • SENTRY_DSN: WordPress PHP runtime から送る Sentry DSN
  • SENTRY_ENVIRONMENT: staging / production の environment 名
  • SENTRY_RELEASE: 任意。例: wordpress@<git-sha>
  • SENTRY_SEND_DEFAULT_PII: 任意。true の場合のみユーザー/IP を送信
  • WORDPRESS_BROWSER_SENTRY_DSN: 任意。WordPress browser runtime を別 DSN に分けたい場合に使用
  • WORDPRESS_BROWSER_SENTRY_ENVIRONMENT: 任意。未設定時は SENTRY_ENVIRONMENT を流用
  • WORDPRESS_BROWSER_SENTRY_RELEASE: 任意。未設定時は SENTRY_RELEASE を流用
  • WORDPRESS_BROWSER_SENTRY_SEND_DEFAULT_PII: 任意。未設定時は SENTRY_SEND_DEFAULT_PII を流用

just deploy-wp-staging / just deploy-wp-production は、AWS Secrets Manager の staging_wp / production_wp から取り出した SENTRY_* を VPS 上の /opt/wordpress/.env へそのまま反映します。WordPress 側ではこの .env を正として runtime の Sentry 設定を読み込みます。

ステップ 3: デプロイの実行

プロジェクトルートの just レシピを使用してデプロイを行います。

フルデプロイ (OS 設定・パッケージ込)

初回デプロイや、Docker/cloudflared 自体の設定変更時に使用します。

# Staging
just deploy-wp-staging

# Production
just deploy-wp-production

just wp-deploy-vps / just deploy-wp-* は、完了後に just wp-verify-vps を自動実行します。さらに verify=true のままなら just wp-drift-audit-vps <env> remote-deploy も続けて実行し、plugin / ACF / option / URL など deploy で壊してはいけない構成を read-only 監査します。remote-deploy は通常 deploy で seed しない固定ページ不足を deploy 失敗扱いにしません。 固定ページを含む live CMS state の厳格監査は、別途 just wp-drift-audit-vps <env>(既定 remote)で行います。一時的に外形確認を省略したい場合のみ verify=false を明示してください。

通常の VPS deploy では WordPress の bootstrap / seed-data を実行しません。 bootstrap は固定ページ、バナー、ストア設定、ランキングなどの CMS content / option を seed 値へ更新するため、production で通常 deploy と同時に走らせると運用中の CMS 内容を上書きします。一度 remote WordPress が稼働した後の CMS state は backup / restore を正本にし、staging / production の deploy 導線には bootstrap task を置きません。 bootstrap.sh 側も remote CMS での実行を拒否します。

apps/wordpress-cms/ の code sync では /opt/wordpress/.env/opt/wordpress/uploads を配備物として扱いません。.env は Secrets Manager 正本から env task が生成する専用ファイルであり、uploads は production の wp-content/uploads に bind mount される media local cache です。 deploy の code フェーズではどちらも明示的に除外されます。repo 側の local 用 .envuploads が VPS に混入した場合は role が fail するため、手動 rsync を行う場合も同じ前提を崩さないでください。

手動 rsync が必要な場合は、apps/wordpress-cms/deploy-rsync-filter.txt を 除外ルールの正本として再利用してください。Ansible も同じ filter を読むため、 このファイルを迂回して独自の rsync 引数を書くと、.env 混入や media local cache 上書きの再発経路になります。/opt/wordpress/ への 2 段目の rsync でも .env / db-data / wordpress-data / uploads を exclude し、--delete 対象に runtime state を混ぜません。

フルデプロイ(infra を含む実行)では、次の安定化設定も同時に反映されます。

  1. VPS に 1GiB の swapfile を作成し、vm.swappiness=10 を永続化する。
  2. WordPress コンテナに healthcheck を設定し、vendure-proxyunless-stopped で自動復帰させる。
  3. Cloudflare Tunnel 配下のため、VPS の 8181/tcp は外部公開せず UFW で閉じる。
  4. Production では ritsubi-wordpress-backup.timer を配備し、DB backup を R2 へ毎時保存する。

差分更新 (コード・環境変数のみ)

日々のソースコード更新や .env の変更を高速に反映したい場合に使用します。

# Staging
just update-wp-staging

# Production
just update-wp-production

運用・メンテナンス

状態確認

VPS 上のコンテナ稼働状態を確認します。

just wp-status-vps staging

just wp-verify-vps <env> は外形確認だけでなく、次もまとめて確認します。

  1. cloudflared が active で --protocol http2 になっていること。
  2. /swapfile が有効で vm.swappiness=10 になっていること。
  3. 8181/tcp が UFW で遮断され、db / wordpress / vendure-proxy が期待状態で稼働していること。
  4. 公開 CMS の wp-login.php200post-new.php?post_type=product_detail が未ログイン時 302 / 303 を返すこと。

ログ確認

特定のサービスのログをリアルタイムで表示します。

# wordpress サービスのログを表示
just wp-logs-vps wordpress staging

# live WordPress drift を監査
just wp-drift-audit-vps staging

[!TIP] just wp-drift-audit-vps <env>SSH で wp-cli を叩くコマンドではありません
WORDPRESS_ENDPOINTCMS_API_TOKEN を使って、live WordPress の /wp-json/ritsubi/v1/drift-audit を read-only で呼び出します。
そのため、drift 監査だけなら GitHub Actions や手元の shell に SSH 鍵は不要です。

SSH ログイン

トラブルシューティングのために VPS へ直接ログインします。

just wp-ssh staging

[!NOTE] リポジトリ標準の just wp-*-vps / Ansible は、手元で通常の ssh ubuntu@<ip> が通る OpenSSH 設定を正として利用します。現在は -i の固定指定を避け、必要時のみ ~/.ssh/agent-environment を自動読込するため、普段使っている agent / RSA fallback / Host 設定と一致した経路で接続できます。

[!IMPORTANT] ただし drift 監査だけは例外です。just wp-drift-audit-vps は SSH ではなく認証付き HTTP endpoint を使います。SSH が必要なのは wp-ssh / wp-cli-vps / wp-logs-vps などの server-side 操作です。

WP-CLI の実行

VPS 上の WordPress に対して wp-cli コマンドを直接実行できます。 production では wp-cli-vps が read-only と判定できない WP-CLI command の前に ritsubi-wordpress-backup.service を実行します。backup が失敗した場合、本操作には進みません。

# プラグイン一覧を表示
just wp-cli-vps "plugin list" staging

# ユーザー一覧を表示
just wp-cli-vps "user list" production

# production DB を変更するコマンドは直前 backup 後に実行される
just wp-cli-vps "option update blogdescription 'updated by maintenance'" production

複数行の PHP や => を含む配列を ad-hoc で実行したい場合は、inline の php -r / wp evalssh 経由で直書きせず、file-based helper を使います。 production で wp-php-vps-file を使う場合も、PHP の DB 書き込み有無を静的判定しないため 常に直前 backup を取ります。

# wp-load.php を自動読込した状態で PHP ファイルを実行
just wp-php-vps-file path/to/script.php production

[!WARNING] => を含む PHP 断片を shell の多重 quoting に載せると、> がリダイレクトとして解釈されてリポジトリ root にゼロバイトファイルを生成することがあります。一時調査やデータ補正でも、inline 実行ではなく wp-php-vps-file を使ってください。

DB バックアップ (production)

production VPS では ritsubi-wordpress-backup.timer毎時 :15 UTC で WordPress DB backup を実行し、Cloudflare R2 バケット ritsubi-ecommerce-backupwordpress/production/db/ へ保存します。

保持期間は 720 時間(30 日)です。

前提:

  1. production_infraBACKUP_R2_* を解決できること
  2. media は cms-assets.ritsubi-platform.com 側 R2 offload を正本とし、定期 backup は DB only であること

確認・手動実行:

# timer の状態確認
just wp-backup-status-vps production

# 手動で 1 回実行
just wp-backup-run-vps production

# 直近ログを直接確認
ssh ubuntu@116.80.84.2 "sudo journalctl -u ritsubi-wordpress-backup.service -n 120 --no-pager"

期待する状態:

  1. ritsubi-wordpress-backup.timeractive (waiting)
  2. ritsubi-wordpress-backup.service が直近成功している。
  3. journal に Uploaded WordPress backup to s3://.../wordpress/production/db/... が出ている。

AccessDenied が出る場合の復旧手順:

  1. AWS Secrets Manager b2b-ecommerce/prod/infraBACKUP_R2_ACCESS_KEY_ID / BACKUP_R2_SECRET_ACCESS_KEY を更新する。
  2. just wp-deploy-vps production backup verify=false で VPS 上の /etc/ritsubi/wordpress-backup.env を再配布する。
  3. just wp-backup-run-vps production を再実行し、journal に Uploaded WordPress backup to ... が出ることを確認する。

Cloudflare Tunnel 502 の切り分け

cms.ritsubi-platform.com / cms-staging.ritsubi-platform.com502 Bad Gateway が断続的に出る場合、まず WordPress 本体ではなく cloudflared の transport を確認します。今回の実障害では WordPress/Apache は 200 を返しており、cloudflared の QUIC timeout: no recent network activity が直接原因でした。

確認観点:

  1. WordPress コンテナ自体は稼働しているか(just wp-status-vps production)。
  2. journalctl -u cloudflared に QUIC timeout / reconnect が出ていないか。
  3. systemctl cat cloudflaredExecStart--protocol http2 を含むか。
  4. 変更が repo に入っていても既存ホストへ未反映の可能性があるため、staging / production それぞれへ cloudflared role を再適用したか。

確認例:

# Production の tunnel ログ
ssh ubuntu@116.80.84.2 "sudo journalctl -u cloudflared --since '-30 minutes' --no-pager | tail -n 120"

# systemd unit の実際の起動コマンド
ssh ubuntu@116.80.84.2 "sudo systemctl cat cloudflared"

# cloudflared role の再適用
just wp-deploy-vps production cloudflared
just wp-deploy-vps staging cloudflared

# cloudflared と公開導線の簡易検証
just wp-verify-vps production
just wp-verify-vps staging

期待する状態:

  1. cloudflaredSettings / Initial protocol / Registered tunnel connectionhttp2 を示す。
  2. wp-admin/post-new.php?post_type=product_detail などの CMS 管理 URL が 502 ではなく、未ログイン時は 302、ログイン済みでは 200 を返す。
  3. 8181/tcp は VPS へ直接公開されず、Cloudflare Tunnel 経由だけで到達できる。
  4. WordPress 側の access log に 5xx が無いのにブラウザで 502 が出る場合は、引き続き tunnel 層を疑う。

定期監視

.github/workflows/scheduled-safe-probes.yml は 30 分ごとに staging / production の Vendure・Storefront に加えて、WordPress 公開 CMS 導線も確認します。

確認対象:

  1. https://cms-*.ritsubi-platform.com/wp-login.php200
  2. https://cms-*.ritsubi-platform.com/wp-admin/post-new.php?post_type=product_detail が未ログイン時 302 / 303
  3. 失敗時は既存の Slack 通知ジョブが起動する。

Media Offload の確認

Cloudflare R2 への media offload は、Secrets Manager に値があるだけでは成立しません。 staging_wp / production_wpWORDPRESS_R2_*VPS 上の /opt/wordpress/.env とコンテナ環境へ反映されていること が前提です。

media offload の詳細手順、既存メディアの一括移行、R2 実体確認、トラブルシュートは wordpress-media-offload.md を正本とします。

確認観点:

  1. AWS Secrets Manager に WORDPRESS_R2_ACCESS_KEY_ID, WORDPRESS_R2_SECRET_ACCESS_KEY, WORDPRESS_R2_BUCKET_NAME, WORDPRESS_R2_ENDPOINT, WORDPRESS_R2_REGION が存在すること。
  2. 反映後の VPS 上で /opt/wordpress/.envWORDPRESS_R2_* が書き出されていること。
  3. WordPress コンテナ内で WORDPRESS_R2_* が見えていること。
  4. wp-cliAS3CF_SETTINGS が定義されていること。
  5. WP Offload Media の delivery-providerstorage のままではなく、cloudflare など custom domain を許可する provider になっていること。
  6. 添付ファイルの URL が期待する offload 配信先を向いていること。

確認例:

# VPS 上の env ファイルに R2 設定が反映されているか
ssh ubuntu@116.80.84.4 "sudo grep '^WORDPRESS_R2' /opt/wordpress/.env"

# WordPress コンテナに R2 環境変数が入っているか
ssh ubuntu@116.80.84.4 "sudo docker compose -f /opt/wordpress/docker-compose.yml exec -T wordpress printenv | grep '^WORDPRESS_R2'"

# AS3CF_SETTINGS が定義されているか
just wp-cli-vps "eval 'echo defined(\"AS3CF_SETTINGS\") ? AS3CF_SETTINGS : \"AS3CF_SETTINGS_NOT_DEFINED\";'" staging

# 配信 provider / delivery-domain を確認
just wp-cli-vps "option get tantan_wordpress_s3" staging

# 有効プラグインの確認
just wp-cli-vps "plugin list --status=active" staging

[!IMPORTANT] WordPress の media offload では、provider=aws のままでも R2 への保存自体はできますが、配信用の delivery-provider が既定の storage のままだと custom domain を使えません。実運用では delivery-provider=cloudflare とし、delivery-domaincms-assets.ritsubi-platform.com / cms-assets-staging.ritsubi-platform.com に設定してください。 [!IMPORTANT] staging_wp / production_wpWORDPRESS_R2_* が存在していても、VPS 上の /opt/wordpress/.env に出てこない場合は反映漏れです。 just update-wp-staging / just update-wp-production を実行して env を再同期し、その後に再確認してください。 [!IMPORTANT] stagingproduction の WordPress env 反映は同時に走らせず、原則 1 環境ずつ順番に実行してください。環境ごとの一時 env ファイルが混線すると、誤った環境変数が別環境へ反映される事故につながります。 [!CAUTION] docker compose 実行時に The "<name>" variable is not set. Defaulting to a blank string. という警告が出る場合、VPS 上の .env 内で $ を含む値が Compose の変数展開対象になっている可能性があります。Secrets の値が欠落したように見える場合は、 /opt/wordpress/.env の生成内容も併せて確認してください。

既存メディアの一括 Offload

新規アップロードだけでなく、既存の attachment をまとめて R2 へ移したい場合は、 offload-existing-media.phpwp-cli eval-file で実行します。

# Staging
just wp-cli-vps "eval-file /var/scripts/offload-existing-media.php" staging

# Production は wp-cli-vps の guard が直前 DB backup を取ってから実行する
just wp-cli-vps "eval-file /var/scripts/offload-existing-media.php" production

期待する結果:

  1. OK <attachment_id> が並ぶ
  2. 最後に {"processed":...,"skipped":...,"failed":0} が出る
  3. media_counts()not_offloaded0 になる
  4. REST / GraphQL の source_urlcms-assets.* を返す

[!NOTE] このスクリプトは既に offload 済みの attachment を skip し、未 offload 分だけ wp_update_attachment_metadata() を明示実行して R2 へ送ります。

セキュリティ設計

  1. ポートの閉鎖: Cloudflare Tunnel を使用しているため、外部からポート 8181 を開放する必要はありません。Ansible が UFW を自動設定し、外部からの直接アクセスを遮断します。
  2. 環境変数の最小化: デプロイ時には、WordPress に必要な変数のみが抽出され、VPS 上の .env に書き出されます。AWS 認証情報などは VPS 上には保存されません。
  3. 初期化の分離: bootstrap は local/dev の再現性を作るための導線です。remote CMS の state は backup / restore を正本にし、deploy では初期化や seed を実行しません。