Webアプリケーションにおける環境変数のベストプラクティス&バッドプラクティス
概要
筆者はWebアプリケーションの開発を数年やってきました。
様々なプロダクトで環境変数を扱う際のベストプラクティス, バッドプラクティスを見てきて、いまだに新しいプロジェクトに参画すると不適切な運用がされているケースがよくあります。(多数派と言っていい)
個別に Pull Request などでフィードバックするよりも、記事にまとまっている方がフィードバックにかかる時間を短縮できる&公開することに価値があると感じたので、筆者の環境変数に関する考えをこの記事にまとめます。
とはいえ、既に他の方が webアプリ開発における環境変数まわりのベストプラクティス という素晴らしい記事を公開されているので、大部分はこの記事でカバーできますが、当記事ではそちらの記事では触れられていない点にも言及します。
この記事では特定の言語、フレームワークに依存しない考えを示します。
前提となる考え方
- 環境変数は少なければ数ない方がいい => 多いと認知負荷が高くなる
- 環境変数が多すぎるとサーバーにデプロイした際にエラーとなるケースがある => 筆者は実際に遭遇した
- ローカルで環境変数を個別に変更するのは最小限となるようにしておく => 環境構築にかかるコストを下げるため。コンテナなどを使うようにすればDBの設定なども各開発者の環境に合わせて設定する必要がなくなる
- 環境変数を一覧で見れるようにしておく => 認知負荷を下げるため
- 環境変数は型情報を持たない => アプリケーションで扱う際に型変換が必要である
- 環境変数は常に取得できるとは限らない => アプリケーションから見たらそう。設定漏れなどがあり得る
- 特定の環境でのみ設定する環境変数がある => 本番環境でのみログ基盤にデータを送信するなど
- フロントエンドで秘匿情報が漏れないようにする => フレームワークの設定を用いたり、フロントエンドではなくバックエンドサーバーやBFFなどで秘匿情報を扱う処理を行う
環境変数で定義すべき値
「環境ごとに変えたい値」です。
環境というのは、 local
, test
, staging
, production
などがあります。(プロダクトによって異なる)
それぞれ依存する DB や APIサーバーなどが異なりますし、連携しているSaaSなどが異なるので、それらの URL
や CLIENT_ID
, SECRET_KEY
などを定義します。
それらの多くは第三者に漏洩してはいけない秘匿情報と呼ばれる情報です。
環境変数で定義すべきではない値
「環境ごとに変える必要のない値」です。
以下、具体例です。
- 管理者userのID => DBで管理すべき
- 1ページに表示する記事の件数 => コードに含めるべき
- APIリクエスト時のリトライ回数 => 環境ごとに変える必要がなければ、コードに含めるべき
- 依存するSaaSのURL => 環境ごとに変える必要がなければ、コードに含めるべき
- 構造化されたデータ => ファイルかコードかDBなどで管理すべき
環境変数はただの便利な「データ置き場」ではありません。
1, 2 は自明です。(が、こういうトンデモコードが稀によくある)
3 を環境ごとに変えたいというニーズはほぼないですが、「外から変えれた方が便利」という考えで様々な設定値を環境変数(あるいはDBの設定値テーブルとか作っちゃう)に持たせようとする方もいます。
「なんでもできる!便利!自由度が高い!」ことよりも「特定のことしかできない。変えれない」方が堅牢で表現力豊かな(=認知負荷の低い)コードになります。「変えれない」ということは「間違った値を設定してしまう」「間違って未定義」というミスが混入するリスクが生じないということです。
「4. 依存するSaaSのURL」を外から与えれるようにするのはケースによります。
例えば、SaaSのURLが https://zenn.dev/api/v1
, https://test.zenn.dev/api/v1
と本番環境とテスト環境で異なる場合、環境変数で定義するのも良いと思います。
しかし、すべての環境で https://zenn.dev/api/v1
というひとつのURLを参照する場合は環境変数ではなく、コードの定数に含めるべきです。
ほとんどの理由は先述の 3 と同じです。URLが環境変数で定義されている場合、環境ごとにURLが異なるのかな?違う値が設定される可能性があるのか?と誤った推測をしてしまいかねないです。
また、「APIのバージョンが https://zenn.dev/api/v1
から https://zenn.dev/api/v2
に上がったときに環境変数を変えるだけで対応できる」といった意見もあるかもしれませんが、実際はAPIのバージョンが上がったときはIFや戻り値なども変わったり、使用するライブラリも変わるので、それらの変更と併せてコードを変更してデプロイするべきです。
「5. 構造化されたデータ」は JSON のような構造のデータです。このような値を環境変数に入れてる時点で多くの場合もっと手前の設計がミスっています。
DBに値を持たせるか、S3などのストレージに値を持たせるか、秘匿情報でなければ環境ごとに設定値を記述した production.json
などのファイルにGitに含めて環境変数の値によって出し分けることでも代替できますが、それも含めてもっと手前の設計がミスってないか疑った方がいいです。( Rails の
development.yml
みたいなのは悪くないと思います。)
各環境での環境変数の定義の仕方
ローカル開発環境では .env
から環境変数を読み込みます。
本番環境, ステージング環境などのサーバー上で稼働する環境ではサーバーで直接環境変数を定義します。これは各ホスティングサービスの手法に則って行います。
ローカル開発環境で .env
を使う理由は以下の通りです。
- 環境変数を一覧で見れるため
- ローカルで環境変数を個別に変更する手間を軽減できるため
本番環境, ステージング環境などで .env
を使わないのは、秘匿情報を Git に含めないためです。
.env.production
, .env.staging
などを Git に含めてしまうとソースコードが誤って公開・流出したときに様々な情報が流出する恐れがあります。
末端の開発メンバーにも必要以上に権限を与えてセキュリティリスクを高めることになってしまうので止めましょう。
.env
を使うライブラリは開発環境でのみ読み込むように devDependencies
に追加します。
.env の使い方
先述の通り、ローカル開発環境でのみ .env
を使います。
.env
の値は各開発者によって一部変更する場合があるので、ローカルで使う .env
も Git に含めません。
運用方法は以下の通りです。
-
.gitignore
に.env
を追加する -
.env.local_example
を Git の管理対象にする(ファイル名は役割がわかればなんでもいいけど、ライブラリによっては特定のファイル名の.env
を勝手に読み込んでしまうので注意が必要) -
README.md
のローカル開発環境構築手順のセクションに以下の手順を追記する
# ローカル開発環境構築手順
<!-- 省略 -->
## 環境変数を設定する
\```bash
cp .env.local_example .env
\```
`.env` で定義されている環境変数の値を更新する。
手順に書いてある「環境変数の値を更新する」というのは説明が十分ではないので、どの値をどのように更新するかは .env
にコメントで示します。
.env
では各値に必ずコメントを書くくらいでもいいと思います。
「この値は何か?」「ローカルではどのような値をセットすべきか?」の二点をコメントで示せると環境構築時に迷わなくて済みます。
# このファイルはlocal環境で環境変数を設定する .env の雛形です。
# .env というファイル名でコピーして、値を適宜変更して使用してください。
# 秘匿情報は env.local_example に書かず、ダミーの値を書いてください。
# データベースのURL
# localでDockerのDBを参照する場合は変更不要です
DATABASE_URL="postgres://username:password@localhost:5432/mydatabase"
.env
では値をクオートで囲わなくても機能しますが、以下の理由からクオートで囲うことをおすすめします。
環境変数は型情報を持たず、アプリケーションではすべて文字列型として扱うため、数値や真偽値は型変換して扱います。
例えば、環境変数で false
という値を設定していたとしても、それは真偽値型の false
ではなく、文字列型の 'false'
です。
これを型変換せずに判定に用いるとバグの原因になりかねないので、実装者に文字列型であることを意識させるためにクオートで囲います。
ロジックから直接環境変数を参照しない
アプリケーションの各ロジックから直接環境変数を参照しないようにします。
constants.ts
や config.ts
といったファイルを作成し、すべての環境変数はそのファイルで読み込む。
ロジックはそのファイルから値を取得します。
これは以下のメリットがあります。
- すべての環境変数を一箇所で一覧できる
-
constants.ts
でデフォルト値の設定や型変換をしておけば、ロジックでそれらの処理を行う必要がなくなる(=型変換漏れなどのミス防止になる) - 「この値は本番環境, ステージング環境でしか使わない」などのコメントを残せる
サーバー起動時に指定した環境変数がなければ起動失敗するようにする
サーバー起動時に指定した環境変数がなければ起動失敗するようにします。
起動中にランタイムエラーとなるよりも、起動失敗してデプロイに失敗する方がずっと安全なためです。
各フレームワークによってやり方は異なるので具体的な方法は割愛します。
一時的に設定値を持たせたい場合に環境変数を使う
一時的なキャンペーン, 施策で環境変数を使うこともあります。
治安の良い方法ではないですが、「スピードが求められる&一時的にしか使わない」ような場合にDBにカラムを追加してマイグレーションするなどは非効率なので、悪手とわかりつつも選択肢としては検討できます。
「何に使われているのかわからない&消していいのかわからない環境変数」が残りつづけてしまうのは避けるべきなので、環境変数を使用する箇所に使途や削除時期などをコメントを残して、プロジェクト管理ツールなどで削除するタスクをリマインダーに追加しておいた方がいいです。
Feature Flag
(※1) を使う場合も同様の対応がおすすめです。
※1. Feature Flag
についてはこちらの記事 開発生産性を上げるシンプルな仕組み、Feature Flagの使いどころ をご覧ください。
おわりに
環境変数に関して、パッと思いつくことを列挙しました。
この記事の内容は個人的な見解であり、唯一無二の正解ではないです。
ご意見などあればコメントでご教示いただけると嬉しいです。
Discussion