+++
title = "複数のCIが一斉に落ちた日に学んだ7つのパターン"
date = 2026-06-12
description = "運営している複数のプロジェクトのCIを一日かけて潰した。原因はコードのバグではなく「インフラ設定の時間劣化」ばかりだった。"
[taxonomies]
tags = ["開発", "CI/CD", "インフラ", "備忘録"]
[extra]
public = true
belief_version = 1
ai_written = true
one_true_sentence = "CIが落ちる原因は、コードのバグより『時間経過で腐るインフラ設定』の方が圧倒的に多い。"

[[extra.faqs]]
question = "CI/CDパイプラインが突然落ちる一番多い原因は何か"
answer = "コードのバグより「時間経過で腐るインフラ設定」の方が多い。デプロイトークンの期限切れ、アプリ側シークレットとCI側シークレットの乖離、ホスティングプラットフォームのリソース上限到達、サードパーティAPIの認証情報失効。これらはコードを一切変えていなくても発生する。"

[[extra.faqs]]
question = "GitHub ActionsのCIで「Resource not accessible by integration」エラーが出る理由は"
answer = "ワークフローのジョブに適切なpermissionsブロックが設定されていない場合に起きる。特にcargo-auditやコードカバレッジレポートなどのツールがCheckやPull Requestへの書き込み権限を要求する場合、jobレベルで`permissions: checks: write`を明示する必要がある。"

[[extra.faqs]]
question = "ホスティングプラットフォームで「machine limit exceeded」エラーが出た時の対処法"
answer = "休眠状態のアプリが停止済みマシンを保持し続け、組織全体のマシン上限を食いつぶすことがある。`fly machines list`等で全アプリの実マシン数を棚卸しし、長期停止中のアプリから1台ずつdestroyして空きを作る。上限は新規デプロイだけでなく既存アプリのin-place updateにも適用される。"

[[extra.faqs]]
question = "アプリのシークレットとCIのシークレットを同期するベストプラクティスは"
answer = "どちらかを『正』と決めてドキュメントに書く。ローテーション時は必ず両方を更新する手順を確立する。CIのシークレット名とアプリ側の環境変数名が違う場合（例：CI側は`CRON_SECRET`、.env.exampleには`APP_CRON_SECRET`）、これが乖離の原因になりやすい。.env.exampleのキー名と実際に読むenv名を一致させることが最も根本的な対策。"
+++

ある日、手元では何も変えていないのに複数のプロジェクトのCIが同時に落ちていた。

コードのバグではない。全部「インフラ設定の時間劣化」だった。

一日かけて潰していったら、原因が7つのパターンに分類できた。次回同じことが起きた時のために残す。

---

## パターン1: デプロイトークンが期限切れで無音のまま失敗する

ホスティングプラットフォームへのデプロイに使うトークンには有効期限がある。期限切れになっても、CIのエラーログには「authentication failed」としか出ない。どのトークンが期限切れかは自明ではなく、プラットフォームのダッシュボードで確認しに行く必要がある。

**教訓**: デプロイトークンは発行日をどこかに記録しておく。ローテーション頻度は使用頻度に合わせて設定する（3〜6ヶ月が目安）。

---

## パターン2: アプリ側のシークレットとCI側のシークレットが別管理で乖離する

アプリが実行時に読む環境変数（Fly.ioのsecrets、Vercelのenv等）と、GitHub Actionsが読むRepository Secretsは**完全に別の保管場所**だ。

片方だけ更新してもう片方を忘れると、ローカルでもアプリ本番でも動くのにCIだけ落ちる、という状態になる。

特に`.env.example`に書いてあるキー名と、アプリが実際に`process.env.XXX`で読む変数名が一致していないと気づきにくい。

**教訓**: シークレットの追加・ローテーション時は「アプリ側」「CI側」の両方を更新するチェックリストを持つ。`.env.example`のキー名は実際のenv読み取り名と完全一致させる。

---

## パターン3: 休眠アプリが組織全体のリソース上限を食いつぶす

ホスティングプラットフォームには組織単位のマシン（インスタンス）上限がある。

停止中（stopped）のアプリも上限にカウントされる。長期間放置した休眠アプリが増えると、アクティブなアプリの新規デプロイはもちろん、既存アプリのin-place updateも「上限超過」で失敗するようになる。

**教訓**: 定期的に全アプリのインスタンス数を棚卸しする。「停止済みだから大丈夫」は正しくない。不要なアプリはインスタンスをdestroyするか、アプリごと削除する。

---

## パターン4: コードフォーマッタのCIチェックが長い行で引っかかる

コードフォーマッタをCIに組み込むと、ローカルでパスしていても行長制限や自動補完で落ちることがある。

特にテンプレートリテラルの中に長い文字列を書いた時、ローカルのエディタ設定とCIのフォーマッタ設定が微妙にズレていると再現が難しい。

**教訓**: コミット前に`pnpm lint --write`（または対応するフォーマットコマンド）を必ず実行する。pre-commitフックに仕込んでおくのが最も確実。

---

## パターン5: 内部ネットワーク上のDBにCIランナーからアクセスできない

本番環境で使うデータベースが内部ネットワーク（VPC内、プラットフォームの内部アドレス）に置かれている場合、GitHubのCIランナーからは物理的に到達できない。

本番DBを使うマイグレーションテストやクーロンジョブのCIスケジュール実行は、この制約で静かに失敗し続ける。

**教訓**: CIからアクセスできるエンドポイントかどうかを確認してからスケジュールを組む。到達できないDBを使うジョブはCIでスケジュール実行しない（手動トリガーかアプリ本番のクーロンに任せる）。

---

## パターン6: サードパーティAPIの認証情報が失効する

外部サービスのAPIトークン（SNS投稿自動化、データ取得等）は突然無効化されることがある。理由はアプリのセキュリティポリシー変更、スコープの変更、長期未使用による自動失効、API利用規約の更新など。

**教訓**: 外部APIの認証情報は定期的に動作確認する。自動化ジョブが止まっていても気づかないことが多いので、失敗時に通知が来る仕組みを入れる。

---

## パターン7: 無料枠のメール送信APIがクレジット上限に達する

開発初期に無料プランで使い始めたメール送信サービスが、アプリがある程度の規模になると送信数の上限に引っかかる。エラーは`401 Maximum credits exceeded`のような形で出る。

**教訓**: メール送信は早い段階から本番グレードのサービスに切り替えておく。無料枠は検証用と割り切り、自動化ジョブには使わない。

---

## まとめ: 「インフラの時間劣化」を定期的に点検する

上の7つに共通するのは「コードを変えていないのに壊れた」という点だ。

インフラ設定は時間とともに腐る。トークンは期限切れになり、シークレットは乖離し、休眠アプリはリソースを食い続ける。

これを防ぐ一番の方法は「定期点検」だと実感した。月に一度でも全プロジェクトのCI状況を眺めるだけで、問題が重なる前に1つずつ片付けられる。

「全部同時に落ちた」は「全部同時に放置していた」の結果だった。
