プログラマのための IT 教科書

CI/CD

この部の 4 / 5 章 ・ 全体で 73 / 76 章 ・ 読了目安 40 分

この章を読むとできるようになること
  • CI が落ちた時に自分で原因を切り分けられる
  • デプロイ戦略ごとの向き不向きを説明できる
  • 安全に戻す手段を先に用意できる

前の章で、コードを動かす「場所」の話をしました。この章は、そこへコードを届ける仕組みの話です。

現場に入ると、PR を出した瞬間に何かが自動で走り始めます。緑のチェックが付けばマージでき、 赤くなると止まります。あれが CI です。そしてマージした後、誰も手を動かしていないのに 新しいコードが本番で動き出します。あれが CD です。

この2つは「なんとなく動いている魔法」ではありません。壊れた時に直せる人になるのが、この章の目標です。

CI は何のためにあるか

CI(Continuous Integration / 継続的インテグレーション)を一言で言うと、 「手元では動いた」をチームで潰す仕組みです。

こういう経験は、CI がないチームでは日常的に起きます。

A さん「マージしました!ローカルでは動いてます」
B さん「main が起動しないんですが…」
A さん「あ、新しく入れたライブラリ、package.json に入れ忘れてました」

これは A さんが不注意なのではありません。人間は必ず忘れます。 テストを流し忘れる、lint を掛け忘れる、生成コードのコミットを忘れる。 1日に何十回もあるマージのうち、1回忘れれば main は壊れます。

CI は、その注意力を機械に肩代わりさせる装置です。

誤解しないでほしいのは、CI が守っているのはあなたではなく main だということです。 目的はミスを見つけて咎めることではなく、main ブランチが常に動く状態を保つことにあります。 main が壊れると他の全員が仕事を止め、「自分の変更が原因なのか main が既に壊れているのか」が 分からなくなって、チーム全体が調査に引きずり込まれます。CI はそれを防ぐための共有資産です。

CI が回っていると何が変わるか

CI がないCI がある
壊れたことに気づくのが数日後数分後
「誰の変更が原因か」を人間が探すその PR だと即座に分かる
リリース直前にまとめて壊れる壊れた瞬間に1件ずつ直る
マージが怖いので PR が巨大になる小さく安全にマージできる

最後の行が実は一番効いています。戻せる・すぐ分かる仕組みがあるから、小さく速く出せるのです。

CI で何を回すか

CI で走らせるものは、だいたいこの5種類です。

種類何を見るか例
lint / format書き方の統一、危ないパターンESLint、go vet、golangci-lint
型チェック型の整合性tsc --noEmit
テスト振る舞いが壊れていないかVitest / Jest、go test
ビルドそもそも成果物が作れるかnext build、go build、Docker イメージ
セキュリティスキャン既知の脆弱性、秘密情報の混入npm audit、govulncheck、シークレット検出

Go では -race を回す

Go のテストは、必ず競合検出を付けて回してください。

go test -race ./...

データ競合(複数の goroutine が同じ変数を同時に触る)は、 普通に実行すると 100 回に 99 回は通ってしまいます。 通ってしまうから本番まで届き、 本番の負荷で初めて壊れます。-race を付けると、その場で検出できます。

代償として実行時間が数倍になるので、「PR では -race あり、ローカルでは無し」のように 使い分けるチームもあります。

型チェックとテストは別物

TypeScript の型チェックは「形が合っているか」しか見ません。 calculateTotal が合計ではなく差額を返していても、型が number なら通ります。

  • 型: 存在しないプロパティを触っていないか、null の可能性を握り潰していないか
  • テスト: 計算結果が正しいか、境界で壊れないか

両方必要です。 どちらかで代用はできません。

速さが命

CI について、新人のうちに覚えてほしい一番大事なことです。

遅い CI は、誰も待たなくなる。

CI が30分掛かるチームで何が起きるか。

1. PR を出す
2. 30分待てないので、別の作業に切り替える
3. 戻ってきたら赤い。原因を直して push
4. また30分
5. 「もういいや、後で見る」→ 確認せずにマージする文化が生まれる

CI が遅いと、CI を無視する習慣が育ちます。そうなると CI はコストだけ払って 効果はゼロの存在になります。速さは快適さの問題ではなく、CI が機能するかどうかの問題です。

CI が10分を超えたら見直す

目安として、PR で回る CI は10分以内を目標にしてください。5分ならさらに良いです。

10分を超えたら、それは「マシンが遅い」ではなく「設計を見直す時期が来た」というサインです。 並列化・キャッシュ・ジョブの分割で、たいていは半分以下になります。

放置すると15分になり、20分になり、ある日「CI はいつも遅いもの」という 諦めがチームに定着します。そこから戻すのは非常に大変です。

パイプラインの設計

速いものから落とす

CI のジョブには順番の設計があります。原則はこれです。

速くて、失敗しやすいものを先に走らせる。

lint(10秒)→ 型チェック(40秒)→ 単体テスト(2分)→ 結合テスト(6分)

lint で落ちる PR に、6分の結合テストを走らせる意味はありません。 早く落ちれば、フィードバックが早く返り、CI マシンの時間も節約できます。

この考え方を fail fast(早く失敗する) と言います。

並列に走らせるもの、直列にするもの

一方で、何でも直列にすると遅くなります。

関係例やり方
互いに依存しないlint / 型チェック / 単体テスト並列に走らせる
前段の成果物が必要ビルド → E2E テスト直列(needs で繋ぐ)
落ちても他を止めたくない複数バージョンでのテストマトリクス + fail-fast: false

GitHub Actions ならこう書きます。

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run lint
 
  typecheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npx tsc --noEmit
 
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm test
 
  # ここだけ直列: 上が全部通ってからビルドする
  build:
    needs: [lint, typecheck, test]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run build

needs を書かないジョブは自動的に並列で走ります。 「lint → 型 → テスト」と並べたくなりますが、互いに独立なら並列が正解です。 順番の話は「どれを先に見るか」ではなく、「どこで打ち切るか」の設計です。

キャッシュ

CI の時間の大半は、たいてい依存関係のインストールです。 毎回 node_modules をゼロから作れば、それだけで数分掛かります。

      - uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: npm          # package-lock.json のハッシュをキーにキャッシュ
 
      - uses: actions/setup-go@v5
        with:
          go-version: '1.23'
          cache: true         # go.sum をキーにモジュールとビルドキャッシュを再利用

キャッシュ設計のポイントはキーの選び方です。

  • キーが細かすぎる(例: コミットハッシュ)→ 毎回ミスして意味がない
  • キーが粗すぎる(例: ブランチ名だけ)→ 古い依存が残って謎の失敗を生む

正解は「依存を決めているファイルのハッシュ」です。 npm なら package-lock.json、Go なら go.sum、それぞれのハッシュをキーにします。

直す前に計測する

「なんとなく遅い」で手を付けると、効かない場所を最適化しがちです。 GitHub Actions なら、ジョブの画面で各ステップの所要時間が見られます。 まず一番長いステップを特定してください。 たいてい犯人は 「依存インストール」「Docker イメージのビルド」「E2E テスト」のどれかです。

10秒のステップを5秒にしても意味はありません。6分のステップを2分にするのが仕事です。

PR で回すもの / main で回すもの

全部を毎回回す必要はありません。

タイミング回すもの
PR(毎 push)lint、型、単体テスト、ビルド。速さ優先
main マージ後上に加えて結合テスト、イメージ push、ステージングへのデプロイ
夜間(定期)E2E フルセット、依存の脆弱性スキャン、負荷テスト

「重いけれど毎回は要らない」ものを夜間に逃がすのが、CI を10分以内に収める定石です。

CI が落ちた時の切り分け

赤いチェックが付いた時にどうするか。ここが実務で一番差の出るところです。

手順

1. 「自分の変更が原因か」「main が既に壊れているか」を切り分ける
2. ログを開き、【最初の】エラーを読む(最後ではない)
3. そのエラーをローカルで再現する
4. 再現しないなら、環境差を疑う

まず「main は生きているか」を見る

これを最初にやってください。 自分の PR のログを読み込む前です。

  • main のワークフロー実行が緑か赤か
  • 他の人の PR も同じところで落ちていないか

main が既に赤いなら、あなたの変更は無関係かもしれません。 そこに気づかず2時間デバッグする、というのが新人が最もよく溶かす時間です。

main が壊れている場合、あなたがやるべきことは自分のコードを直すことではなく、 チームに知らせることです。

main の CI が失敗しているようです(〇〇のテストが失敗)。 直近のマージは PR #123 に見えます。 私の PR も同じところで落ちているので、いったん待機します。

逆に、自分が main を壊してしまった時は、隠したり黙って直そうとしないでください。 壊れている間、チーム全員が影響を受けます。やることは3つです。 すぐ知らせる。戻すか直すかを判断する(迷ったら revert が早い)。落ち着いてから原因を共有する。

main を壊すこと自体は悪ではありません。放置することが悪です。

ログは最初のエラーを読む

CI のログは数千行あります。新人がやりがちなのは、一番下までスクロールして 最後のエラーを読むことです。これはたいてい間違いです。

...
ERROR  src/order/total.ts:12  Type 'string' is not assignable to type 'number'   ← 本当の原因
ERROR  src/order/index.ts:3   Cannot find module './total'                        ← 巻き添え
ERROR  build failed with 2 errors                                                  ← ただの結果
npm ERR! code ELIFECYCLE                                                           ← ただの結果
npm ERR! Exit status 1                                                             ← 最後の行

最後の行(Exit status 1)には何の情報もありません。 最初のエラーが原因で、後続はその余波というのが基本形です。

ログを開いたら、まず上に向かって「最初の ERROR / FAIL」を探してください。 GitHub Actions のログ画面では、ブラウザの検索(Cmd+F)で error や FAIL を 探すのが速いです。

ローカルで再現する

原因の当たりが付いたら、CI が実行しているのと同じコマンドを手元で叩きます。

# ワークフローの run: に書いてあるコマンドをそのまま実行する
npm ci          # ← npm install ではない。lock ファイル通りに入れる
npm run lint
npx tsc --noEmit
npm test

npm install ではなく npm ci を使うのが重要です。 install は lock ファイルを更新してしまうことがあり、CI と違う依存で動いてしまいます。 「手元では通るのに CI では落ちる」の典型的な原因がこれです。

再現しないなら環境差を疑う

手元で通って CI で落ちるなら、コードではなく環境が違います。よくある差はこの5つです。

差具体例対策
バージョン手元 Node 22 / CI は Node 20.node-version、go.mod の go ディレクティブを揃える
OS手元 macOS / CI は Linuxファイル名の大文字小文字、パス区切り、改行コード
タイムゾーン手元 JST / CI は UTC日付テストで固定 TZ を指定する
ロケール文字列のソート順、数値の桁区切りロケール依存の比較を避ける
依存の解決lock を無視して入れているnpm ci を使う

macOS はファイル名の大文字小文字を区別しませんが、Linux は区別します。 import { Button } from './button'(実体は Button.tsx)は 手元では通り、CI では落ちます。 これは非常によく踏みます。

「CI だけ落ちる」は環境差か、テストがおかしいか

再現しない時の犯人は、だいたい次の3つに絞られます。

  1. 環境差(上の表)
  2. テストの実行順依存 — 前のテストが残したデータに依存している。CI は並列やシャッフルで 順番が変わり、そこで露見する
  3. 時刻・乱数・外部通信 — テストの中で new Date() や実 API を叩いている

3番目が疑わしければ、それは CI の問題ではなくテストの設計の問題です。 時刻と乱数は注入して固定します。

Flaky なテスト

Flaky(フレーキー)テストとは、同じコードなのに通ったり落ちたりするテストです。 たいていの原因は、待ち時間の甘さ、実行順への依存、共有リソースの取り合いです。

やってはいけないのは、リトライ回数を増やして黙らせることです。

# これで「直った」ことにしない
- run: npm test -- --retry 3

Flaky を retry で誤魔化すと、次に起きるのはこれです。

  1. 落ちても「また flaky だろう」と誰も見なくなる
  2. 本物のバグが flaky に紛れて見逃される
  3. 気づいた時には、CI の赤が意味を持たなくなっている

正しい対処は、原因を直すか、直せないなら一時的に skip して、チケットを切って 期限を決めて直すことです。retry は延命であって治療ではありません。

あなたの PR で CI が赤くなりました。最初にやるべきことは?

CD とデプロイ戦略

CI が通ったコードを、実際に動かす環境へ届けるのが CD です。

  • 継続的デリバリー(Continuous Delivery) — いつでも出せる状態を保つ。出す判断は人間
  • 継続的デプロイ(Continuous Deployment) — main にマージされたら自動で本番まで出る

多くのチームは「ステージングまでは自動、本番は人が承認」から始めます。 いきなり全自動にする必要はありません。戻せる仕組みが揃ってから進めるものです。

3つのデプロイ戦略

戦略やり方切り戻しの速さコストDB スキーマとの相性
ローリング古い Pod を少しずつ新しいものに置き換える中(再度ローリングで戻す)低い(追加リソースは少し)新旧が同時に動くので互換必須
ブルーグリーン新環境を丸ごと立て、ルーティングを一気に切り替える速い(向き先を戻すだけ)高い(本番2セット分)切替の瞬間だけ新旧が混ざる
カナリアまず数%のトラフィックを新版へ流し、様子を見て広げる速い(比率を0に戻す)中新旧が長く同時に動くので互換必須

Kubernetes の Deployment は、デフォルトがローリングです。何も設定しなければこれになります。

spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0   # 落としてから作らない = 常に処理能力を保つ
      maxSurge: 1         # 一時的に1つ多く立てる

どれを選ぶか

  • ローリング — 既定値。ほとんどのバックエンドサービスはこれで十分
  • ブルーグリーン — 切替を一瞬で終わらせたい、切り戻しを最速にしたい時。コストと引き換え
  • カナリア — 影響が読み切れない変更(性能特性が変わる、外部依存が増える)を、実トラフィックで確かめたい時
ローリングとカナリアは「新旧が同時に動く」

ここが設計上、最も事故が起きるポイントです。

ローリング中は、v1 の Pod と v2 の Pod が同じデータベースを同時に触ります。 つまり v2 だけが知っている新しいカラムに v1 が対応できないと、その数分間で壊れます。

「新しいコードが出れば古いコードは消える」と考えると必ず踏みます。 新旧は必ず一定時間、共存します。

ヘルスチェックが切り戻しの前提

Kubernetes のローリング更新は、新しい Pod が「準備完了」になるまで古い Pod を消しません。 その判定が readiness probe です。

readinessProbe:
  httpGet:
    path: /healthz
    port: 8080
  initialDelaySeconds: 5
  periodSeconds: 5

ここが中身のないエンドポイント(常に 200 を返すだけ)だと、 起動に失敗しているコンテナが「準備完了」と判定され、壊れた版が全台に広がります。

ヘルスチェックは「プロセスが生きているか」ではなく、 「リクエストを処理できる状態か」を返してください(DB に繋がっている、必要な設定が読めている)。

ロールバックを先に用意する

開発の流れで「出す」より「戻せる」ことの方が重要だと書きました。CD ではそれが具体的な設計になります。

戻せないデプロイをしない

デプロイの計画を立てる時、必ず先にこの問いに答えてください。

これが本番で壊れていたら、何分で元に戻せますか。

答えられないなら、まだ出す準備ができていません。

# Kubernetes なら、直前のリビジョンに戻すのは一撃
kubectl rollout undo deployment/my-service
 
# 履歴を確認する
kubectl rollout history deployment/my-service

コンテナイメージには必ず一意なタグ(コミットハッシュなど)を付けてください。 latest だけで運用していると、「戻したい版がどれか分からない」という状況になります。

DB マイグレーションは戻せないことがある

ここがロールバックの最大の弱点です。

アプリのコンテナは1コマンドで戻せますが、データベースは違います。

-- これを実行した後、「戻す」とはどういう意味でしょうか
ALTER TABLE users DROP COLUMN nickname;

カラムを消したら、そこに入っていたデータは消えます。 アプリを前の版に戻しても、データは戻りません。

だから、スキーマ変更は「戻す」のではなく 「常に前方互換を保つ」 形で進めます。 これを expand→contract(拡張してから縮小) と呼びます。

expand → contract

nickname カラムを display_name に改名したい、という例で見ます。 一気にやると、ローリング中の古い Pod が nickname を探して壊れます。

正しくは3回に分けてデプロイします。

段階やることこの時点で動くもの
1. expanddisplay_name を追加する(nickname は残す)旧コードも新コードも動く
2. 両方書くアプリが両方のカラムに書き、読むのは新しい方。既存データを移送する旧コードも新コードも動く
3. contract旧コードが完全に消えたのを確認してから nickname を削除新コードのみ

ポイントは、各段階が単独でロールバック可能なことです。 段階2で問題が起きても、段階1の状態にアプリを戻せば動きます。

スキーマ変更とコード変更を同じデプロイに混ぜない

新人が最もやりがちな事故がこれです。

「カラムを消す SQL」と「そのカラムを使わなくしたコード」を同じ PR で同時に出すと、 ローリング中の数分間、まだ古いコードが動いている Pod がカラムを探して落ちます。

ALTER TABLE を含む変更を書く時は、必ず自分に聞いてください。 「この瞬間、古いコードはまだ動いているか?」 答えは、ほぼ常に「はい」です。

デプロイと機能公開を分ける

ここまでの話の総仕上げです。

新人のうちは「デプロイする = 新機能がユーザーに見える」と考えがちですが、 実務ではこの2つを分離します。

  • デプロイ = 新しいコードを本番の環境に置くこと
  • リリース(機能公開) = そのコードの機能をユーザーに見せること

分離する道具がフィーチャーフラグです。

// コードは本番に出ているが、機能はまだオフ
if (await featureFlags.isEnabled('new-checkout', { userId })) {
  return renderNewCheckout();
}
return renderLegacyCheckout();

これができると、次のことが可能になります。

できること意味
未完成の機能をマージできる巨大な長期ブランチを作らずに済む(マージ地獄の回避)
一部のユーザーにだけ出せる社内メンバー → 5% → 全体、と段階を踏める
デプロイせずに戻せる障害時、フラグをオフにするだけ。数秒で復旧
公開タイミングを事業側が決められるエンジニアの深夜リリースが不要になる

3行目が特に強力です。切り戻しにデプロイが要らないというのは、 障害対応の速度がまったく変わることを意味します。

フラグには「消す予定」をセットで書く

フィーチャーフラグは便利ですが、放置すると分岐がコードに永久に残ります。 フラグが50個あるコードベースは、どの経路が本番で動いているのか誰にも分からなくなります。

フラグを足す時は、同時に「全体公開後に削除する」チケットを切ってください。

// TODO(PROJ-456): 全体公開が完了したらこの分岐を削除する

足す時に、消す計画も決める。 これがフラグ運用の唯一のコツです。

やってはいけないこと

CI を skip してマージ

「急ぎのリリースなので、CI を待たずにマージしました」

急いでいる時ほど、確認を飛ばした変更は壊れます。そして急いでいる時ほど、 壊れた時のダメージが大きいです。

管理者権限で必須チェックを回避できる設定があっても、それは 「本番が既に燃えていて、修正を1分でも早く出すしかない」ような場面のためのものです。 日常的に使うものではありません。

Flaky を retry で誤魔化す

前述の通りです。再実行して通ったから OK、を習慣にしないでください。 それは「CI の赤に意味がない」状態への第一歩です。

金曜の夕方に大きなデプロイをする

開発の流れで触れた話が、ここでも効いてきます。

問題は「金曜だから」ではなく、問題が起きた時に対応できる人が揃っているかです。

場面判断
金曜16時に大きめの機能をデプロイ月曜に延ばす
金曜16時に本番障害の修正をデプロイ出す(放置する方が危険)
月曜10時に大きめの機能をデプロイ良い。1週間かけて様子を見られる

デプロイの後は最低30分は張り付いてエラー率とレイテンシを見ます。 その30分を確保できない時間帯には出さない、と考えると分かりやすいです。

秘密情報を CI ログに出す

これは事故のレベルが違います。CI のログは、リポジトリを見られる人が全員読めます。

# 絶対にやらない
- run: echo "DB_PASSWORD=$DB_PASSWORD"
- run: env                        # 環境変数を全部出す
- run: curl -v https://api.example.com -H "Authorization: Bearer $TOKEN"  # -v はヘッダを出す

set -x を付けたシェルスクリプトも、実行するコマンドをそのまま出力するので危険です。

正しい扱いはこうです。

      - name: Deploy
        env:
          GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}   # secrets から注入する
        run: ./scripts/deploy.sh                   # ログには値が出ない
漏れた秘密情報は「消す」のではなく「無効化する」

うっかりトークンをログや commit に出してしまった時、ログを消して安心してはいけません。 その値は既に外に出たものとして扱います。

やることは、削除ではなくローテーション(無効化して新しい値を発行)です。 そして、すぐ報告してください。隠すと、後から悪用された時に原因究明が不可能になります。

これは新人が責められる種類のミスではありません。報告が遅れることだけが問題です。

users テーブルの不要になったカラムを削除し、同時にそのカラムを参照しないようアプリを修正した PR を、Kubernetes 上のサービスにローリング更新でデプロイします。何が起きますか。

ケーススタディ: 20分の CI を6分にする

C さんのチームでは、CI が20分掛かっていました。

npm ci             4分   (キャッシュなし)
lint               1分
型チェック          1分
単体テスト          3分
E2E テスト          8分   (PR ごとに全シナリオ)
Docker ビルド       3分   (毎回フルビルド)

全部が直列でした。手を入れた順序はこうです。

  1. 計測する — 一番長いのは E2E の8分、次が npm ci の4分
  2. キャッシュを入れる — setup-node の cache: npm で 4分 → 40秒
  3. 並列化する — lint / 型 / 単体テストを並列に。5分 → 3分(一番遅いものの時間)
  4. E2E を移す — PR では主要導線だけ(2分)、フルセットは main マージ後と夜間へ
  5. Docker のレイヤキャッシュ — 依存インストール層を分けて 3分 → 1分

結果は 20分 → 6分 です。特別な技術は一つも使っていません。

重要なのは順番です。計測せずに Docker から手を付けていたら、3分が1分になっただけで 「あまり変わらないね」で終わっていました。

ケーススタディ: retry で隠れていたバグ

D さんのチームでは、決済まわりの結合テストが週に2〜3回落ちていました。 再実行すると通るので、いつしか --retry 2 が付けられ、誰も気にしなくなりました。

半年後、本番で「まれに二重課金される」という報告が上がります。 調査すると、原因はテストが指摘していたものと同じでした。

  • テストは、並列実行時に同じ注文 ID が2回処理されると落ちていた
  • それは flaky ではなく、冪等性の欠陥をテストが正しく検出していた
  • retry で通っていたのは、たまたま並列の順番が変わっただけ

「たまに落ちる」は「たまに壊れる」です。 テストが正しくて、コードが間違っている 可能性を、いつも最初に考えてください。

実務の落とし穴まとめ

  1. CI が遅い — 10分を超えると誰も待たなくなり、確認せずマージが始まる
  2. ログの最後を読む — 原因は最初のエラー。最後の行はただの結果
  3. main の状態を見ずにデバッグする — 自分は無関係かもしれない
  4. npm install で再現しようとする — CI は npm ci。依存が変わって再現しない
  5. flaky を retry で黙らせる — 本物のバグが赤に紛れて見逃される
  6. スキーマ変更とコード変更を同時に出す — ローリング中の古い Pod が落ちる
  7. 戻し方を決めずにデプロイする — 「何分で戻せますか」に答えられないなら出さない
  8. フラグを消さない — どの経路が本番で動いているか誰も分からなくなる
  9. 秘密情報をログに出す — 消しても手遅れ。無効化して報告する

まとめ

  • CI は 「手元では動いた」をチームで潰す仕組み。人間の注意力に頼らないための機械
  • CI が守っているのはあなたではなく main。壊したら隠さず即座に知らせる
  • 回すのは lint / 型 / テスト / ビルド / セキュリティスキャン。Go は -race を付ける
  • 速さが命。10分を超えたら設計を見直す。速いものから落とし、独立なものは並列、依存はキャッシュ
  • CI が落ちたら、まず main が生きているかを切り分ける。次にログの最初のエラーを読む
  • 再現しないなら環境差(バージョン / OS / タイムゾーン / ロケール / 依存の解決)を疑う
  • デプロイ戦略はローリング / ブルーグリーン / カナリア。新旧は必ず一定時間共存する
  • 戻し方を先に用意する。DB は戻せないので expand→contract で段階を分ける
  • デプロイと機能公開を分ける。フラグをオフにするだけで戻せる状態が最強
  • flaky を retry で誤魔化さない。「たまに落ちる」は「たまに壊れる」

公式ドキュメント

迷ったら一次情報に戻ってください。

対象リンク
GitHub Actions ドキュメント(日本語)https://docs.github.com/ja/actions
DORA(デプロイ指標の調査)https://dora.dev/
Google Cloud Deployhttps://cloud.google.com/deploy/docs?hl=ja

次の章では、出したものが本当に動いているかをどう知るか——監視・アラート・ そしてオンコールで呼ばれた夜に何をするかを扱います。

読み終わったら記録しておくと、目次で進み具合が分かります。