プログラミング
.env ファイルの問題点
Where .env Went Wrong (secretspec.dev)
要約
この記事は、ソフトウェア開発で広く使われている`.env`ファイル形式が、環境変数を扱う上で多くの問題を抱えていると指摘しています。`.env`ファイルは、環境変数をプロセスに渡すという単一の機能を便利に拡張しましたが、設定スキーマの定義、機密情報の管理、環境モデルの表現といった、より複雑な要件を満たすには不十分であり、結果としてコードやドキュメントの不整合、セキュリティリスク、管理の複雑化を招いていると論じています。
全文翻訳
Where .env Went Wrong
2026年7月30日
Domen Kožar
.env はソフトウェアにおける最も成功した事故の一つです。それは3つの export コマンドのショートカットとして始まりました。その後、プロジェクトの設定スキーマ、シークレットストア、環境モデル、オンボーディングガイド、CIインターフェース、デプロイメントフォーマットとなりました。環境変数は一つの仕事をうまくこなします:文字列をプロセスに渡すこと。.env はその配信メカニズムを真実の源に変えました。利便性がアーキテクチャになったのです。そこに .env の間違いがありました。
環境変数は値を提供するだけ
環境変数は小さな問題を解決します:実行中のプロセスに値を取得すること。アプリケーションは、開発者、CIシステム、またはシークレットマネージャーが供給したかを知ることなく、DATABASE_URL を読み取ることができます。.env ファイルはそれらの値を保存して再ロードしやすくします。それは便利です。しかし、チームはファイルを使用してアプリケーションが必要なものを記述します。KEY=value は、値が必要か、シークレットか、コミットしても安全か、本番でのみ利用可能か、または1つのサービスに制限されているかを示すことができません。これらの要件は、任意のプロセスや開発者のラップトップを超えて存続します。それらは永続的なプロジェクト宣言に属します。.env は配信のために値を保存します。アプリケーションのシークレットモデルを定義することはできません。
文字列はスキーマではない
典型的な例ファイルを見てみましょう:.env.example
DATABASE_URL=
REDIS_URL=redis://localhost:6379
STRIPE_API_KEY=
DEBUG=false
このファイルは、答えるよりも多くの疑問を投げかけます。空の値は必須またはオプションを意味しますか?REDIS_URL は開発時のデフォルトですか?STRIPE_API_KEY は本番専用ですか?DEBUG はブール値ですか?Dotenv はこれらの答えをエンコードできません。Node.js は、すべての値が文字列になることを文書化しています。ブール値に関する dotenv の問題(2015年に開かれたもの)は、依然として "false" が truthy(真)であることに驚く開発者からの反応を集めています。チームは、不足している情報を他の場所に配置します:バリデーションコード、README、.env.example、またはチームメイトの記憶。これらのソースは drift(ずれる)します。ファイルはまた、DEBUG と STRIPE_API_KEY を同等に見せます。一方は Git に属する通常のセッティングです。もう一方は権限を付与し、アクセス制御とローテーションを必要とします。それらを混同すると、ファイル全体が機密になります。明示的な宣言がない場合、欠落した値は late(遅れて)失敗します:アプリケーションはコードがそれらを使用しようとするまでそれらを発見しません。
ファイルは増殖し始める
新しい要件は通常、別のファイルを作成します:.env.env.local.env.development.env.development.local.env.test.env.production
ファイル名は環境モデルになります。サフィックスはスコープを定義し、ロード順序は継承を定義し、ファイルをコピーすることはデプロイメントになります。これは Twelve-Factor App のガイダンスを逆転させます。そのポイントは、環境変数が独立した制御であるべきだということでした。なぜなら、名前付き環境はデプロイメントが増殖するにつれて脆くなるからです。.env.production は、ファイル名でそのグループ化を再作成します。今や、新しい値はすべて .env.example に追加され、README に文書化され、コードで検証され、適切な実際のファイルにコピーされなければなりません。一つでも漏らすと、環境は drift します。.env 仕様はない
.env は標準化されているように見えますが、すべてのパーサーは独自のフォーマットを定義します。Node.js は正式な仕様の欠如を文書化しており、python-dotenv も同様です。各ローダーは独自の選択をします。python-dotenv は ${NAME} を展開しますが、$NAME は展開しません。Node dotenv は変数展開を別のツールに委任します。Docker Compose は独自のシェルスタイルの演算子をサポートします。Vite は逆順での参照もサポートし、同じ式がシェルや Docker Compose では機能しないと警告します。コメントや引用符も異なります。Node dotenv はバージョン15で、引用符なしの値での # の意味を破壊的な変更として変更しました。ある devenv ユーザーは、引用符がエクスポートされたキーの一部になったことを発見しました。どの値が勝つのか?
パーサーは優先順位についても意見が異なります。Node dotenv は通常、最初のファイルが優先されます。Docker Compose は最後の env_file を優先し、その後 environment セクションがそれをオーバーライドします。Vite は既存のプロセス変数をファイルよりも優先します。Docker Compose は2つの似た名前を異なる動作させます。env_file: コンテナに変数を供給しますが、compose.yaml を補間するためにそれらを使用しません。docker compose --env-file は補間に影響します。設計どおりにクローズされた問題で、メンテナーはオプションの名前が不幸に選ばれたと説明しました。誰が最初に .env をロードしたのか?
優先順位はタイミングにも依存します。Node dotenv の ES モジュールガイダンスは、インポートされたモジュールが初期化中に環境を読み取る場合に特別な処理が必要です。Vite は、Bun の自動 .env ロードが Vite 自身のロード順序に干渉する可能性があると警告します。VITE_* 値はビルド時に置き換えられ、クライアントバンドルの一部になります。同じ行が実行時シークレット、ビルド時定数、または公開ブラウザ値になる可能性があります。ローダーはタイミングとコンテキストに基づいて決定します。その時点で .env は小さなプログラムのように動作し、制御フローはファイル名、フラグ、作業ディレクトリ、親プロセス、およびライブラリバージョンに広がります。無視されたファイルは依然としてファイルである
dotenv プロジェクトは .env をコミットしないように言っています。.gitignore は一つの事故を防ぎます。それは暗号化、アクセス制御、監査、または取り消しを追加しません。ファイルはエディタのバックアップ、チャットメッセージ、アーカイブ、サポートバンドル、コンテナビルドコンテキスト、古いラップトップに最終的に残る可能性があります。ある devenv 統合は、.env の内容を Nix ストアにコピーすることが報告されました。そこではパスは機密ではありません。開発者が退職するとき、取り消すファイルアクセスはありません。彼らが受け取った各認証情報は個別のコピーです。1Password、Vault、クラウドシークレットマネージャー、またはシステムキーリングに保存されたシークレットでさえ、dotenv ベースのアプリケーションが使用できるように、プレーンテキストにコピーする必要があります。ローカルコピーは元のものよりも管理が少なくなります。環境変数配信にも限界があります。Docker は管理されたシークレットをファイルとしてマウントします。なぜなら、環境変数はコンテナ間で漏洩する可能性があるからです。プロセスはまた、1つのグローバルマップを取得するため、フロントエンドビルド、ワーカー、マイグレーション、およびWebサービスは、それぞれが数個しか必要としない場合でも、同じシークレットをしばしば受け取ります。Dotenv はそのスコープを表現する方法がありません。.env を再び小さくしよう
.env の便利な部分は、「このアプリケーションは値が必要」から「アプリケーションは実行できる」までの短いパスです。KEY=value を期待するツールのアダプターとして、または通常のローカル設定に使用してください。プロジェクトの要件を定義したり、シークレットの永続的なコピーを保存したり、ファイル名で環境をエンコードしたり、どのサービスがどの値を受け取るかを決定したりするために使用しないでください。永続的な設計は3つのジョブを分離します:コミットされた宣言は、アプリケーションが必要とするシークレットを述べます。保護されたストレージは、誰がそれらの値を読み取ることができるかを制御します。明示的な配信は、各プロセスに必要な値だけを提供します。その後、各部分は独立して変更できます。チームは、アプリケーションを書き直すことなくストレージを変更したり、起動前に要件を検証したり、各コンポーネントを独自のシークレットに制限したりできます。
SecretSpec はこれをどのように適用するか
SecretSpec は、コミットしても安全なファイルに宣言を配置します:secretspec.toml
[project]
name = "payments"
revision = "1.0"
[profiles.default]
DATABASE_URL = { description = "Postgres connection string" }
REDIS_URL = { description = "Redis connection string", required = false }
STRIPE_API_KEY = { description = "Stripe API key" }
[profiles.development]
REDIS_URL = { default = "redis://localhost:6379" }
このファイルは、シークレット値を含まずに、要件、デフォルト、および説明を記録します。プロバイダーは値の場所を選択し、プロファイルは要件の実際の違いを説明します。既存のプログラムは、コード変更なしで SecretSpec を採用できます:ターミナルウィンドウ
secretspec run -- ./server
このコマンドは、解決されたシークレットを子プロセスの環境に注入します。移行中に便利ですが、推奨される統合は SecretSpec SDK です。SDK を使用すると、アプリ