Dev Study
← 開発の実務
レッスン 9 / 12·約 9 分·第16期 開発の実務

CIは、きれいな別の機械で同じ手順をもう一度走らせること

リクエストがアプリケーションに届くまで
  1. ブラウザURL入力
  2. DNS名前→IP
  3. ルータ経路選択
  4. ロードバランサ振り分け
  5. Nginx受け口
  6. アプリ処理今ここ
  7. DB保存

前回は、レビューが読んでいるのが差分であることを見ました。今回は同じ手順を「手元」と「きれいな機械」で走らせ、その結果を比べます。

まず結論

「壊れていない」は、何と比べたかでしか言えません。この回で比べるのは、自分の手元の機械と、リポジトリの中身だけから組み立てた「きれいな機械」です。

手元で通ったビルドは、リポジトリに入っていないファイルや設定に支えられていることがあります。同じ1コマンドを別の機械で走らせて初めて、それが分かります。

? 読む前に予測

手元では `npm run build` が通ります。同じコミットを新しいコンテナに取り出し、同じコマンドを走らせるとどうなるでしょうか。ビルドは `.gitignore` に書いた `.env` を読みます。

当てることが目的ではありません。外した予測ほど記憶に残るので、直感で選んでください。

抽象原理

「手元で通った」は、何と比べた結果か

このコースの芯は「『壊れていない』は、何と比べたかでしか言えない」です。では「手元でビルドが通った」は、何を確かめたことになるでしょうか。

手元の機械には、リポジトリに入っているファイルの他に、入っていないものも置いてあります。無視したファイル、前に手で作った設定、入れたままの道具。手元の成功は、それら全部が揃った状態で通ったという結果です。そのうちどれが効いていたのかは、手元だけでは分かりません。

Fowler の CI 記事は、ビルドの自動化を扱う節でこう書きます。

I'll elaborate my earlier rule of thumb: anyone should be able to bring in a clean machine, check the sources out of the repository, issue a single command, and have a running system on their own environment.

きれいな機械を持ってきて、リポジトリから取り出し、1コマンドで動く状態になる。これは手元にしか無いものを取り除いた比較です。同じ手順が通れば、成功を支えていたのはリポジトリの中身だと言えます。

手元の成功と、きれいな機械の成功が確かめている範囲
手元の機械追跡されたファイル + 無視したファイル + 手で入れた設定
リポジトリに入るのは、追跡されたファイルだけ
リポジトリgit ls-files に出るもの
取り出して、同じ1コマンド
きれいな機械リポジトリの中身だけで通るか
  • ・手元の成功は、上の箱の全部が揃ったときの結果
  • ・きれいな機械の成功は、真ん中の箱だけで足りることを示す
具体実物

無視した .env が、手元でだけビルドを支えていた

小さなリポジトリで試しました。ビルドは node --env-file=.env build.js で、.env から API_URL を読みます。.gitignore には .env を書いてあります。

きれいな機械は、git clone した作業ツリーを Docker コンテナ(node:24-alpine、ネットワーク遮断、実行後に自動削除)に渡して作りました。環境は macOS 26.5.2、git 2.50.1、Node v24.21.0 です。出力は加工していません。

手元と、きれいな機械で同じ npm run build(2回実行して同一。== の見出し行は実演スクリプトが表示した要約で、実際のコマンドとは異なる)コマンド出力
== git ls-files (リポジトリに入っているもの)
.gitignore
build.js
package.json
== git status --short --ignored
!! .env
== 手元: npm run build
 
> build
> node --env-file=.env build.js
 
built dist/config.js with API_URL = https://api.example.test
exit=0
== きれいな機械: git clone → docker run --rm --network none -v clean:/w node:24-alpine npm run build
-- container: node v24.21.0, files:
.git
.gitignore
build.js
package.json
 
> build
> node --env-file=.env build.js
 
node: .env: not found
exit=9

!! .env は、無視されているが手元にあるファイルの印です。手元では exit=0、きれいな機械では node: .env: not found と exit=9。同じコミットの同じ1コマンドが、手元でだけ通っていました。

gitignore の説明文書は目的をこう書きます。

The purpose of gitignore files is to ensure that certain files not tracked by Git remain untracked.

追跡していないファイルを、追跡しないままにする。狙いどおりに働いた結果、.env はリポジトリに入らず、取り出した先にも無かったのです。

✓ ここまでの確認

実演で、手元では通ったビルドがきれいな機械で落ちた理由として正しいのはどれですか。

抽象原理

CI の定義と、きれいな機械の関係

CI は一般にこう定義されます。Fowler の2024年改訂版の冒頭です。

Continuous Integration is a software development practice where each member of a team merges their changes into a codebase together with their colleagues changes at least daily. Each of these integrations is verified by an automated build (including test) to detect integration errors as quickly as possible.

SWE at Google 23章も冒頭で定義を引いていますが、これは脚注で Fowler の記事を示す引用の引用です。文言は members of a team integrate their work frequently で、2024年版と違います。改訂前の版を引いたと見られますが、その版は確かめていません。

定義そのものに「きれいな機械」は出てきません。先ほどの引用は、ビルドの自動化を扱う節にある要件です。「Fowler は CI をきれいな機械での再実行と定義した」と書くと、出典に無いことを言わせます。この教材がつなぐのは、「統合のたびに自動ビルドで検証する」なら、その自動ビルドは誰の手元でもない機械で走る、という点です。

Fowler は同じ節でこうも書きます。

A common mistake is not to include everything in the automated build.

きれいな機械で落ちたとき、その落ち方は「自動ビルドに入っていないもの」の一覧になります。

具体実物

開発で動いても、本番では undefined

別コース「ビルドとバンドル — 書いたコードと届くコードが違う理由」の FieldNote に、process.env.API_URL が本番で undefined になる例がありました。開発中は動き、本番に出すと undefined/items を叩きに行く、という例です。

仕組みは今回と違います。あちらの論点は、その行がブラウザ側で動くなら接頭辞の無い環境変数は見えないことと、ビルド時の静的置換でした。.gitignore も CI も関係しません。

それでも、比べている相手を取り違えた点は同じです。あちらの本文は「厄介なのは開発時に正しく動いてしまうところです」と書いていました。開発サーバで通ったことは、本番の出力で通ることの証拠になりません。今回も、手元で通ったことは、リポジトリだけで通ることの証拠になりませんでした。

✓ ここまでの確認

`.gitignore` の事例と、別コースで見た「本番で undefined になる process.env.API_URL」に共通する点はどれですか。

⇄ 2つを見比べる
具体例 A

無視した .env に頼るビルド

手元では .env が作業ツリーにあるので通ります。リポジトリには入っていないので、取り出しただけの機械では見つからずに落ちました。

どこで通ったか表
場所 .env 結果
手元 ある exit=0
きれいな機械 ない exit=9
具体例 B

本番で undefined になる API_URL

開発中は動きました。その行がブラウザ側で動くなら、接頭辞の無い環境変数は見えず、本番では undefined のまま fetch します。

どこで通ったか表
場所 API_URL 結果
開発中 見える 動く
本番の出力 undefined undefined/items

原因の仕組みがまったく違う2つの失敗に、共通する構造は何でしょうか。

「どこで確かめたか」と「どこで動いてほしかったか」を、両方で並べてみてください。

この章のまとめ

  • 手元の成功は、リポジトリに無いものも含めて全部が揃った状態の結果。どれが効いたかは手元だけでは分からない。
  • 実演では同じ1コマンドが手元で exit=0、きれいな機械で node: .env: not found / exit=9。
  • .gitignore の目的は追跡していないファイルを追跡しないままにすること。すでに追跡しているファイルには効かない。
  • Fowler の a clean machine はビルドの要件として書かれたもので、CI の定義そのものではない。
  • SWE at Google 23章の定義はFowler からの引用で、文言は2024年改訂版と違う。
  • git diff の出力に出るのは追跡されたファイルの変更だけ。原因がリポジトリの外にあると差分には出ない。

自分の現場で確かめる

読んだだけでは身につきません。手元の環境で実際に見てみてください。

理解度チェック

読んだ直後は「分かった」と感じやすいものです。実際に解いて確かめてください。

問題 1 / 3正解: 0

すでにコミットしてしまった `.env` を、あとから `.gitignore` に書き足しました。どうなりますか。

用語集(3語)
CI(継続的インテグレーション)
変更をこまめに統合し、そのたびに自動のビルドとテストで検証する習慣。Fowler の2024年改訂版は統合の頻度を `at least daily` と書く。
きれいな機械
この教材での呼び方。リポジトリから取り出したものだけを持ち、手元の追跡外ファイルや設定を持たない機械。Fowler はビルドの要件として `a clean machine` と書く。
無視されたファイル
`.gitignore` に一致し、追跡されていないファイル。`git status --short --ignored` では `!!` で表示される。すでに追跡されているファイルには `.gitignore` は効かない。