世界を動かす技術を、日本語で。

環境変数はレガシーの混乱です:深く掘り下げてみましょう

概要

  • プログラミング言語 の進化と対照的に、 環境変数 の仕組みはUnix時代からほぼ変化なし
  • 環境変数は 親プロセスから子プロセスへ 引き継がれる仕組み
  • Linuxでは execveシステムコール を通じて環境変数が渡される
  • Bash、C、Python など言語ごとに内部管理方法が異なる
  • POSIX標準では 大文字推奨 だが、実際の制限は緩い

ソフトウェア開発における環境変数の基礎

  • 環境変数 は、アプリケーションの ランタイムパラメータ として利用される仕組み
  • ファイルやIPC、ネットワークを使わずに 値を受け渡す ための手段
  • グローバルかつフラットな文字列辞書 であり、 型や名前空間 は存在しない
  • 例: export SECRET_API_KEY=2u845102348u234 のように値を設定

環境変数の正体と伝播

  • 環境変数は OS内部の特別な辞書 ではなく、 親から子へ明示的に引き継がれるデータ
  • Linuxでは execveシステムコール が新プロセス起動時にenvp配列として渡す
    • 引数: filename(実行ファイルパス)、argv(コマンドライン引数)、envp(環境変数配列)
  • ほとんどのツール(Bash、Python subprocess、Cのexecl等)は 親の環境変数をそのまま子へ渡す
  • 例外: loginコマンド などは新しい環境をセットアップ

カーネルによる環境変数の扱い

  • 新プロセス起動時、 カーネルは環境変数をスタック上に連続したヌル終端文字列配列 として配置
  • このレイアウトは静的で、プログラムは自身でコピーして管理する必要
  • 例: HOME=/, PATH=/usr/bin などが16進表示で並ぶ

各言語による環境変数の内部管理

  • Bash
    • ハッシュマップのスタック構造 で管理
    • localでスコープ付き変数、exportで子プロセスに伝播
    • ローカル変数もexport可能
  • glibc(C言語)
    • 動的な配列environ で管理
    • putenvgetenvで操作、線形探索なので高速ではない
  • Python
    • os.environ はCのenviron配列から構築
    • os.environの変更はos.putenvを呼ぶが、逆方向は同期されない
    • os.environとCライブラリの間に一貫性のズレ が生じる場合あり

環境変数のフォーマットと制限

  • Linuxカーネルやglibcは フォーマットに寛容
    • 同名変数の重複や、=なしのエントリも許容
    • 例: NONSENSE_WITH_EMOJI 😀 のような値も受け入れる
  • 制限事項
    • 1変数あたり128KiB (x64 Intel CPUの場合)
    • 全体で2MiB (コマンドライン引数と共有)
    • 制限はスタックサイズやページサイズによる

環境変数の挙動の違いと注意点

  • Bashは 重複名や不正フォーマットを自動で整理
  • 変数名に空白を含む場合、NushellやPythonは許容、Bashは参照不可だが子プロセスには渡る
  • 不正な変数はBashの invalid_envハッシュマップ に格納

POSIX標準と推奨される命名規則

  • POSIX では変数名に=が含まれていなければ許容
  • POSIX準拠アプリは 大文字・数字・アンダースコア のみ使用
  • 小文字やその他の文字も許容されるが、標準ツールとの衝突回避のため 小文字はアプリ独自用途に予約
  • 実際は ALL_UPPERCASE が実務的に推奨される
  • 安全な命名: ^[A-Z_][A-Z0-9_]*$、値はUTF-8またはPOSIX Portable Character Set

まとめと実践的アドバイス

  • 環境変数は 便利だが古い設計 であり、 名前空間や型安全性はない
  • 大文字+アンダースコア+数字 の形式で命名し、値はUTF-8推奨
  • パフォーマンスや安全性 を求める用途では過度な利用を避ける
  • 言語やシェルごとの違い に注意し、意図しない伝播や競合を防ぐ設計が重要

Hackerたちの意見

環境変数はよく秘密情報を渡すために使われるけど、普及している割には良くないプラクティスだと思う。 - Linuxシステムでは、どのユーザープロセスでも同じユーザーの他のプロセスの環境変数を確認できる。脅威モデルについて議論はできるけど、特に開発者のシステムでは、同じユーザーとして動いているプロセスがめっちゃ多い。 - 個人的には、非コンテナ化されたLLMエージェントが開発者のメインOSユーザーと同じユーザースペースで動くようになったことで、これがさらに顕著な問題になってると思う。これは秘密情報を抜き取る悪用者にとって夢のような状況だ。 - 環境変数は通常、他の生成されたプロセスに渡されるけど、実際に必要なのは主プロセスだけのことが多い。 - systemdはユニットの環境変数を全てのシステムクライアントにDBUSを通じて公開していて、秘密に環境変数を使うことを警告している[1]。これって、非ルートユーザーがルート専用のユニットやサービスに設定された環境変数にアクセスできるってことだと思う。間違ってるかもしれないけど、まだ試してないからね。でも、これが本当なら、多くのシステム管理者には大きな驚きだろうな。秘密をファイルや環境変数に出さずに管理するためには、秘密管理プロセス(例えば1Passwordのop CLIツール)と、その秘密が必要なプロセス(flaskやterraformなど)との間で一時的なファイル共有をするのが唯一の解決策だと思う。これがsystemdの資格情報システムの仕組みなんだけど、広くサポートされてるわけじゃない。環境変数や通常のプレーンテキストファイルを使わずに秘密を渡す良い方法はないかな? 編集: 1Passwordのopクライアントは、各新しい「セッション」が自分の承認を必要とするので、いいスタートだと思う。だから、秘密が必要なCLIセッションでそのツールを有効にできるけど、opバイナリを使おうとする悪意のあるプロセスはその承認を利用できない。新しいポップアップが出るからね。でも、これはまだ第一歩。第二歩は…その秘密を必要なプロセスとどう共有するかで、また元の議論に戻る。

Linuxシステムでは、どのユーザープロセスでも同じユーザーの他のプロセスの環境変数を確認できる。脅威モデルについて議論はできるけど、特に開発者のシステムでは、同じユーザーとして動いているプロセスがめっちゃ多い。 これはすごく良いポイントだね!でも、どうやって回避するのかは分からないな。そのプログラムが認証情報を見つけてファイルを復号化できるなら、ユーザーとして動いてる限り、他のプロセスもその認証情報を見つけられるってことだし。

環境変数を使わずに秘密を共有する良いクロスプラットフォームで簡単な方法はないかな?

Linuxのセキュリティモデルは、ネームスペースなしではかなり壊れてる。systemdには役立つ機能がいくつかあるけど、環境変数よりも良いものを求めるなら、自然とcgroupsに手が伸びるよね。

環境変数や通常のプレーンテキストファイルを使わずに秘密を渡す良い方法はないかな? memfd_secretが思い浮かぶね https://man7.org/linux/man-pages/man2/memfd_secret.2.html でも、あまり言語サポートは見たことがないな。おそらくLinux専用だからかも。Rustで書く人たち(あとはGoも、FFIがどれだけ簡単かによるけど)は試してみるべきだと思う。Cの関数をラップするのは簡単だろうから、PHPでもサポートを得たいと思ってたけど、php-fpmを変更しなきゃいけないって考えるとちょっと躊躇しちゃった。Cコードをハックしたくないし、できないからね。実際には、プロセスマネージャーが秘密ファイルディスクリプタを開いた後に子プロセスを生成して、それを渡すのが理想だね。可視メモリにも/proc/*/environにも出さない形で。

少なくとも2012年から、環境変数は普通のメモリと同じくらい安全になってるよ:コミットb409e578d9a4ec95913e06d8fea2a33f1754ea69 著者:Cong Wang 日付:2012年5月31日木曜日 16:26:17 -0700 proc: /proc//environの処理を整理する 他のプロセスの環境を読むことはできないけど、そのプロセスをptrace-readできれば、そのプロセスの秘密は全部わかるからね。cmdlineはまた別の話だよ。

答えではないけど、秘密を渡すための低レベルのプリミティブとそれに対応する高レベルの言語構造があればいいなと思う。例えば、my_secret = create_secret(value) みたいな感じで。そうすれば、その時点からは不透明な値になるのが理想だね。

環境変数は秘密を渡すためによく使われる。でも、その普及にもかかわらず、それは悪い習慣だと思う。環境変数は、コンテナオーケストレーションシステムで管理されるコンテナ化されたアプリケーションの設定パラメータや秘密を渡すために推奨されているんだ。設計上、他のプロセスはコンテナ内で実行されている環境変数を検査できないし、環境変数は子プロセスに渡されるのは、設計上、親プロセスと同じ環境(つまり、同じ値)で子プロセスを実行することが目的だからだよ。さらに、子プロセスを生成するプロセスが環境変数を設定する責任があるから、すでにその秘密に対して少なくとも読み取りアクセスを持っていることになる。全体的に見て、君の懸念は根拠のない理由に基づいていると思うけど、詳しく話し合うのは大歓迎だよ。

  • Linuxシステムでは、どのユーザープロセスでも同じユーザーの他のプロセスの環境変数を調べることができる。脅威モデルについて議論できるけど、特に開発者のシステムでは、同じユーザーとして実行されているプロセスがたくさんあるんだ。ただ、ほとんどのオペレーティングシステムのセキュリティモデルは、ユーザーとしてプロセスを実行することはそのユーザーとして行動することを意味する。いくつかの注意点はあるけど(FreeBSDにはcapsicum、Linuxにはlandlock、SELinux、AppArmor、Windowsには整合性ラベルがある)、一般的には、誰かに何かを実行させることができれば、そのプログラムはそのユーザーの代理として行動する権限を委譲される。 (多くのシステムのユーザーアカウントには他のユーザーを偽装する権限がある場合もある。)これは唯一のセキュリティモデルではないけど(完全に能力ベースのオペレーティングシステムも存在する)、ほとんどのコンピューティング形式で使われているセキュリティモデルなんだ。これの一つの結果として、自分のドメイン内の何でも制御できる。自分のプロセスを終了させたり、スリープさせたり、そして何よりもデバッグできる。秘密を持っているものは、ptrace/process_vm_readv/ReadProcessMemory/etcでそれを取得できる。

あなたが説明したのは、古典的なUnixのセキュリティモデルで、少しの改善があるね。時代に合わせてはいるけど、古さが目立ってきた。特に、安価で普及したコンピューティングに適応するのが難しいっていうのが、元々設計されていなかったから。もし他のプロセスから秘密を守りたいなら、同じユーザーアカウントで実行しない方がいいよ。リモートでアクセスするのも一つの手だけど、他のトレードオフや難しさが伴うからね。

環境変数や普通のプレーンテキストファイルを使わずに秘密情報を渡す良い方法はない? プレーンテキストファイルは問題ないけど、そのファイルの権限が問題なんだよね。プログラムのソースをコントロールできるのが一番良い方法で、そうすれば秘密情報が漏れないように、秘密情報を読めるユーザーとしてプログラムを起動するように変更できる。起動後はプログラムがファイル全体を読み込んで、すぐに権限を落として秘密情報を読めないユーザーに切り替えるんだ。秘密情報だけじゃなくて、他のことにも使えるよ。

面白い読み物だね。もう一つ興味深い事実は、setenv()がPOSIXで根本的に壊れていて、ライブラリコードでは基本的に呼び出すべきじゃないってこと。アプリケーションコードでは、代替手段がない場合にのみ呼び出すべきで、スレッドが始まる前に呼び出すべきだ。理由は、getenv()が変数への生ポインタを渡すから、setenv()で変数を上書きすることは防げないから。極めて注意して扱うべきだね。

確か、Solarisはその問題を解決したけど、Linuxはその解決策をコピーすることを拒否してる。

環境変数を設定する正しい方法はexecve()の中だけだと思う。exec()を跨いでの通信は、まさにそのための道具だからね。

https://github.com/freebsd/freebsd-src/commit/873420ca1e6e8a...

そもそもライブラリコードでsetenvを呼ぶ理由って何?

Linuxで環境変数を設定する時はいつも不安になる。ちゃんと動く方法は(ある程度ディストリビューション特有だけど)あるけど、オンラインで見つける通常の手順は再起動すると(たぶんターミナルを閉じると)動かなくなる。Windowsみたいに、ただ動くシンプルな環境変数GUIを追加すればいいのに。Windowsは変更を有効にするためにターミナルを再起動(または新しいのを開く)しなきゃいけないのが面倒だけど、それ以外はうまくいくからね。

でも、オンラインで見つける通常の手順は、再起動(またはターミナルを閉じるとき?)に一時的に機能しなくなるんだよね。環境はセッション間で持続しないから、毎回新しいセッション(ログインや新しいターミナルウィンドウ)で実行されるように変更を加えないといけないんだ。システムの設定によっては、.bash_profileはログインのたびに一度だけ実行されるし、.bashrcは非ログインの新しいセッション(つまり、新しいターミナルウィンドウ)で一度だけ実行されるんだ。これらのファイルを使う場合は、こんな感じで書くのが一般的だよね:if [ -f ~/.bashrc ]; then source ~/.bashrc fi を.bash_profileファイルに入れて、他のほとんどのことは.bashrcに入れることで、区別を気にしなくて済むようにするんだ。もしbashやbash系を全く使ってないなら、Zshやfishみたいな別のものを使ってるだろうから、そこの設定に合わせてやらないといけないよ。 > 簡単な環境変数のGUIをWindowsみたいに追加すればいいのに、ターミナルに依存しないやつが。 これがLinuxにはないのは、すべてのターミナルが参照する「一つの場所」がないからなんだよね。考えられるのは、.bashrcファイルを読み込んで、変数に似たものを探して、bashコードを解析して設定方法を見つけて、一般的なケースで更新できるGUIツールを書くことだけど…テキストエディタで変更を書く方がずっと簡単だよね。

面白いな、主にLinuxユーザーとしては、Windowsの挙動が本当にイライラする。エンドユーザーのマシンで、環境が汚染されるせいで、繰り返し起こる問題の原因になってるし…なんで何かが動かないのか考えてみたら、理由もなくc:\Perl64\binの$SOFTWAREを使ってることが判明したりするんだよね。

systemdシステムでは、/etc/environmentにKEY=VALUEのペアを設定するか、/etc/environment.d/内の任意のファイルに設定すればいいよ(技術的には他にもいくつか場所があるけどね)。理論的には、ファイルを手動でパースしてGUIを書くのは比較的簡単なはず。アプリの再起動部分はどうしようもないけど、環境変数は実行中のプロセスに注入されないから、プロセス自体が変更するしかないんだ(利用規約が適用される場合もある)。実行中に変更があっても、プログラムがすでにキャッシュした値を使ってるかもしれないから、無視されることもあるよ。[0]: https://www.freedesktop.org/software/systemd/man/latest/envi...

https://xkcd.com/927/ - 標準の状況:Linuxで環境変数を設定する方法が14種類競合している。環境変数を設定するためのユニバーサルな方法を作るべきだ!ターミナルに特化せずに、ただ機能する方法が必要だ!状況:Linuxで環境変数を設定する方法が15種類競合している。

環境変数にはとっくに見切りをつけたよ。今はコンパイラが実行ファイルと同じディレクトリにあるdmd.confファイルを読むようになってる。

SRE/Sysadmin/DevOps/なんでもここに、ブログではENVVARの基準を設定することについて難しいことは話してなかったけど、全ての置き換えは秘密の話をするときに同じくらいイライラすることを指摘しておくよ。アプリケーションが特定の秘密のボールト(Hashicorp Vault/OpenBao/Secrets Managerなど)にアクセスするようなボールトを含むものは、すぐに大規模なベンダーロックインになって、ライブラリの置き換えが非常に難しくなるから、ボールトの稼働時間が極めて重要になるんだ。これが、アップグレードやメンテナンスの時にOpsを非常に困難な立場に置くんだ。設定ファイルには秘密がある場合、どうやってそれを設定ファイルに入れるかという問題がある。設定ファイルは一般的に公開システムに保管されているからね。ほとんどの場合、「アプリケーションに渡す前に特権システムによるテンプレート置き換え」か「全体の設定ファイルが秘密のボールトに読み込まれてアプリケーションに渡される」みたいな形になる。テンプレート化はエラーが起こりやすいし、全体の設定ファイルを秘密のマネージャーに読み込むのも面倒だよね。誰かが読み込みを間違える可能性もあるし。設定ファイルの話をすると、ほとんどのシステムがコンテナを運用しているし、Opsの専門会社でない限り、これらの設定ファイルは決して正しい場所にないから、Opsがマウントを間違えるリスクが高くなるんだ。どんな形式を使っても、JSON/YAML/TOMLはノルウェー問題のような奇妙な設定ファイルのバグが発生しやすい。Kubernetes Secrets APIから秘密を取得するのは見たことがあるけど、またロックインが発生する。Kubernetesオペレーターやそのようなシステムを設計しているのでなければ、このアプローチは強くお勧めしないよ。サブプロセスの問題に悩まされるのを見たことはあるけど、最近はサブプロセス生成が減ってきているね。ほとんどのチームは、サブプロセスの代わりにメッセージバス型のシステムを選んでいるよ。そっちの方が堅牢で、独立したスケーリングができるからね。

KubernetesのシークレットAPIのロックインってどうなってるの?ちょっと気になったんだけど、そのデプロイメントYAMLをKubernetesのデプロイメント以外に使おうとしてたの?ほとんどのアプリケーションでは、シークレットをアプリにマウントして、環境変数かアプリが読み込むJSONファイルとして注入するのがいいよ。その後、バックエンドでは、暗号化に使うKMSプロバイダーをetcdで設定できるからね。

コマンド設定を使って文字列を取得することもできるよ。これならベンダーログインも必要ないし、別のテンプレートステップもいらない。

これには全部同意。だからこそ、設定には環境変数とdotenvを使い続けてるんだ。めっちゃシンプルで、うまく機能するし、秘密情報管理ツールとも互換性がある。ただ、最近はここ数年sOpsに傾いてるんだ。YAMLはアプリの設定方法を表現するのにすごく良いし、sopsを使うとその一部を暗号化するのが簡単なんだ。ただ、GPGキーの扱いは難しいこともあるけど、Vault/OpenBaoがそれを解決してくれる。でも、ロックインの問題が出てくるんだよね(OpenBaoだと少しマシだけど)。

Varlockをめっちゃおすすめするよ!プロジェクト内の環境変数を管理するのに最適な方法だね。必要なものやオプションのもの、タイプ、どこから取得するかを定義できるんだ。[1] https://varlock.dev/

https://mise.jdx.dev/ を使ってるよ。

どれだけ厄介なことになるかの例を挙げると、前の会社で特定のENV変数がどう設定されているかをデバッグしようとしてたんだ。最初はユーザーの.bashrcとかそんな簡単なものだと思ってたんだけど、すぐに約10層のENV変数の読み込みがあって、最初の数層はこんな感じだった: - 会社全体 - 地域 - ビジネスユニット - 部署 - チーム などなど。結局、どこでその変数が設定されているのかを正確に見るために、bashのデバッグフラグをオンにしなきゃいけなかったよ。

他の高級言語がこれを持ってるかはわからないけど、Node.jsが環境変数のアクセスや変更を正確にトレースするためのコマンドラインフラグを追加したよ。 https://nodejs.org/api/cli.html#--trace-env 環境変数を色んなAPIで設定したり、解除したり、変更したりできるから、複雑なデバッグシナリオではめっちゃ便利そう。

「一つの名前空間で十分だろう。」

環境変数についての最悪な点の一つは、その暗黙的で不透明な性質だよね。ほとんどのアプリケーションは*nixの世界では環境変数に依存してる。もっと明示的でわかりやすい設定ファイルやリモートサービス(consul/etcdなど)やコマンドライン引数がサポートされていても、環境変数は伝統的にサポートされてる。でも、記事にも書いてある通り、これは単なるグローバルなハッシュマップで、子プロセス用にクローンや拡張ができるんだ。1979年には良い設計の決定だったかもしれないけど、今は時々痛い目に遭うこともあるよ。例えば、kubernetesはデフォルトでコンテナの環境を「サービスリンク」で汚染しちゃうし、その「デフォルト」の環境変数がアプリが期待する環境変数と衝突したら、壊れたアプリケーションのデバッグが大変になるよ。 https://kubernetes.io/docs/tutorials/services/connect-applic... 環境変数はどこにでもあって、ITの世界はレガシーの妥協がスタンダードとされて、決して挑戦されないネオコンの世界に生きてるんだよね(こんにちは /bin, /usr/bin, /lib, /usr/lib)。

へへ、hjklをそのネオコンのバケツに入れられるね。Viのhjklは、40年以上前のダム端末のせいでこうなってるんだよね。その端末はNokia N9スマートフォンよりも売れた台数が少なかった。

すごく面白い読書だった。毎日環境変数を使ってるけど、実際にどう機能してるか考えたこともなかった。普段当たり前だと思ってることが、こんなに面白い実装をしてるってわかるね。 :)

お気に入りの環境変数のトリビア(トリビアの単数形ね)は、PS1とか「みんな」が環境変数だと思ってるやつが、実は環境変数じゃなくてシェル変数だってこと。PS1は「env」の出力にも出てこないし!