“私のローカルでは動く” (英語: “Works on my machine”) — 自分のローカル環境でバグを再現できない開発者が発するクラシックな台詞ですが、バグはチームの他のメンバーや本番環境では再現されます。この状況は、開発者のマシンとバグが再現される環境のあいだで、構成、依存関係のバージョン、オペレーティングシステム、またはデータの違いによって生じます。Stack Overflow Survey 2023によると、58% の開発者が月に少なくとも一度この台詞を使い、31% が毎週使用しています。コードが場所によって異なる動作をする理由と、環境を標準化する方法を解説します。
ポイント
「私のローカルでは動く」 — チームメンバーやテスターがバグを報告したときに、開発者が発する台詞ですが、開発者のマシンでは再現されません。外的には問題の否定と見えますが、技術的には現実の状況です: コードはある環境では正常に動作し、別の環境では失敗することがあります。構成のあるビットが違うだけで、アプリケーションの動作が徹底的に変わります。
この台詞は IT コミュニティでミームとなっています。なぜなら、実際に正しくかつ無用だからです。開発者の観点からすれば、コードは彼のマシンで動いています。チームの観点からすれば、問題は存在しており、置き組みではなく解決する必要があります。この状況のユーモア は、開発者が正直を言っているが、その正直さはバグ修正に役立たないということです。このミームは非常に人気があり、Reddit、XKCD、DevOpsカンファレンスで数千の投稿がこの話題に割かれています。
プロセスの観点からすれば、「私のローカルでは動く」という台詞は、環境の再現性に問題があることを示す指標です。二人の開発者が同じコードで同じ結果を得られないならば、環境のセットアッププロセスは標準化されていません。DevOpsの実践 では、環境はリポジトリからのワンコマンドで、手動操作なしで再現できるようにすべきです。
開発者の ローカル環境 はいつも本番環境と異なります。開発者は macOS あるいは Windows を使用し、サーバーは Linux で動作します。異なるオペレーティングシステムは、異なるファイルシステム、エンコーディング、スレッドのタイミング、システムコールを持ちます。両方の環境が Linux であっても、カーネル、glibc、OpenSSL のバージョンが異なる可能性があります。
二番目の原因は インストールされたソフトウェアの違い です。開発者のマシンには Node.js 20 のグローバルバージョンがインストールされているが、CI/CD 構成ではバージョン 18 が指定されているかもしれません。また、開発者がローカルで PostgreSQL 16 を使用し、本番では PostgreSQL 14 を使用することもあります。マイナーバージョンの違いは通常気づかれませんが、メジャーアップデートは SQL クエリの動作を変更する可能性があります。npm Inc.によると、依存関係に関連するバグの 67% はパッチバージョンの違いによって生じています。
三番目の原因は ネットワーク環境 です。ローカルマシンにはラテンシ、幅容量の制限、DNSの問題がありません。本番環境では、外部 API への何らかのリクエストが 5 ms の代わりに 500 ms かかることがあります。タイムアウト、リトライロジック、レースコンディション — これらの問題はすべて、実際の負荷と実際のネットワーク条件下でのみ現れます。Toxiproxy などのツールを使った ネットワークエミュレーション は、デプロイ前にこうした問題を検出するのに役立ちます。
一番目の原因は データの不足 です。開発者はテストフィクスチャで動作しますが、本番には予期せざる値を持つ百万単位のレコードがあります。開発者が必須だと考えていたフィールドの NULL、名前の Unicode 文字、長すぎる文字列 — これらのべてが、シンセティックなデータのローカル DB では再現できないバグを引き起こす可能性があります。
二番目の原因は コンパイルフラグとビルドフラグの違い です。リリースビルド (Release/Distribution) はデバッグビルド (Debug) と異なる可能性があります。コンパイラの最適化、デバッグログの削除、関数のインライン — これらはすべて、バグを隠したり、その反対に現わしたりする可能性があります。具体例: デバッグビルドでは assert が動作するが、リリースでは変数初期化の順序が異なるために失敗する。
三番目の原因は ローカルキャッシュと一時ファイル です。ブラウザに古いスクリプトがキャッシュされていたり、Redis に古いデータが保存されていたり、ファイルシステムに前回実行の一時ファイルが残っていたりするため、開発者がバグに気づかない可能性があります。クリーンな実行 (プライベートモード、キャッシュ消去、fresh install) によって、「自然には”現れなかったバグがよく再現されます。
四番目の原因は グローバル依存関係とローカル依存関係の競合 です。Ruby gems、Python pip、Node.js npm などのツールでは、グローバルにインストールされたパッケージがローカルでの動作を「サポート」するが、本番には存在しないことがあります。仮想環境 (virtualenv、venv、nvm) を使用することで、プロジェクトをグローバルのインストールからアイソレートし、再現可能な環境を作ります。
「私のローカルでは動く」という台詞は、チームの 信頼を破壊 します。開発者が定期的にバグを再現できない場合、チームメンバーは彼の能力やテストの徹底度を疑うようになります。時間が経つと、これはマイクロマネジメントにつながります: 変更ごとに第二の開発者によるチェックが必要となり、開発が遅くなります。Google Project Aristotle によると、チームの心理的安全性は生産性に直接影響し、環境に関する継続的な議論はそれを低下させる要因の一つです。
二番目の問題は code review の遅延 です。開発者がローカルでバグを再現できない場合、「コチラでは動いている — つまり問題はあなたのほうだ」と言って同料の pull request を拒否するかもしれません。これは競合を招き、機能の提供を遅らせます。環境の標準化は、この競合を解消します: 両方の開発者が同じ Docker コンテナで動作していれば、「どちらが正しいか」という質問は意味をなさなくなります。
三番目の問題は トラッカーでのバグの失われ です。「開発者に再現されない」バグは、度々「Cannot Reproduce」とコメント付きでクローズされます。一か月後にバグが本番で発生し、修正に 10 倍のコストがかかります。ルール: バグが少なくとも一つの人で再現されれば — 存在します。開発者のマシンで動くかどうかは関係ありません。
一番目で最も効果的な方法は Docker です。プロジェクト全体が追加操作なしで docker-compose up で動作する必要があります。データベース、キャッシュ、メッセージキュー、ウェブサーバー — すべてがコンテナ内で動作します。開発者は Docker と Git だけをインストールします。それ以外はコンテナ内にあります。これにより、OS に関わらず、チームのすべてのメンバーが同じ環境を持つことが保証されます。
二番目の方法は バージョン管理ツール です。Docker が使えない場合 (ライセンス制約、レガシーインフラ) は、nvm (Node.js)、rbenv (Ruby)、pyenv (Python)、sdkman (Java) を使用します。バージョン管理ツールにより、プロジェクト内で言語やツールのバージョンを切り替えることができます。.nvmrc、.ruby-version、.python-version ファイルはリポジトリに置き、CI/CD でチェックする必要があります。
三番目の方法は仮想マシンのための Vagrant です。Vagrant は VirtualBox または VMware 上で指定された OS と構成を持つ仮想マシンを起動します。VM 内では、provisioning スクリプト (shell、Ansible、Puppet) を通じてすべての依存関係がインストールされます。Vagrant は Docker よりも重いですが、OS レベルの完全なアイソレーションを提供します — 特定の Linux カーネルバージョンに依存するプロジェクトに役立ちます。
四番目の方法は Makefile と bootstrap スクリプト です。install、test、build、clean 目的を持つ簡単な Makefile でも、ルーチン作業を標準化できます。make install コマンドは、すべての依存関係をインストールし、DB を構成し、テストデータを作成する必要があります。すべての開発者のための 統一エントリポイント は、環境セットアップでの手動ミスを排除します。
主なツールは依存関係の lockファイル です。package-lock.json (npm)、yarn.lock (Yarn)、Podfile.lock (CocoaPods)、pubspec.lock (Flutter) は、各パッケージの正確なバージョンを固定します。lockファイルがなければ、異なる時期に依存関係をインストールした二人の開発者が、異なるマイナーバージョンを得る可能性があります。Lockファイルはリポジトリにあり、手動で編集してはいけません。
二番目のツールはリポジトリの .env.example です。コメント付きの環境変数のテンプレートファイルです。開発者はこれを .env にコピーし、自分の値を入力します。CI/CD パイプラインは、すべての必須変数が設定されているかをチェックします。GitLab 2023 によると、.env.example を使用するチームは、環境変数に関連するインシデントの数を 40% 減少させています。
三番目のツールは pre-commit フック です。各コミットの前に実行される自動チェック: リンタ、フォーマッタ、型チェック、テスト。すべての開発者が同じようにフックを構成していれば、「ローカルマシンでは通った」フォーマットや型のエラーが本番に伝わることはありません。JavaScript 用の Husky と Python 用の pre-commit が人気の解決策です。
四番目は CI/CD パイプライン で、クリーンな環境でテストを実行します。テストが CI では通るがローカルでは通らない場合 — 問題はローカル環境のセットアップにあります。テストが CI で通らない場合 — pull request はマージされません。この厳密なルールにより、「ローカルでは動く」バグがメインブランチに入るのを防げます。
よくある質問
これは 防御反応 です: 開発者はデバッグに多くの時間を費やしており、コードが動作しないと聞くのは心理的に相手です。この台詞は、罪悪感なしに「スイッチ」して原因調査を始める時間を与えます。
クリーンな環境 (clean install、プライベートモード) で バグの再現 を依頼してください。再現しない場合は、依存関係のバージョンと環境変数を比較してください。効果がない場合は、本番と同じ Docker 環境を起動してください。
Docker は、固定構成の アイソレートされたコンテナ を提供し、どの OS でも同じように動作します。すべての開発者が同じ Dockerfile を使用するため、環境は同じです。コンテナでバグが再現されない場合 — 問題はシステムではなく、コードにあります。
Lockファイルは、すべてのトランジティブ依存関係の正確な ハッシュとバージョン を固定します。パッケージレジストリで依存関係の新しいバージョンがリリースされても、lockファイルからのインストールにより、各開発者が他の開発者と同じパッケージセットを得られます。
VirtualBox を使用した Vagrant は、プロジェクトが OS カーネルの特定のモジュールに依存していたり、カーネルレベルの完全なアイソレーションが必要な場合に適しています。90% のプロジェクトでは、Docker のほうが軽く、高速で、便利です。選択は、プロジェクトが OS とどの程度深くインタラクションしているかによります。
まとめ
ターンキー方式のモバイルアプリケーションを開発します
IT Sectrは2017年からスタートアップや企業向けにiOS・Androidアプリケーションを開発しています。私たちがご相談に乗り、最適なソリューションをご提案します。