コンテンツにスキップ

Deploy Runbook

Vendure / Dashboard / Storefront を staging / production へ deploy する際の事前準備、想定エラー、復旧コマンドを 1 ページに集約する。背景の incident は #848 などを参照。

TL;DR (定常運用)

目的 コマンド
全面 (Vendure + Dashboard + Storefront) を local から deploy just deploy-all <env>
Vendure backend + Dashboard だけ deploy just deploy-vendure-with-migration <env>
production へ 5 分以内に反映 (build 済み image 昇格のみ) just fast-deploy-production
Storefront だけ deploy just cloudflare-deploy-storefront <env>
Storefront + Dashboard を prod-preview へ deploy Deploy Prod Preview workflow
全面 (staging) を CI から起動 just manual-deploy-staging
全面 (production) を CI から起動 (main 再配信) just manual-deploy-production
WordPress CMS plugin を VPS へ配信 just wp-deploy-vps <env>

manual-deploy-staging などの just recipe 引数は positional で渡す。just manual-deploy-staging ref=develop ... のように recipe 名の後ろへ key=value を置くと、その文字列が ref として渡され GitHub Actions dispatch が No ref found for: ref=develop で失敗する。明示指定する場合は次の形を使う:

just manual-deploy-staging develop true true true true

deploy-vendure-with-migration は drift audit (--allow-pending-migrations) → image build → Fly deploy → Dashboard deploy → post-deploy verify を一括する。新 migration / 新 entity を含む deploy では必ずこちらを使う (通常の manual-deploy-vendure は drift で abort する)。

deploy-all は (1) Vendure backend + Dashboard → (2) Storefront の順で deploy し、Vendure migration 完了を待ってから Storefront を Cloudflare へ反映する。deploy_storefront=false / deploy_dashboard=false で個別 skip 可能。

Production 5 分 fast lane

production へ手動で 5 分以内に反映したい場合は、CI / staging で build 済みの Vendure image を昇格する fast-deploy-production を使う:

just fast-deploy-production

この入口は既定で origin/main を対象にし、main 未到達 ref は FAST_DEPLOY_PRODUCTION_ALLOW_NON_MAIN=true を明示しない限り拒否する。さらに MANUAL_DEPLOY_SLA_SECONDS=300DEPLOY_REQUIRE_PREBUILT_IMAGE=true を既定で付ける。production 用 image が既にある場合はそれを使い、無い場合は同一 SHA の staging image を production tag へ registry copy してから deploy する。どちらも見つからない場合は ローカル Docker build へ fallback せず abort する。5 分以内反映の契約を守るため、build が必要な変更は先に staging / CI で image を作る。

明示 image を昇格する場合:

just fast-deploy-production "" registry.fly.io/ritsubi-ecommerce:prod-<sha>

Dashboard 変更を含まない backend-only hotfix では Dashboard deploy を外せる:

just fast-deploy-production "" "" false

新 migration / 新 entity を含む場合も fast lane は allow_pending_migration=true で drift audit を通す。ただし release phase で実 migration が走り、schema 変更があると production backup が必要になるため、5 分を超える可能性がある。その場合は SLA 警告を本物の signal と扱い、次回以降は事前 backup または通常の deploy-vendure-with-migration production に戻す。

Deploy 面 inventory

Surface Where deployed Local recipe deploy-all 同梱
Vendure app + worker Fly (ritsubi-ecommerce / -staging) just fly-deploy-vendure
React Dashboard Cloudflare Workers (vendure-dashboard*) nx run ritsubi-vendure-server:dashboard:deploy:<env>
Storefront Cloudflare Workers (ec-storefront*) just cloudflare-deploy-storefront <env>
Storefront Prod Preview Cloudflare Worker (ec-storefront-prod-preview) just cloudflare-deploy-storefront prod-preview ❌ (release 前 lane)
Dashboard Prod Preview Cloudflare Worker (vendure-dashboard-prod-preview) nx run ritsubi-vendure-server:dashboard:deploy -- prod-preview ❌ (release 前 lane)
Storefront Preview Cloudflare Worker (ec-storefront-preview) nx run ritsubi-storefront:cloudflare:deploy:preview ❌ (PR 用)
WordPress CMS plugin VPS just wp-deploy-vps <env> ❌ (別 cadence)
Fly secrets sync Fly (bundled in deploy-fly) just sync-fly-secrets <env> ✅ (内側で実行)
Fly Redis password sync Fly Redis app (ritsubi-redis-*) just sync-fly-redis-password <env> ✅ (内側で実行)
CF maintenance KV namespace Cloudflare (one-time) just cloudflare-maintenance-kv-create <env> – (初回 setup)
CF R2 buckets Cloudflare (one-time) just cloudflare-r2-cache-bucket-create <env> – (初回 setup)

WordPress は別 git ref / 別 release cadence で配信するため deploy-all には含めない。WP の plugin / theme 変更時は明示的に just wp-deploy-vps を叩く (docs/03-implementation/infrastructure/wordpress-drift-runbook.md 参照)。

Pre-flight (recipe が自動で行う)

scripts/ops/manual-deploy-vendure.sh の Phase 0 で:

  1. Advisory lock: /tmp/ritsubi-deploy-locks/<env>.lock で同一環境への並列 deploy を直列化。既に他 process が deploy 中なら早期 abort して Fly registry / machine update 競合を防ぐ。PRE_DEPLOY_SKIP_LOCK=true で skip 可
  2. Auth pre-flight: Fly (flyctl auth whoami) / AWS Secrets Manager (aws sts get-caller-identity) / Cloudflare (with-env.sh 経由で CLOUDFLARE_API_TOKEN 取得) を 1 秒以内で probe。遅い phase (image build / Dashboard deploy) で auth 切れに当たって巻き戻すリスクを排除PRE_DEPLOY_SKIP_AUTH_CHECK=true で skip 可
  3. 非互換 migration 検知: production + bluegreen 戦略時に直近 24h の migration を scan し DROP COLUMN / ALTER COLUMN ... NOT NULL 等の pattern を検出したら warning。DEPLOY_FLY_STRATEGY=canary への切替を促す。PRE_DEPLOY_SKIP_MIGRATION_SCAN=true で skip 可、PRE_DEPLOY_MIGRATION_SCAN_STRICT=true で abort 化
  4. plugins dist precheck: packages/plugins/dist/index.js を確認し、欠落していれば nx reset + pnpm -C packages/plugins build を実行
  5. Fly machine settle 待ち: flyctl secrets set 等で transitional な machine が残っていないか polling (最大 90 秒)。PRE_DEPLOY_SKIP_MACHINE_SETTLE=true で抑止可
  6. production backup: BACKUP_POSTGRES_PRODUCTION_SKIP=true を deploy-fly へ transit して二重 backup を避ける
  7. drift audit: --fail-on-driftallow_pending_migration=true (with-migration recipe) では reconcilable drift を warn 化

これらは過去 incident で実際にハマったポイントなので、手動で実行する手順を runbook 化していない。skip するには各々の env を export する。

Vendure staging が deploy_dashboard=false でも長い場合は、Dashboard ではなく backend image build/push を疑う。build-vendure-image.sh は CI で Fly registry cache と GitHub Actions cache を使い、local では registry cache read-only + filesystem cache mode=min を既定にする。詳細な 切り分けは deployment-guide.md の「staging manual deploy が image build/push で待つ場合」を参照し、 local log は ${TMPDIR:-/tmp}/ritsubi-vendure-image-build/、CI log は vendure-image-build-<env>-<sha> artifact の .summary.json を見る。

Deploy strategy 全体像

各 surface の deploy 戦略と zero-downtime 保証を以下にまとめる。「blue-green」は Fly Machine の strategy = "bluegreen" を指し、Cloudflare Workers は仕組み上の atomic switch で同等の zero-downtime を実現する。

Surface 戦略 zero-downtime 並走期間 設定の正本
Vendure production (Fly) bluegreen あり (全 machine 並走 → health 後 swap) apps/vendure-server/fly.toml [deploy]
Vendure staging (Fly) rolling (max_unavailable=1) △ (短時間瞬断) なし (1 台運用) apps/vendure-server/fly.staging.toml [deploy]
React Dashboard (CF Workers) atomic switch (CF default) なし (新 version 即時 100%) apps/vendure-server/wrangler.jsonc
Storefront (CF Workers) atomic switch (CF default) なし (新 version 即時 100%) apps/storefront/wrangler.toml
Prod Preview Workers atomic switch + Cloudflare Access なし deploy-targets.sh + Access application
Storefront Preview atomic switch なし 同上 (--env preview)
WordPress plugin (VPS) rsync + WP-CLI なし (in-place 上書き) just wp-deploy-vps

ポイント:

  • 純粋な blue-green (旧新並走 → swap) は Vendure production の Fly Machine だけ
  • Vendure production は Fly の bluegreen に加え、manual-deploy-vendure の deploy safety gate で新 release の Machine / process group / Fly checks / public /health/ready / public /version を二重検証する。さらに切替前に target image tag / build metadata / 現行 production health を確認し、 provenance mismatch や degraded current production は flyctl deploy 前に止める。 自動 rollback は追加の production switch を発生させるため既定 off
  • この保証は deploy wrapper 経由の production deploy に対する fail-closed contract。Fly が traffic swap を担当するため「一瞬も traffic が向かない」ことを repo 側だけで保証するものではないが、 health / version が target SHA に収束しない deploy は成功扱いにしない
  • Cloudflare Workers は新 version upload → routing を一括差し替えする atomic switch。並走期間は無いが、deploy の瞬間に traffic が切り替わるため実用上は zero-downtime
  • CF Workers にも gradual deployments (10% → 50% → 100% の段階配信) と versions API が存在するが、本プロジェクトでは未採用。必要になったら deploy-runbook の改修と一緒に検討する
  • prod-preview は段階配信ではなく hidden lane です。production traffic は流さず、 Cloudflare Access 保護 host で未リリース Storefront / Dashboard build を production backend に接続して検証します。
  • prod-preview は release candidate の短時間確認用です。Cloudflare Worker / route / Access / KV は維持してよいが、追加の Vendure Fly app / machine / DB は作らない。 Fly cost を増やさないため、backend は常に現行 production Vendure を使う。
  • prod-preview の deploy target は prod-preview だが、AWS Secrets Manager は production secrets を読む。GitHub Actions の OIDC subject も production trust に 揃えるため、deploy/smoke job の GitHub Environment は production を使い、 Worker target / public URL / smoke environment は prod-preview のまま維持する。
  • release 後に prod-preview を閉じる場合は Cloudflare 側だけを止める。厳密に閉じたい時は order-prod-preview.ritsubi-platform.com/* / dashboard-prod-preview.ritsubi-platform.com/* の route を削除し、次回 release 前に再作成する。production Vendure / WordPress / DB は触らない。
  • WordPress plugin は in-place 上書きで完全な zero-downtime にはならない。dev/staging で動作確認した上で短時間メンテ枠を取る運用

blue-green を一時的に canary へ切り替える

DROP COLUMN NOT NULL や非互換 enum 変更など、deploy 中に新旧 machine の混在で fail する migration を含むときは Vendure production の blue-green を canary に倒す:

DEPLOY_FLY_STRATEGY=canary just deploy-vendure-with-migration production

DEPLOY_FLY_STRATEGYcanary|bluegreen|rolling|immediate を受け付ける。fly.toml[deploy] strategy は触らず、env override で 1 回限りの切替を実現する。

CF Workers gradual deployments を有効化したい場合 (未採用)

Storefront / Dashboard も blue-green 相当に並走させたい場合は Cloudflare gradual deployments を導入する。具体的には:

  1. wrangler deploy --version で version だけ作成 (traffic は割り当てない)
  2. wrangler versions deploy <version_id>@10% <prev_version_id>@90% で段階配信
  3. 各段で /health/ready 等を polling、問題なければ 100% に進める

gradual deployments を採用する判断は (a) Storefront の SSR が複雑な runtime invariant を持つ、(b) Dashboard 側で破壊的 UI 変更を含む、いずれかが発生したタイミングで再評価する。

想定エラーと復旧手順

1. Cannot find module '@ritsubi/plugins/dist/*.js'index.js / rule-engine/shipping.js 等)

症状: ローカルの Dashboard build フェーズ、または CI の pre-deploy drift auditnx run ritsubi-vendure-server:drift:auditnode dist/scripts/audit-runtime-drift.js)で失敗。@ritsubi/plugins/dist/index.js@ritsubi/plugins/dist/rule-engine/shipping.js のような plugins の dist サブモジュールが解決できない。

原因: nx ビルドキャッシュが、plugins の source リファクタ(ファイル移動 / 分割 / 拡張子変更)より前の stale な packages/plugins/dist を "build fresh" 扱いで復元し、tsc を再実行せず古い dist を供給する。CI では @ritsubi/plugins:build [remote cache]Nx read the output from the cache instead of running the command for N out of N tasks がログに出る。packages/plugins/dist は git untracked なので必ず CI / local の nx cache 側の問題(commit 由来ではない)。

復旧(ローカル):

pnpm exec nx reset
pnpm -C packages/plugins build
ls packages/plugins/dist/index.js  # 存在することを確認

復旧(CI / deploy): ローカルの nx reset は効かない。develop scope の poisoned nx GitHub Actions cache を bust する。deploy ログの Cache restored from key: Linux-X64-nx-ref-... のキーを確認して削除する:

gh api -X DELETE "repos/{owner}/{repo}/actions/caches?key=<その nx-ref key>"
# もしくは develop scope の nx-ref cache を id 指定で一掃
gh api --paginate "repos/{owner}/{repo}/actions/caches?ref=refs/heads/develop&per_page=100" \
  | jq -r '.actions_caches[]|select(.key|test("nx-ref"))|.id' \
  | xargs -I{} gh api -X DELETE "repos/{owner}/{repo}/actions/caches/{}"

削除後に deploy を再実行すると cache miss → tsc -p の full build で正しい dist が再生成される。codeql 等の巨大 cache に押されて GitHub LRU が nx cache を自然 evict すると、削除なしでも次回 deploy で解消することがある。

manual-deploy-vendure.sh の pre-flight は local の packages/plugins/dist を自動復旧するが、CI 経由 deploy の drift audit はこの cache poisoning で再発し得る(2026-06 の rule-engine リファクタで発生)。恒久的な再発防止が必要なら drift audit を --skip-nx-cache で常に fresh build にする選択肢がある(deploy ごとに plugins 再 compile 数秒のコスト)。

2. failed to push registry.fly.io/...: unknown: app repository not found

症状: image push 段階で transient 失敗。

原因: Fly registry token の一時的な auth glitch。

復旧: BUILD_PUSH_RETRIES (default 2) で自動 retry されるため、operator は何もしなくてよい。3 回目以降も失敗するときは flyctl auth whoami で token の app access を確認:

flyctl apps list | grep ritsubi-ecommerce

3. aborted: machine is replacing: concurrent update in progress

症状: deploy-fly フェーズで全 machine update が失敗。

原因: 直前の flyctl secrets set が machine restart を trigger し、deploy がそれと衝突した。

復旧: pre-flight Fly machine settle 待ち が 90 秒で次状態確定を待つように改修済 (2025-06)。それでも発生する場合は手動で:

flyctl status -a ritsubi-ecommerce        # transitional machine が無いか確認
flyctl machine wait <machine_id> -a ritsubi-ecommerce
just deploy-vendure-with-migration production

4. release_command failed: VENDURE_HARDEN_PLUGIN_ENABLED must be 'true'

症状: production deploy の release_command で HardenPlugin guard が abort。

原因: Fly secret が未設定。

復旧:

# 注意: bare `flyctl secrets set` は bluegreen deploy を暗黙発動するため禁止。wrapper を使う。
bash scripts/ops/fly-secrets-set.sh -a ritsubi-ecommerce VENDURE_HARDEN_PLUGIN_ENABLED=true
# --deploy なしなので stage のみ。次の deploy で反映される:
just deploy-vendure-with-migration production

5. release_command failed: Runtime drift detected

症状: release_command の startup で SchemaDrift detector が abort。

原因: コード側で entity / custom field を追加したが対応する migration が抜けている。

復旧: 不足 migration を作って commit → push → deploy 再実行。詳細は schema-drift-runbook.md

6. post-deploy-check.sh HTTP 000

症状: /health/ready が 000 で false alarm。

原因: deploy 直後の dashboardApiCanary warmup や machine 切替で probe が瞬間的に timeout。

復旧: probe は 5s x 6 attempts で retry されるよう改修済 (2025-06)。それでも残るときは:

curl -sS https://commerce.ritsubi-platform.com/health/ready | jq .

を手動で確認し、status: ok ならば deploy 自体は成功。verify だけ手動で再実行:

bash apps/vendure-server/scripts/post-deploy-check.sh production

7. Vendure 成功 / Dashboard 失敗

症状: [manual-deploy-vendure] ABORT: fly_fail=0 dashboard_fail=N

原因: Vendure backend は OK だが、Dashboard 側の Cloudflare deploy や build が失敗。

復旧: backend は既に新 image で稼働しているため、Dashboard を単独再実行する:

SECRETS_CONFIG=production_vendure ./scripts/ops/with-env.sh -- \
  ./scripts/ops/nx.sh run ritsubi-vendure-server:dashboard:deploy:production

operator が DEPLOY_DASHBOARD_BLOCKING=false を export して deploy した場合は exit 0 で続行され、recipe 末尾に WARN ログが残る (Vendure は live、Dashboard は前 version のまま)。後追いで Dashboard を deploy すれば良い。

8. worker process が停止して queue が詰まる

詳細は job-queue-runbook.md を参照。/health/readyjobQueueLagfail を観測する。fly.toml の [[restart]] policy = "always" で自動復旧されるが、max_retries 超過したら machine を手動で start する:

flyctl machine start <worker_machine_id> -a ritsubi-ecommerce

CI 経由 deploy (workflow_dispatch)

# staging に全面 deploy (vendure / storefront / dashboard)
just manual-deploy-staging develop true true true true

# staging smoke 成功後、Storefront / Dashboard を prod-preview へ配信
gh workflow run .github/workflows/deploy-prod-preview.yml --ref develop

# prod-preview smoke を手元から再実行
just env-status prod-preview
just storefront-shadow-probe prod-preview
just storefront-business-canary prod-preview

# release 後に hidden lane を明示的に閉じる場合は Cloudflare route だけを外す。
# Cloudflare 側は低コストなので、通常は Access 保護のまま idle にして次回 release で再利用してよい。

# 新 migration を含む CI deploy (allow_pending_migration=true)
gh workflow run .github/workflows/deploy-staging.yml --ref develop \
  -f vendure=true -f storefront=true -f dashboard=true \
  -f allow_pending_migration=true

CI deploy は GITHUB_ACTIONS=true を見て AWS secrets injection 経路を skip し (prepare-aws-deploy-secrets action が既に inject している)、local 専用 fallback と二重実行しない。

CI image build / buildcache 方針

deploy パイプラインの build 時間に効く原則。詳細根拠は各ファイルの inline コメントが SSOT。

  • Fly registry は deploy 成果物(image push)専用にし、buildcache の経路から外す。buildcache は最適化に過ぎず deploy の critical path を塞いではならない。正本は scripts/ops/build-vendure-image.sh
  • CI の buildcache は gha backend に一本化(Docker 公式が GitHub Actions 内で推奨)。cache-tomode=min + ignore-error=true で非ブロッカー。registry への buildcache 書き出しは既定 OFF(exporting cache to registry が deploy を ~111s 塞いでいた実測を撤去)。
  • registry mode=max への深い cache が要る時のみ BUILDX_CI_REGISTRY_CACHE_TO=true
  • Dockerfile (apps/vendure-server/Dockerfile.fly): workspace packages の build を app COPY より前に置き、COPY apps/vendure-server/ は build 入力 subdir(src/scripts/static/types/public/tsconfig/project/package)に分割して cache 無効化面を最小化する。scripts/ops/copy-vendure-static.mjscopy:static が参照するため image へ同梱必須。
  • deploy / build job の pnpm install は app スコープに絞る: setup-pnpm-workspacepnpm-filter 入力に ritsubi-storefront... / ritsubi-vendure-server... を渡し、monorepo 全体 install(~48s)を避ける。nx/wrangler/tsc は root devDep(catalog:)で filter install でも常に入るため app スコープで足りる。Vendure Fly Dockerfile base stage と同じ実証済みパターン。
  • flyctl は mise.toml で version 固定 し、各 job は mise-toolsflyctl を入れて mise tool cache(hashFiles('mise.toml') キー)から配布する。install-flyctl(curl latest, 非 cache, ~19s)は段階的に廃止中。

関連リンク