🛡️
Node.jsのバックエンドアプリケーションで環境変数を管理するベストプラクティス
はじめに
Node.jsでバックエンドアプリケーションを開発する中で、環境変数の管理には多くの課題がありました。主にCloud Runをデプロイ先として使用していましたが、以下のような問題に直面しました。
デプロイ時の課題
- 必要な環境変数の設定を管理画面で失念し、アプリケーション実行時にエラーが発生
これらの問題に対処するため、環境変数を管理する処理を実装しましたが、新たな課題が浮上しました。
- 不要な環境での環境変数設定の要求
- 環境と環境変数の複雑な関係管理によるヒューマンエラー
- 結果として、環境変数が必要な環境での設定漏れ
四苦八苦七転八倒の末に、ようやく最終的な解決策を見出すことができました。今回は、ECサイトのユースケースを例に説明します。
プロジェクトの背景
私のプロジェクトではNode.jsを使用し、APIサーバーとバッチを実装しています。ドメイン層は共有しており、エントリーポイントは以下のように分かれています:
- APIサーバー:
main.ts
- バッチ:
batch.ts
(複数のシナリオが存在)
環境変数の複雑さ
環境変数の必要性は、以下の3つの次元で決定されます:
- 動作環境(本番、ステージング、開発)
- 動作モード(APIサーバー、銀行振込入金確認バッチ、在庫補充発注バッチ)
- 環境変数の種類(銀行APIキー、DB接続情報、メール関連設定など)
この複雑な組み合わせにより、認知コストが非常に高く、ヒューマンエラーが発生しやすい状況でした。
最終的に実現したい要件
- 各環境で必要な環境変数が未設定の場合、デプロイ時にエラー
- 不要な環境変数が未設定でもエラーを発生させない
- 環境変数の静的型付け
- 型の不一致がある場合、デプロイ時にエラー
これらの課題を解決するための最終的なアプローチを共有します。
完成形
環境変数の型定義
type NodeEnvType = 'test' | 'development' | 'production';
type AppEnvType = 'develop' | 'staging' | 'production';
type ServiceEnvType = "api-server" | "bank-detection-batch" | "inventory-refill-batch";
type BankApiKeyType = string; // 銀行のAPIキー
type DbConnectionType = string; // DB接続情報
type MailSenderAddressType = string; // メール送信元アドレス
type AuditMailAddressType = string; // BCCに設定する監査用メールアドレス
type MailSenderApiKeyType = string; // メール送信APIキー
type StagingBasicAuthType = `${string}:${string}`; // ステージング用BASIC認証文字列
環境変数のインターフェース定義
interface EnvironmentConfig {
BANK_API_KEY: BankApiKeyType,
DB_CONNECTION_STRING: DbConnectionType,
MAIL_SENDER_ADDRESS: MailSenderAddressType,
AUDIT_MAIL_ADDRESS: AuditMailAddressType,
MAIL_SENDER_API_KEY: MailSenderApiKeyType,
STAGING_BASIC_AUTH: StagingBasicAuthType,
}
各サービスと環境の組み合わせごとに必要な環境変数を定義
type RequiredEnvVars = {
[service in ServiceEnvType]: {
[env in AppEnvType]: Array<keyof EnvironmentConfig>;
};
};
必要な環境変数の定義
const requiredEnvVars: RequiredEnvVars = {
"api-server": {
production: [
"DB_CONNECTION_STRING",
"MAIL_SENDER_ADDRESS",
"AUDIT_MAIL_ADDRESS",
"MAIL_SENDER_API_KEY",
],
staging: [
"DB_CONNECTION_STRING",
"MAIL_SENDER_ADDRESS",
"MAIL_SENDER_API_KEY",
"STAGING_BASIC_AUTH",
],
develop: [
"DB_CONNECTION_STRING",
"MAIL_SENDER_ADDRESS",
"MAIL_SENDER_API_KEY",
],
},
"bank-detection-batch": {
production: [
"DB_CONNECTION_STRING",
"BANK_API_KEY",
"MAIL_SENDER_ADDRESS",
"AUDIT_MAIL_ADDRESS",
"MAIL_SENDER_API_KEY",
],
staging: [
"DB_CONNECTION_STRING",
"BANK_API_KEY",
"MAIL_SENDER_ADDRESS",
"MAIL_SENDER_API_KEY",
],
develop: [
"DB_CONNECTION_STRING",
"BANK_API_KEY",
"MAIL_SENDER_ADDRESS",
"MAIL_SENDER_API_KEY",
],
},
"inventory-refill-batch": {
production: [
"DB_CONNECTION_STRING",
"MAIL_SENDER_ADDRESS",
"MAIL_SENDER_API_KEY",
],
staging: [
"DB_CONNECTION_STRING",
"MAIL_SENDER_ADDRESS",
"MAIL_SENDER_API_KEY",
],
develop: [
"DB_CONNECTION_STRING",
"MAIL_SENDER_ADDRESS",
"MAIL_SENDER_API_KEY",
],
}
};
環境変数が未設定時のデフォルト値
const defaultValues: EnvironmentConfig = {
BANK_API_KEY: "",
DB_CONNECTION_STRING: "",
MAIL_SENDER_ADDRESS: "",
AUDIT_MAIL_ADDRESS: "",
MAIL_SENDER_API_KEY: "",
STAGING_BASIC_AUTH: ":",
};
環境変数を取り出して型付けをして返す処理
/**
* test, development, production
*/
export const NODE_ENV: NodeEnvType = (function () {
const env = process.env.NODE_ENV;
if (!env) {
throw new Error('NODE_ENV is not defined');
}
if (!['test', 'development', 'production'].includes(env)) {
throw new Error('NODE_ENV is not test, development, production');
}
return env as NodeEnvType;
})();
/**
* develop, staging, production
*/
export const APP_ENV: AppEnvType = (function () {
const env = process.env.APP_ENV;
if (!env) {
throw new Error('APP_ENV is not defined');
}
if (!['develop', 'staging', 'production'].includes(env)) {
throw new Error('APP_ENV is not develop, staging, production');
}
return env as AppEnvType;
})();
/**
* api-server, bank-detection-batch, inventory-refill-batch
*/
export const SERVICE_ENV: ServiceEnvType = (function () {
const env = process.env.SERVICE_ENV;
if (!env) {
throw new Error('SERVICE_ENV is not defined');
}
if (!['api-server', 'bank-detection-batch', 'inventory-refill-batch'].includes(env)) {
throw new Error('SERVICE_ENV is not api-server, bank-detection-batch, inventory-refill-batch');
}
return env as ServiceEnvType;
})()
/**
* 銀行のAPIキー
*/
export const BANK_API_KEY: BankApiKeyType = (function () {
const requiredEnvs = requiredEnvVars[SERVICE_ENV][APP_ENV]
if (!requiredEnvs.includes("BANK_API_KEY")) {
return defaultValues.BANK_API_KEY
}
const env = process.env.BANK_API_KEY;
if (!env) {
throw new Error('BANK_API_KEY is not defined');
}
return env as BankApiKeyType;
})()
/**
* DB接続情報
*/
export const DB_CONNECTION_STRING: DbConnectionType = (function () {
const requiredEnvs = requiredEnvVars[SERVICE_ENV][APP_ENV]
if (!requiredEnvs.includes("DB_CONNECTION_STRING")) {
return defaultValues.DB_CONNECTION_STRING
}
const env = process.env.DB_CONNECTION_STRING;
if (!env) {
throw new Error('DB_CONNECTION_STRING is not defined');
}
return env as DbConnectionType;
})()
/**
* メール送信元アドレス
*/
export const MAIL_SENDER_ADDRESS: MailSenderAddressType = (function () {
const requiredEnvs = requiredEnvVars[SERVICE_ENV][APP_ENV]
if (!requiredEnvs.includes("MAIL_SENDER_ADDRESS")) {
return defaultValues.MAIL_SENDER_ADDRESS
}
const env = process.env.MAIL_SENDER_ADDRESS;
if (!env) {
throw new Error('MAIL_SENDER_ADDRESS is not defined');
}
return env as MailSenderAddressType;
})()
/**
* BCCに設定する監査用メールアドレス
*/
export const AUDIT_MAIL_ADDRESS: AuditMailAddressType = (function () {
const requiredEnvs = requiredEnvVars[SERVICE_ENV][APP_ENV]
if (!requiredEnvs.includes("AUDIT_MAIL_ADDRESS")) {
return defaultValues.AUDIT_MAIL_ADDRESS
}
const env = process.env.AUDIT_MAIL_ADDRESS;
if (!env) {
throw new Error('AUDIT_MAIL_ADDRESS is not defined');
}
return env as AuditMailAddressType;
})()
/**
* メール送信APIキー
*/
export const MAIL_SENDER_API_KEY: MailSenderApiKeyType = (function () {
const requiredEnvs = requiredEnvVars[SERVICE_ENV][APP_ENV]
if (!requiredEnvs.includes("MAIL_SENDER_API_KEY")) {
return defaultValues.MAIL_SENDER_API_KEY
}
const env = process.env.MAIL_SENDER_API_KEY;
if (!env) {
throw new Error('MAIL_SENDER_API_KEY is not defined');
}
return env as MailSenderApiKeyType;
})()
/**
* ステージング用BASIC認証文字列
*/
export const STAGING_BASIC_AUTH: StagingBasicAuthType = (function () {
const requiredEnvs = requiredEnvVars[SERVICE_ENV][APP_ENV]
if (!requiredEnvs.includes("STAGING_BASIC_AUTH")) {
return defaultValues.STAGING_BASIC_AUTH
}
const env = process.env.STAGING_BASIC_AUTH;
if (!env) {
throw new Error('STAGING_BASIC_AUTH is not defined');
}
return env as StagingBasicAuthType;
})()
まとめ
環境変数が必要か、不必要かの全ての複雑性は requiredEnvVars
に集約し、取り出す時にはほぼ同じ処理の繰り返しで済むようになりました。
関数定義を
const hoge = (function(){
// 処理
})()
のようにして、関数定義を即時実行した結果を変数に代入することで、もしある環境変数が必要な環境で設定されていなければ、実行直後にエラーで落ちることになり、ミスに気づけるようになりました。
環境変数を使わない環境ではデフォルト値が返るので、不必要な環境変数を設定しないといけないということも起こりません。
これを呼び出す時には
import {BANK_API_KEY} from './env';
console.log(BANK_API_KEY);
というように呼び出すことができます。
以上です。ありがとうございました。
Discussion