このシリーズは、熟練エンジニアと甥の架空の会話を描いています。各エピソードでは、ソフトウェアがアイデアから本番環境に至るまでの各段階を探求します。


👦 甥:おじさん。やっとVS Codeを開いたよ。npm startでいい?

👨‍🦳 叔父:待て。それを動かす前に — どこに接続されるんだ?どのデータベースで、誰の認証情報を使って、どの環境に?

👦 甥:…実は知らない。ただ実行すると動くんだ。

👨‍🦳 叔父:それが問題だ。「動く」ことと「何をしているか分かっている」ことは違う。設定を開いて見せてくれ。


設定:秘密情報はコードに含めるべきではない

👨‍🦳 叔父:この行を見てみろ。

const dbUrl = "postgres://admin:P@[email protected]:5432/wishlist";

Enter fullscreen mode Exit fullscreen mode

👦 甥:…本物のパスワードだ。ハードコードされてる。Gitにコミットされようとしているファイルの中に。

👨‍🦳 叔父:そして一度コミットされると、リポジトリの履歴に永遠に残る。次のコミットでその行を削除してもだ。リポジトリへのアクセス権を持つ人 — またはリポジトリが誤って公開された場合 — は本番データベースのパスワードを手に入れることになる。

.env  (never committedlisted in .gitignore)

DATABASE_URL=postgres://localhost:5432/wishlist_dev
NODE_ENV=development

Enter fullscreen mode Exit fullscreen mode

const dbUrl = process.env.DATABASE_URL;

Enter fullscreen mode Exit fullscreen mode

👦 甥:つまりコードは同じままだけど、どの.envが読み込まれるかによって接続先が変わるってこと?

👨‍🦳 叔父:その通り。同じコード、異なる設定、各環境ごと。ローカルではwishlist_devを使い、ステージングではステージング用のURLを使い、本番では本番用のURLを使う — どれもコード自体に書かれることはない。これはエピソード4の原則と同じだ — 箱は必要のないことを知るべきではない。サービスコードは自分が「本番環境にある」ことを知る必要はない。ただURLを渡されればいい。


ミラーリング:データベースを偽装してはいけない

👦 甥:ローカル開発では、マシンに本物のPostgresをセットアップする代わりに、SQLiteのようなシンプルなものを使ってもいい?

👨‍🦳 叔父:できる。だがそれは静かに嘘をつくことになる。エピソード5を覚えているか — 私たちが設計したUNIQUE (user_id, product_id)制約のことだ。SQLiteとPostgresは、制約、型、または特定のクエリ動作を常に同じように強制するわけではない。SQLiteに対して完全に動作するコードを書き、マシン上のすべてのテストに合格し、その後ステージングの本物のPostgresに触れた瞬間に全く異なるエラーに遭遇するかもしれない。

👦 甥:つまりローカルは本番と同じデータベースエンジンでなければならない。

👨‍🦳 叔父:現実的に可能な限り同じ — 同じエンジン、同じメジャーバージョン、可能な限り管理できる範囲で。ピクセルパーフェクトに同一である必要はない。本番は少し新しいマイナーバージョンで動作したり、Kubernetes内で動作したりするかもしれないが、ローカルは単一コンテナで動作する。重要なのは動作であって、文字通りの同一性ではない。これがDockerの目的だ — そのエンジンの一貫性があり、使い捨て可能なコピーをローカルで実行する一般的な方法を提供することだ。一部のチームはDev Containers、Podman、またはリモートクラウドワークスペースを選ぶ — ツールは原則ほど重要ではない:本番環境が使用する同じエンジンであって、異なる動作をする軽量な代替品ではない。

docker-compose.yml

services:
  postgres:
    image: postgres:16
    environment:
      POSTGRES_DB: wishlist_dev
    ports:
      - "5432:5432"

Enter fullscreen mode Exit fullscreen mode

docker-compose up

Enter fullscreen mode Exit fullscreen mode

👦 甥:1つのコマンドで、会社が実際に本番で実行しているのと同じデータベースエンジンを、手元のテストデータでローカルに実行できる。

👨‍🦳 叔父:それが目標だ。本番の完璧なクローンではない — 本番の規模や実際のユーザーデータをローカルに必要とするわけではない — しかし、局所的に合格したものがステージングでも合格する可能性が高い程度に動作が近いこと。


実際に何かをテストするシードデータ

👦 甥:データベースは動いているけど空だ。ユーザーや商品がなければ、重複チェックをどうやってテストする?

👨‍🦳 叔父:シードする — ランダムではなく意図的に。

seed.js

users:    [{ id: 'u1', name: 'Test User' }]
products: [
  { id: 'p1', name: 'Running Shoes' },
  { id: 'p2', name: 'Discontinued Jacket', active: false }
]

Enter fullscreen mode Exit fullscreen mode

👦 甥:なぜ意図的に非アクティブな商品をシードするの?

👨‍🦳 叔父:エピソード5の決定を実際に実行する必要があるからだ — もはやアクティブでない商品を誰かがウィッシュリストに追加したり、すでにウィッシュリストに追加していたりした場合どうなるか。シードデータが「5つのランダムなハッピーパス商品」だけなら、2つのエピソードで設計した正確なエッジケースを局所的にトリガーすることは決してない。シードデータは、エピソード1の隠れた質問を反映すべきで、明白なケースだけではない。


「自分のマシンでは動く」

👦 甥:おじさん — 「たまたま動く」罠は実際におじさんにも起こったことがある?

👨‍🦳 叔父:一度、ひどい目に遭った。すべてのものがノートパソコンでは完璧に合格した。そしてCIが何度も何度も失敗した — コードが「存在しない」と主張する設定値のチェックで。半日以上かかって見つけた:私のマシンには6ヶ月前に取り組んだプロジェクトから残っていた古い環境変数があった。それが、他の誰の設定にもない値を静かに埋めていた。私のノートパソコンはより正しかったわけではない — たまたま成功のように見えただけの、違う方法で壊れていただけだった。

👦 甥:つまりコードは実際には正しくなかった。

👨‍🦳 叔父:ちょうど1台のマシンでの偶然で正しかった。あの日以来、新しいノートパソコンが私のプロジェクトをきれいに実行できない場合、私は設定が間違っていると仮定する — チームメイトではない。この1つの習慣は、このエピソードの他のどんな規律よりもチームの時間を節約してくれた。


モック:まだ存在しないもの

👦 甥:エピソード3でNotification Serviceについて言及していた — 価格下落アラート。それはまだ存在しない。Wishlistを開発するためにローカルで実行する必要がある?

👨‍🦳 叔父:いや、ここで多くのローカルセットアップが静かに苦痛になる。すべてのエンジニアが自分の機能を開発するために他のすべてのチームのサービスをローカルで実行する必要がある場合、オンボーディングは悪夢となり、チームの半分はコードを書く代わりにDockerをデバッグすることになる。代わりに — モックする。

const notificationService = process.env.NODE_ENV === "development"
  ? new MockNotificationService()   // logs to console, does nothing real
  : new NotificationService();      // real network call

Enter fullscreen mode Exit fullscreen mode

👦 甥:つまりWishlist ServiceはまだnotificationServiceという名前のものを呼び出す。ただ局所的にその名前の背後にあるものが実際に何であるかは重要ではない。

👨‍🦳 叔父:その通り — そしてこれはエピソード4の設計のおかげできれいに機能することに注意してほしい。通知がどのように送信されるかの詳細をWishlist Serviceが知るべきではないと私たちは決定した。その同じ境界が、ビジネスロジックに1行も触れることなくモックをスワップインすることを可能にする。


なぜ本番を直接指さないのか?

👦 甥:正直 — これらすべてをスキップして、ノートパソコンを直接本番データベースに接続することはできない?それは本番であるので、間違いなく本番と一致する。

👨‍🦳 叔父:障害を説明するのが好きでない限りはだめだ。本番データは本物で価値がある。本番システムは、外から見えない方法で脆弱だ。1回の不注意な

DELETE FROM wishlist;

Enter fullscreen mode Exit fullscreen mode

WHERE句なしで、「何か素早くテストしている」間に実行された — そして午後は会社全体のインシデントレビューになる。これが、ローカル開発が独自の規律として存在する理由全体だ:間違いが安価で、元に戻せ、完全に自分で犯すスペースを与えるためだ。


実際に重要なテスト:新しいチームメイトはこれを実行できるか?

👨‍🦳 叔父:ローカルセットアップが実際に良いものであり、個人として機能しているだけではないことを知る方法はこれだ — このリポジトリを見たことのない新しいチームメイトにノートパソコンを手渡し、何が起こるかを見る。

git clone ...
cp .env.example .env
docker-compose up
npm install
npm run seed
npm start

Enter fullscreen mode Exit fullscreen mode

👦 甥:もしこれが私のマシン以外で最初から最後まで動作しないなら...

👨‍🦳 叔父:それは実際には再現可能ではない — それはただ「自分のマシンでは動く」だけであり、そのフレーズは業界全体でほとんどどんなバグよりも多くのエンジニアリング時間を費やしてきた。


Configure → Mirror → Mock

👦 甥:フレームワーク。

👨‍🦳 叔父:

Configure
  ↓
Mirror
  ↓
Mock

Enter fullscreen mode Exit fullscreen mode

Configure — すべての秘密と環境固有の値はコードの外に存在し、実行時に読み込まれる。同じコードは1行も変更することなくすべての環境で実行されるべきだ。

Mirror — ローカル環境は本番と同じコア技術を使用するべきだ — 同じデータベースエンジン、同じメジャーバージョン — テストしようとしている正確な条件の下で異なる動作をする軽量な代替品ではない。

Mock — この機能の境界外にあるもの — 他のチームのサービス、まだ存在しない依存関係 — は局所的に代役を立てるので、セットアップは小さく保たれ、すべてのエンジニアは会社のインフラストラクチャ全体をノートパソコンに必要とせずに実行できる。


叔父の名言

「静かに嘘をつくローカル環境は、まったく動作しない環境よりも悪い — 少なくとも壊れたセットアップは壊れていることを教えてくれる。」


👦 甥:つまり、実際のaddItemロジックを書く前に、これらすべてを。

👨‍🦳 叔父:すべてだ。今やコードを書くことができる。何と話しているかを正確に知り、自分のマシンで動作するものがステージングでも同じように動作することを信頼して。しかし、コードを書くことは規律の半分に過ぎない — どのように保存し、共有し、チームメイトを邪魔することなくマージするかは次のレッスンだ。


🧠 エンジニアのように考える — 宿題

エピソード3から設計してきた同じ機能を取る。

  • Configure — この機能がハードコードされるべきでない少なくとも3つの値(URL、キー、フラグ)をリストアップせよ。.envファイルに何が含まれるかのスケッチを描け。
  • Mirror — 「よりシンプルな」ローカル代替品(異なるデータベース、インメモリフェイク)を使用することで、ステージングまたは本番でのみ現れるバグを隠す可能性がある場所を1つ挙げよ。
  • Mock — この機能が独自の境界外の何かに依存するものを1つ挙げ、実際のものをローカルで実行する必要なくどのようにスタブするかのスケッチを描け。

正確なツールを正しく理解することを心配する必要はない。目標は、環境が自分の下で変化する前に、機能が環境に密かに依存しているすべての場所に気づく練習をすることだ。

— エピソード7 終了 —