webアプリ開発における環境変数まわりのベストプラクティス (original) (raw)

追記
文中で .env に依存させないというプラクティスを紹介しました。僕は基本的にはこれでよいと思っていますが、フレームワークによっては.envなどと深く結合して利便性を提供しているものもあります。この場合は無理して.envから脱却せず、うまいこと利用するのもありだなと最近感じています。ただし、フレームワークが.envと深く結合していない場合は、dotenvなどのライブラリを導入するよりも、起動時に環境変数として注入する方式のほうがよいと感じています。


nodejsを例に解説します。nodejsでは環境変数はprocess.env.環境変数名でとりだせます。また、開発環境・テスト環境・本番環境をそれぞれNODE_ENVという環境変数にdevelopment test productionと入れる文化があります。

アプリケーションコードに自分が今いる環境(開発|ステージング|本番)を意識させない

これはつまり、コード内で環境識別変数(今回で言うところのNODE_ENV)によってif分岐を作らないという意味です。各環境にどのような設定が入るかはアプリケーションコード外にその種類分作成しましょう!

bad

アプリケーションコード

if(開発環境){
   const logger = new Logger({
    level: 'debug'
  });
} else if (ステージング環境){
   const logger = new Logger({
    level: 'info
  });
} else if (本番環境){
   const logger = new Logger({
    level: 'error'
  });
}

good

.env.development

LOG_LEVEL=debug

.env.staging

LOG_LEVEL=info

.env.production

LOG_LEVEL=error

アプリケーションコード

const logger = new Logger({
  level: process.env.LOG_LEVEL
});

残念ながら使っているフレームワークやDBクライアントライブラリによっては、開発環境・テスト環境・本番環境の設定を強制するものもあります。その場合はすべての環境に同じ環境変数をいれることで対応しましょう。

bad

sequelizeConfig.js

module.exports = {
  development: {
    username: '開発用のusername',
    password: '開発用のpassword',
    database: '開発用のdatabase',
    host: '開発用のhost',
  },
  test: {
    username: 'testのusername',
    password: 'testのpassword',
    database: 'testのdatabase',
    host: 'testのhost',
  },
  production: {
    username: '本番のusername',
    password: '本番のpassword',
    database: '本番のdatabase',
    host: '本番のhost',
  },
};

good

.env.development

DB_USER_NAME=dev
DB_PASSWORD=password
DB_NAME=development
DB_HOST=localhost...

.env.staging

DB_USER_NAME=stg
DB_PASSWORD=secret
DB_NAME=staging
DB_HOST=...

.env.production

DB_USER_NAME=prod
DB_PASSWORD=super_secret
DB_NAME=prod
DB_HOST=...

sequelizeConfig.js

module.exports = {
  development: {
    username: process.env.DB_USER_NAME,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    host: process.env.DB_HOST,
  },
  test: {
    username: process.env.DB_USER_NAME,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    host: process.env.DB_HOST,
  },
  production: {
    username: process.env.DB_USER_NAME,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME,
    host: process.env.DB_HOST,
  },
};

アプリに自分から環境変数を取りに行かせない(アプリ自体を.envに依存させない)

.envファイルを使っている方や、デフォルトで.envファイルの環境を読み込むフレームワークなど多いかと思います。.envファイル自体は使ってもよいのですが、アプリ側から.envファイルを自動で読み込む設定をoffにします。フレームワークの設定だったり、dotenv系のライブラリだったりするかもしれません。それらをなくしてください。

なぜこのようなことをするかというと、アプリ自体に環境変数を能動的に取得してほしくないからです。これは「アプリケーションコードに自分が今いる環境(開発|ステージング|本番)を意識させない」と似ていますが、アプリ側にどの.envに依存するのか、依存しないのかの判断をさせないための手段です。アプリ側には素直に渡された環境変数のみに依存してほしいのです。こうすることで、アプリがどの環境に依存すべきかの判断ロジックや条件分岐ももたずすみます。

アプリ自体はピュアに保っておき、もし.env系のファイルに依存する必要があるならば、何かしらのライブラリ等を利用して外部から注入してあげます。これは後の「 開発環境では.envの設定を外から注入する」セクションで説明します。

アプリ自体をピュアに保っていると、本番環境の環境変数を設定しやすいというメリットもあります。

bad

// .envから環境変数を読み込み
require('dotenv').config();

console.log(process.env.DB_HOST);

good

// 実際の環境変数を読み込み
console.log(process.env.DB_HOST);

開発環境では.envの設定を外から注入する

アプリ自体の.envファイルへの依存を削除しましたが、.env自体は手軽にローカルで環境をいじるのに使いやすいですね。開発環境は.envファイルを使用して、本番環境ではそのままの環境変数を使いたいです。どうすればいいのでしょうか?

僕の場合は、アプリサーバーの起動スクリプトを分けています。nodejsの場合、package.jsonという設定ファイルのscriptsという項目にコマンドを指定してあげればnpm run コマンド名でコマンドを実行できます。ローカルで実行したいときはnpm run local-start.envを読み込んでサーバーを実行します。本番環境で実行したいときはnpm run startをそのまま実行します。

package.json

{
  "name": "my app",
  "version": "3.6.0",
  "description": "My webpack project",
  "main": "index.js",
  "scripts": {
    // その環境の環境変数をそのまま利用
    "start": "nest start",
    "build": "nest build",
    "test": "jest",
    // env-cmdは.envを読み込んで環境変数に設定してくれるnodejsのライブラリ
    "local-start": "env-cmd -f .env npm run start",
    "local-build": "env-cmd -f .env npm run build",
    "local-test": "env-cmd -f .env.testing npm run test",
  },
}

.envはgitに載せない

念の為こちらも書いておきます。.envは性質上gitにのるとやばい情報を扱うことがおおいので、.gitignoreできちんと指定しておきましょう。

基本.envは機密性の高い項目にはダミーデータしか置かないほうがいいですね。そのために、開発中は機密性の高い設定値を要求する外部サービスは使わずその代わりにローカルでdockerを立ててモックするとよいと思います(AWS_SECRET_KEYとかを設定しない代わりにawsが提供するlocalstackイメージを使うなど)。どうしても外部サービスを使いたい場合は、.env.gitignoreにあることを確認して、ローカルの.envだけ書き換えます。後述する.env.localにはダミーデータのみおきましょう!

bad

good

開発用の環境変数は共有したいけど.envはgitにのせないほうがいい、どうしたらいいんだと思っている人もいるかも知れません。以下のようにすると良いと思います。

  1. .env.localファイルを作成する。こちらはgitで管理する。
  2. 新しい人が入ってくるたびに、.env.local.envとしてコピーする。
  3. 環境変数新しいやつ使いたいなとおもったら、.env.env.local両方を更新する。

アプリ内で必須の環境変数がなければサーバーを止める

nodejsだとprocess.env.環境変数名の型はstringundefined(多言語で言うところのnull)になります。これはそもそも環境変数自体が設定されているかわからないためundefinedになりえるのです。しかしこれだと困ることがあります。アプリや内部で利用しているサービス上必須の設定値がundefinedだと困るのです。必須の設定値が未設定の場合、アプリサーバー自体を起動させないほうが安全です。

bad

const paymentService = new PaymentService({
  // process.env.SECRET_KEYだけだとundefinedになりうるのでコンパイルエラーになる。
  // そこでprocess.env.SECRET_KEYが未設定だとデフォルトで空文字をいれる
  secret: process.env.SECRET_KEY ?? ''
})

good

if(!process.env.SECRET_KEY){
  console.error('環境変数SECRET_KEYが設定されていません!');
  process.exit(); // サーバーとめる
}

const paymentService = new PaymentService({
  secret: process.env.SECRET_KEY
})

process.envといった環境変数アクセス用の変数を直接使わずに、使いやすいようにラップする

'環境変数を扱う場合次の2種類の設定項目があると思います。

「必須な設定項目」は前章「アプリ内で必須の環境変数がなければサーバーを止める」で説明したように、なければサーバーを止める処理をします。つまり以下のような処理がコード上に散らばります。

if(!process.env.SECRET_KEY){
  console.error('環境変数SECRET_KEYが設定されていません!');
  process.exit(); // サーバーとめる
}

const paymentService = new PaymentService({
  secret: process.env.SECRET_KEY
})

「オプショナルな設定項目」はデフォルトの値を指定する必要があります。つまり以下のような処理がコード上に散らばります。また、stringをIntに変える処理も加えるときもあり、コードが複雑になりがちです。また、同じ設定項目に対してデフォルト値は基本同じです。同じコードがいろんなところに散らばりますね...。

let port: number = 5000; // デフォルト値
if(process.env.PAYMENT_PORT){
  // jsはややこしいことにうまくintに変更できなければNaNというnumber型の変数を返す。
  const parsed = Number.parseInt(process.env.PAYMENT_PORT); 
  // NaNでなければ代入。
  if(!Number.isNaN(parsed)){
    port = parsed
  }
}

const paymentService = new PaymentService({
  port: port
})

上記のように、環境変数にアクセスする際のボイラープレートがコードのあちこちに散らばってしまっています。どうせなら、1つにまとめて、ボイラープレートもなくしていきたいところです。そこで、直接環境変数にアクセスせずに、ラッパーを作り、そこで前処理をしてから使うようにします。

設定

config.ts

const convertIntOrDefault = (
  raw: string | undefined,
  defaultValue: number,
): number => {
  if (!raw) return defaultValue;
  const parsed = Number.parseInt(raw, 10);
  if (Number.isNaN(parsed)) return defaultValue;

  return parsed;
};

if (!process.env.HOST_DOMAIN) {
console.error('HOST_DOMAIN environmental variable is missing!');
process.exit();
}

export const appConfig = {
  app: {
    // [必須] サービスのドメイン名
    hostDomain: process.env.HOST_DOMAIN,
    // listenするポート番号
    port: convertIntOrDefault(process.env.PORT, 3000),
  },
}

使うとき

main.ts

import {appConfig} from './config';

console.log(appConfig.app.hostDomain);

フレームワークに設定を扱う機能がある場合、そこに登録してもよさそうですね。

以上webアプリ開発における環境変数のベストプラクティスでした~

おまけ

NestJSようの環境変数まわりの記事も書きました!
https://zenn.dev/dove/articles/2990f0e1eba07e