Skilore

環境設定

環境設定はアプリケーションの安全な運用の基盤。環境変数の管理、Feature Flags、設定の階層化、シークレット管理まで、本番環境で安全に設定を管理するベストプラクティスを習得する。

159 分で読めます79,324 文字

環境設定

環境設定はアプリケーションの安全な運用の基盤。環境変数の管理、Feature Flags、設定の階層化、シークレット管理まで、本番環境で安全に設定を管理するベストプラクティスを習得する。

前提知識

このガイドを最大限に活用するために、以下の知識を事前に習得しておくことを推奨します。

  • 環境変数の概念: OSレベルの環境変数とプロセスへの注入方法
  • 12-Factor Appの原則: 特に第3の要素「設定」の理解

この章で学ぶこと

  • 環境変数の設計と安全な管理を理解する
  • 環境ごとの設定分離パターンを習得する
  • Feature Flags の実装と運用を把握する
  • シークレット管理のベストプラクティスを学ぶ
  • 12-Factor App に基づく設定管理を理解する
  • CI/CD パイプラインにおける環境変数の受け渡しを習得する
  • 設定のバリデーションとフェイルファスト戦略を実装する
  • マルチ環境運用でのトラブルシューティング手法を身につける

1. 環境設定の基本概念

1.1 なぜ環境設定が重要なのか

アプリケーション開発において、環境設定(Configuration Management)は見過ごされがちだが、セキュリティ、運用安定性、開発効率のすべてに直結する極めて重要な領域である。

環境設定が引き起こすインシデントの例:

  ケース1: 本番 DB のクレデンシャルが GitHub に流出
    原因: .env ファイルを誤って Git にコミット
    影響: データベース全体が外部に露出、ユーザーデータ漏洩
    対策: .gitignore の徹底、pre-commit hook でのチェック

  ケース2: ステージング環境が本番 API を呼び出す
    原因: 環境変数の設定ミスで API_URL が本番を指していた
    影響: テストデータが本番に混入、顧客に影響
    対策: 環境別設定の厳密な分離、起動時バリデーション

  ケース3: Feature Flag の設定漏れで未完成機能が本番に露出
    原因: デプロイ時に Feature Flag の環境変数を設定し忘れた
    影響: 未完成の UI がユーザーに表示、バグ報告殺到
    対策: デフォルト値の適切な設定、デプロイチェックリスト

  ケース4: 本番環境でデバッグモードが有効のまま
    原因: NODE_ENV が development のままデプロイされた
    影響: スタックトレースが外部に露出、セキュリティリスク
    対策: 起動時の環境チェック、CI/CD での自動検証

1.2 12-Factor App と設定管理

Heroku のエンジニアが提唱した 12-Factor App の原則において、設定(Config)は第3の要素として定義されている。この原則はモダンなクラウドネイティブアプリケーションの基礎となっている。

12-Factor App における設定の原則:

  ① 設定はコードから厳密に分離する
     - 環境変数として外部から注入
     - 設定ファイルをコードリポジトリに含めない
     - 同じコードベースがすべての環境で動作する

  ② 環境間の差異は設定のみで表現する
     - development / staging / production の違いは設定だけ
     - コードの条件分岐で環境を判定しない
     - ビルド成果物は全環境で同一

  ③ 設定はデプロイごとに変わりうる
     - コードの変更なしに設定を変更できる
     - 再デプロイなしでの設定変更が可能
     - 設定の変更履歴を追跡できる

12-Factor App の 12 の要素:
  I.    コードベース(一つのコードベース、複数のデプロイ)
  II.   依存関係(明示的に宣言し分離する)
  III.  設定(環境変数に格納する)← ★本章のテーマ
  IV.   バックエンドサービス(アタッチされたリソースとして扱う)
  V.    ビルド・リリース・実行(3つのステージを厳密に分離する)
  VI.   プロセス(ステートレスなプロセスとして実行する)
  VII.  ポートバインディング(ポートバインディングでサービスを公開する)
  VIII. 並行性(プロセスモデルでスケールアウトする)
  IX.   廃棄容易性(高速な起動とグレースフルシャットダウン)
  X.    開発/本番一致(開発・ステージング・本番を可能な限り一致させる)
  XI.   ログ(ログをイベントストリームとして扱う)
  XII.  管理プロセス(管理タスクをワンオフプロセスとして実行する)

1.3 環境の階層と種類

典型的な環境の階層:
local (開発者のマシン)
├── 個人の .env.local で設定を上書き
├── ホットリロード有効
└── デバッグツール有効
development (共有開発環境)
├── 開発チーム全体で共有するサーバー
├── テスト用の外部サービスに接続
└── 本番に近い構成だがリソースは最小限
staging (ステージング環境)
├── 本番とほぼ同一の構成
├── 本番データのサブセットで動作
├── QA テスト、受け入れテストを実施
└── 本番デプロイ前の最終確認
production (本番環境)
├── 実際のユーザーがアクセスする環境
├── 最高レベルのセキュリティ設定
├── 監視・アラート完備
└── パフォーマンス最適化済み
追加の環境(大規模プロジェクトの場合):
  ├── preview / PR環境: PR ごとに自動生成される一時的な環境
  ├── canary: 本番の一部トラフィックのみ受ける環境
  ├── sandbox: 外部パートナー向けのテスト環境
  └── disaster-recovery: 災害時に切り替える予備環境

.env ファイルの優先順位(Next.js の場合):
  .env                  ← デフォルト(全環境共通)
  .env.local            ← ローカルのオーバーライド(.gitignore)
  .env.development      ← 開発環境固有
  .env.development.local← 開発環境のローカルオーバーライド
  .env.staging          ← ステージング固有
  .env.production       ← 本番環境固有
  .env.production.local ← 本番のローカルオーバーライド(.gitignore)

  読み込み優先順位(後勝ち):
  .env < .env.local < .env.[NODE_ENV] < .env.[NODE_ENV].local

2. 環境変数の設計

2.1 命名規則とプレフィックス

環境変数の命名は一貫性を持たせることが重要である。明確な命名規則を定めることで、変数の用途が一目でわかり、設定ミスを防止できる。

命名規則の基本原則:

  ✅ 推奨パターン:
    SCREAMING_SNAKE_CASE を使用
    ├── DATABASE_URL          → データベース接続先
    ├── REDIS_HOST            → Redis ホスト名
    ├── AWS_ACCESS_KEY_ID     → AWS アクセスキー
    ├── SMTP_PORT             → メールサーバーポート
    └── LOG_LEVEL             → ログレベル

  ✅ プレフィックスによる分類:
    サービス別:
    ├── DB_HOST, DB_PORT, DB_NAME, DB_USER, DB_PASSWORD
    ├── REDIS_URL, REDIS_PASSWORD, REDIS_DB
    ├── AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
    ├── STRIPE_PUBLIC_KEY, STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET
    ├── SENDGRID_API_KEY, SENDGRID_FROM_EMAIL
    └── SENTRY_DSN, SENTRY_ENVIRONMENT

    機能別:
    ├── ENABLE_ANALYTICS      → 分析機能の有効/無効
    ├── ENABLE_RATE_LIMIT     → レート制限の有効/無効
    ├── IS_MAINTENANCE_MODE   → メンテナンスモード判定
    ├── HAS_PREMIUM_FEATURES  → プレミアム機能の有無
    └── MAX_UPLOAD_SIZE       → アップロード上限

    公開範囲別(Next.js):
    ├── NEXT_PUBLIC_API_URL   → クライアントに公開
    ├── NEXT_PUBLIC_GA_ID     → Google Analytics ID
    ├── DATABASE_URL          → サーバーのみ(機密)
    └── JWT_SECRET            → サーバーのみ(機密)

  ❌ アンチパターン:
    ├── KEY           → 何のキーかわからない
    ├── SECRET        → 何のシークレットかわからない
    ├── URL           → 何の URL かわからない
    ├── password      → snake_case ではない
    ├── ApiKey        → camelCase は使わない
    └── my-config     → ハイフンは使えない(シェルで問題)

フレームワーク別の公開プレフィックス:
フレームワーク公開プレフィックス非公開(サーバー)
Next.jsNEXT_PUBLIC_プレフィックスなし
ViteVITE_プレフィックスなし
Create React AppREACT_APP_プレフィックスなし
Nuxt.jsNUXT_PUBLIC_NUXT_ or なし
SvelteKitPUBLIC_プレフィックスなし
Remixなし(全てサーバー)全て
AstroPUBLIC_プレフィックスなし

2.2 型安全な環境変数管理(Zod)

TypeScript プロジェクトでは、Zod を使って環境変数にスキーマバリデーションを適用することで、型安全性を確保し、起動時に設定ミスを早期検出できる。

// ============================================
// config/env.ts - 型安全な環境変数管理
// ============================================
import { z } from 'zod';
 
// ---- サーバーサイド環境変数スキーマ ----
const serverEnvSchema = z.object({
  // アプリケーション基本設定
  NODE_ENV: z.enum(['development', 'staging', 'production', 'test'])
    .default('development'),
  PORT: z.coerce.number().int().min(1).max(65535).default(3000),
  HOST: z.string().default('0.0.0.0'),
 
  // データベース
  DATABASE_URL: z.string().url()
    .refine(url => url.startsWith('postgresql://') || url.startsWith('postgres://'), {
      message: 'DATABASE_URL must be a PostgreSQL connection string',
    }),
  DATABASE_POOL_MIN: z.coerce.number().int().min(1).default(2),
  DATABASE_POOL_MAX: z.coerce.number().int().min(1).default(10),
  DATABASE_SSL: z.coerce.boolean().default(true),
 
  // Redis
  REDIS_URL: z.string().url().optional(),
  REDIS_PASSWORD: z.string().optional(),
  REDIS_DB: z.coerce.number().int().min(0).max(15).default(0),
 
  // 認証
  JWT_SECRET: z.string().min(32, 'JWT_SECRET must be at least 32 characters'),
  JWT_EXPIRES_IN: z.string().default('7d'),
  SESSION_SECRET: z.string().min(32).optional(),
 
  // 外部サービス
  STRIPE_SECRET_KEY: z.string().startsWith('sk_'),
  STRIPE_WEBHOOK_SECRET: z.string().startsWith('whsec_'),
  SENDGRID_API_KEY: z.string().startsWith('SG.'),
  SENTRY_DSN: z.string().url().optional(),
 
  // AWS
  AWS_REGION: z.string().default('ap-northeast-1'),
  AWS_ACCESS_KEY_ID: z.string().optional(),
  AWS_SECRET_ACCESS_KEY: z.string().optional(),
  S3_BUCKET_NAME: z.string().optional(),
 
  // 機能フラグ
  ENABLE_RATE_LIMIT: z.coerce.boolean().default(true),
  ENABLE_CORS: z.coerce.boolean().default(true),
  ENABLE_SWAGGER: z.coerce.boolean().default(false),
 
  // ログ
  LOG_LEVEL: z.enum(['fatal', 'error', 'warn', 'info', 'debug', 'trace'])
    .default('info'),
  LOG_FORMAT: z.enum(['json', 'pretty']).default('json'),
});
 
// ---- クライアントサイド環境変数スキーマ ----
const clientEnvSchema = z.object({
  NEXT_PUBLIC_API_URL: z.string().url(),
  NEXT_PUBLIC_APP_URL: z.string().url(),
  NEXT_PUBLIC_GA_ID: z.string().startsWith('G-').optional(),
  NEXT_PUBLIC_SENTRY_DSN: z.string().url().optional(),
  NEXT_PUBLIC_STRIPE_PUBLIC_KEY: z.string().startsWith('pk_'),
  NEXT_PUBLIC_ENABLE_ANALYTICS: z.coerce.boolean().default(false),
  NEXT_PUBLIC_APP_VERSION: z.string().default('0.0.0'),
});
 
// ---- バリデーションとエクスポート ----
function validateEnv<T extends z.ZodType>(
  schema: T,
  env: Record<string, string | undefined>,
  label: string
): z.infer<T> {
  const result = schema.safeParse(env);
 
  if (!result.success) {
    const errors = result.error.flatten().fieldErrors;
    const errorMessages = Object.entries(errors)
      .map(([key, msgs]) => `  ${key}: ${(msgs as string[]).join(', ')}`)
      .join('\n');
 
    console.error(`\n${label} の環境変数バリデーションエラー:\n${errorMessages}\n`);
 
    // 開発環境では詳細を表示、本番では最小限の情報で停止
    if (process.env.NODE_ENV === 'production') {
      console.error('本番環境の設定エラーにより起動を中止します。');
    } else {
      console.error('必要な環境変数を .env.local に設定してください。');
      console.error('参考: .env.example を確認してください。');
    }
 
    process.exit(1);
  }
 
  return result.data;
}
 
// サーバーサイドでのみ実行(クライアントバンドルに含めない)
export const serverEnv = typeof window === 'undefined'
  ? validateEnv(serverEnvSchema, process.env, 'サーバー')
  : ({} as z.infer<typeof serverEnvSchema>);
 
// クライアントサイドでも利用可能
export const clientEnv = validateEnv(clientEnvSchema, process.env, 'クライアント');
 
// 統合型のエクスポート
export const env = { ...serverEnv, ...clientEnv };
 
// 型のエクスポート
export type ServerEnv = z.infer<typeof serverEnvSchema>;
export type ClientEnv = z.infer<typeof clientEnvSchema>;

2.3 環境変数バリデーションのテスト

// ============================================
// config/__tests__/env.test.ts
// ============================================
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { z } from 'zod';
 
// テスト用のスキーマ(本番と同じ定義を使用)
const envSchema = z.object({
  NODE_ENV: z.enum(['development', 'production', 'test']),
  DATABASE_URL: z.string().url(),
  JWT_SECRET: z.string().min(32),
  PORT: z.coerce.number().int().min(1).max(65535).default(3000),
});
 
describe('環境変数バリデーション', () => {
  const validEnv = {
    NODE_ENV: 'production',
    DATABASE_URL: 'postgresql://user:pass@localhost:5432/mydb',
    JWT_SECRET: 'a-very-long-secret-key-that-is-at-least-32-characters',
    PORT: '8080',
  };
 
  it('有効な環境変数が正しくパースされること', () => {
    const result = envSchema.safeParse(validEnv);
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.PORT).toBe(8080); // 文字列から数値に変換
      expect(result.data.NODE_ENV).toBe('production');
    }
  });
 
  it('無効な NODE_ENV が拒否されること', () => {
    const result = envSchema.safeParse({
      ...validEnv,
      NODE_ENV: 'invalid',
    });
    expect(result.success).toBe(false);
  });
 
  it('DATABASE_URL が URL 形式でない場合に拒否されること', () => {
    const result = envSchema.safeParse({
      ...validEnv,
      DATABASE_URL: 'not-a-url',
    });
    expect(result.success).toBe(false);
  });
 
  it('JWT_SECRET が短すぎる場合に拒否されること', () => {
    const result = envSchema.safeParse({
      ...validEnv,
      JWT_SECRET: 'short',
    });
    expect(result.success).toBe(false);
  });
 
  it('PORT のデフォルト値が適用されること', () => {
    const { PORT, ...envWithoutPort } = validEnv;
    const result = envSchema.safeParse(envWithoutPort);
    expect(result.success).toBe(true);
    if (result.success) {
      expect(result.data.PORT).toBe(3000);
    }
  });
 
  it('PORT が範囲外の場合に拒否されること', () => {
    const result = envSchema.safeParse({
      ...validEnv,
      PORT: '70000',
    });
    expect(result.success).toBe(false);
  });
});

2.4 .env.example の管理

プロジェクトには必ず .env.example を含め、必要な環境変数の一覧とその説明を記載する。

# ============================================
# .env.example
# このファイルを .env.local にコピーして値を設定してください
# cp .env.example .env.local
# ============================================
 
# ---- アプリケーション基本設定 ----
NODE_ENV=development
PORT=3000
HOST=0.0.0.0
 
# ---- データベース ----
# PostgreSQL の接続文字列
# ローカル: postgresql://postgres:password@localhost:5432/myapp_dev
# Docker: postgresql://postgres:password@db:5432/myapp_dev
DATABASE_URL=postgresql://postgres:password@localhost:5432/myapp_dev
DATABASE_POOL_MIN=2
DATABASE_POOL_MAX=10
DATABASE_SSL=false
 
# ---- Redis ----
# ローカル: redis://localhost:6379
# Docker: redis://redis:6379
REDIS_URL=redis://localhost:6379
REDIS_DB=0
 
# ---- 認証 ----
# 32文字以上のランダムな文字列を設定
# 生成コマンド: openssl rand -base64 48
JWT_SECRET=your-jwt-secret-at-least-32-characters-long
JWT_EXPIRES_IN=7d
 
# ---- Stripe ----
# https://dashboard.stripe.com/test/apikeys からテストキーを取得
STRIPE_SECRET_KEY=sk_test_xxxxxxxxxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxxxxxx
NEXT_PUBLIC_STRIPE_PUBLIC_KEY=pk_test_xxxxxxxxxxxx
 
# ---- メール(SendGrid)----
# https://app.sendgrid.com/settings/api_keys から取得
SENDGRID_API_KEY=SG.xxxxxxxxxxxx
 
# ---- エラー監視(Sentry)----
# https://sentry.io から DSN を取得
SENTRY_DSN=https://xxxxx@sentry.io/xxxxx
NEXT_PUBLIC_SENTRY_DSN=https://xxxxx@sentry.io/xxxxx
 
# ---- AWS ----
AWS_REGION=ap-northeast-1
AWS_ACCESS_KEY_ID=your-access-key-id
AWS_SECRET_ACCESS_KEY=your-secret-access-key
S3_BUCKET_NAME=myapp-dev-uploads
 
# ---- フロントエンド公開設定 ----
NEXT_PUBLIC_API_URL=http://localhost:3001
NEXT_PUBLIC_APP_URL=http://localhost:3000
NEXT_PUBLIC_GA_ID=G-XXXXXXXXXX
NEXT_PUBLIC_ENABLE_ANALYTICS=false
NEXT_PUBLIC_APP_VERSION=0.0.0
 
# ---- 機能フラグ ----
ENABLE_RATE_LIMIT=false
ENABLE_CORS=true
ENABLE_SWAGGER=true
 
# ---- ログ ----
LOG_LEVEL=debug
LOG_FORMAT=pretty

2.5 環境変数の自動チェックスクリプト

// ============================================
// scripts/check-env.ts
// プロジェクト起動前に環境変数の整合性をチェックする
// ============================================
import fs from 'fs';
import path from 'path';
 
interface EnvCheckResult {
  missing: string[];
  empty: string[];
  warnings: string[];
}
 
function parseEnvFile(filePath: string): Map<string, string> {
  const envMap = new Map<string, string>();
 
  if (!fs.existsSync(filePath)) {
    return envMap;
  }
 
  const content = fs.readFileSync(filePath, 'utf-8');
  const lines = content.split('\n');
 
  for (const line of lines) {
    const trimmed = line.trim();
    // コメント行と空行をスキップ
    if (trimmed === '' || trimmed.startsWith('#')) continue;
 
    const equalIndex = trimmed.indexOf('=');
    if (equalIndex === -1) continue;
 
    const key = trimmed.substring(0, equalIndex).trim();
    const value = trimmed.substring(equalIndex + 1).trim();
    envMap.set(key, value);
  }
 
  return envMap;
}
 
function checkEnv(): EnvCheckResult {
  const projectRoot = process.cwd();
  const exampleEnv = parseEnvFile(path.join(projectRoot, '.env.example'));
  const localEnv = parseEnvFile(path.join(projectRoot, '.env.local'));
  const envFile = parseEnvFile(path.join(projectRoot, '.env'));
 
  // 実効的な環境変数: .env → .env.local → process.env
  const effectiveEnv = new Map([...envFile, ...localEnv]);
 
  const result: EnvCheckResult = {
    missing: [],
    empty: [],
    warnings: [],
  };
 
  for (const [key, exampleValue] of exampleEnv) {
    const actualValue = effectiveEnv.get(key) || process.env[key];
 
    if (actualValue === undefined) {
      result.missing.push(key);
    } else if (actualValue === '' || actualValue === exampleValue) {
      // 値が空またはサンプル値のまま
      if (key.includes('SECRET') || key.includes('KEY') || key.includes('PASSWORD')) {
        result.warnings.push(`${key}: サンプル値のまま or 空です(機密情報)`);
      } else if (actualValue === '') {
        result.empty.push(key);
      }
    }
  }
 
  // NEXT_PUBLIC_ でないのにクライアントで使おうとしている変数のチェック
  for (const [key] of effectiveEnv) {
    if (key.includes('SECRET') || key.includes('PASSWORD') || key.includes('PRIVATE')) {
      if (key.startsWith('NEXT_PUBLIC_')) {
        result.warnings.push(
          `${key}: 機密情報が NEXT_PUBLIC_ プレフィックスで公開されています!`
        );
      }
    }
  }
 
  return result;
}
 
// 実行
const result = checkEnv();
 
if (result.missing.length > 0) {
  console.error('\n未設定の環境変数:');
  result.missing.forEach(key => console.error(`  - ${key}`));
}
 
if (result.empty.length > 0) {
  console.warn('\n空の環境変数:');
  result.empty.forEach(key => console.warn(`  - ${key}`));
}
 
if (result.warnings.length > 0) {
  console.warn('\n警告:');
  result.warnings.forEach(msg => console.warn(`  - ${msg}`));
}
 
if (result.missing.length === 0 && result.warnings.length === 0) {
  console.log('\nすべての環境変数が正しく設定されています。');
}
 
// 必須の環境変数が不足している場合は終了コード 1 で終了
if (result.missing.length > 0) {
  process.exit(1);
}

2.6 各フレームワークでの環境変数の使い方

// ============================================
// Next.js での環境変数の使い方
// ============================================
 
// 1. サーバーコンポーネント(App Router)
// サーバーサイドの環境変数に直接アクセス可能
async function ServerComponent() {
  // サーバーサイドのみ: OK
  const dbUrl = process.env.DATABASE_URL;
  const apiKey = process.env.STRIPE_SECRET_KEY;
 
  // NEXT_PUBLIC_ もサーバーで利用可能
  const publicApiUrl = process.env.NEXT_PUBLIC_API_URL;
 
  const data = await fetch(publicApiUrl + '/api/data', {
    headers: { Authorization: `Bearer ${apiKey}` },
  });
 
  return <div>{/* ... */}</div>;
}
 
// 2. クライアントコンポーネント
'use client';
function ClientComponent() {
  // クライアントサイドでは NEXT_PUBLIC_ のみアクセス可能
  const apiUrl = process.env.NEXT_PUBLIC_API_URL;     // OK
  // const secret = process.env.DATABASE_URL;          // undefined(安全)
 
  return <div>API: {apiUrl}</div>;
}
 
// 3. API Route(App Router)
// app/api/webhook/route.ts
import { NextRequest, NextResponse } from 'next/server';
 
export async function POST(request: NextRequest) {
  // サーバーサイドなので全環境変数にアクセス可能
  const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
 
  // Webhook の署名検証
  const signature = request.headers.get('stripe-signature');
  // ...
  return NextResponse.json({ received: true });
}
 
// 4. next.config.js での環境変数
/** @type {import('next').NextConfig} */
const nextConfig = {
  env: {
    // ビルド時に埋め込まれる(非推奨: NEXT_PUBLIC_ を使うべき)
    CUSTOM_VAR: process.env.CUSTOM_VAR,
  },
  // ランタイム設定(サーバーサイドのみ)
  serverRuntimeConfig: {
    apiSecret: process.env.API_SECRET,
  },
  // パブリックランタイム設定(クライアントからもアクセス可能)
  publicRuntimeConfig: {
    apiUrl: process.env.NEXT_PUBLIC_API_URL,
  },
};
 
module.exports = nextConfig;
// ============================================
// Vite での環境変数の使い方
// ============================================
 
// vite.config.ts
import { defineConfig, loadEnv } from 'vite';
 
export default defineConfig(({ mode }) => {
  // mode に応じた .env ファイルを読み込む
  const env = loadEnv(mode, process.cwd(), '');
 
  return {
    define: {
      // カスタム変数をグローバルに定義
      __APP_VERSION__: JSON.stringify(env.npm_package_version),
    },
    server: {
      port: parseInt(env.VITE_DEV_PORT || '5173'),
    },
  };
});
 
// コンポーネントでの使用
function App() {
  // VITE_ プレフィックスの変数のみアクセス可能
  const apiUrl = import.meta.env.VITE_API_URL;
  const mode = import.meta.env.MODE; // 'development' | 'production'
  const isDev = import.meta.env.DEV; // boolean
  const isProd = import.meta.env.PROD; // boolean
 
  return <div>API: {apiUrl}</div>;
}
 
// 型定義(env.d.ts)
/// <reference types="vite/client" />
interface ImportMetaEnv {
  readonly VITE_API_URL: string;
  readonly VITE_APP_TITLE: string;
  readonly VITE_GA_ID?: string;
  readonly VITE_ENABLE_MOCK: string;
}
 
interface ImportMeta {
  readonly env: ImportMetaEnv;
}

3. 設定の分離パターン

3.1 環境別設定ファイルパターン

// ============================================
// config/index.ts - 環境別設定の統合管理
// ============================================
 
// 基本設定の型定義
interface AppConfig {
  app: {
    name: string;
    version: string;
    url: string;
  };
  api: {
    baseUrl: string;
    timeout: number;
    retryCount: number;
    retryDelay: number;
  };
  database: {
    poolMin: number;
    poolMax: number;
    ssl: boolean;
    logging: boolean;
  };
  cache: {
    ttl: number;
    maxItems: number;
    strategy: 'memory' | 'redis';
  };
  features: {
    analytics: boolean;
    debugMode: boolean;
    maintenanceMode: boolean;
    rateLimit: boolean;
  };
  security: {
    corsOrigins: string[];
    rateLimitWindow: number;
    rateLimitMax: number;
    csrfEnabled: boolean;
  };
  logging: {
    level: string;
    format: 'json' | 'pretty';
    destination: 'stdout' | 'file' | 'both';
  };
}
 
// 全環境共通のデフォルト設定
const defaultConfig: AppConfig = {
  app: {
    name: 'MyApp',
    version: process.env.npm_package_version || '0.0.0',
    url: 'http://localhost:3000',
  },
  api: {
    baseUrl: 'http://localhost:3001',
    timeout: 30000,
    retryCount: 3,
    retryDelay: 1000,
  },
  database: {
    poolMin: 2,
    poolMax: 10,
    ssl: false,
    logging: false,
  },
  cache: {
    ttl: 300,
    maxItems: 1000,
    strategy: 'memory',
  },
  features: {
    analytics: false,
    debugMode: false,
    maintenanceMode: false,
    rateLimit: true,
  },
  security: {
    corsOrigins: ['http://localhost:3000'],
    rateLimitWindow: 15 * 60 * 1000, // 15分
    rateLimitMax: 100,
    csrfEnabled: true,
  },
  logging: {
    level: 'info',
    format: 'json',
    destination: 'stdout',
  },
};
 
// 環境別のオーバーライド設定
const envOverrides: Record<string, Partial<AppConfig>> = {
  development: {
    api: {
      ...defaultConfig.api,
      timeout: 60000, // 開発時は長めのタイムアウト
    },
    database: {
      ...defaultConfig.database,
      logging: true, // SQL ログ出力
    },
    cache: {
      ...defaultConfig.cache,
      ttl: 0, // 開発時はキャッシュ無効
    },
    features: {
      ...defaultConfig.features,
      debugMode: true,
      rateLimit: false, // 開発時はレート制限なし
    },
    logging: {
      level: 'debug',
      format: 'pretty',
      destination: 'stdout',
    },
  },
  staging: {
    app: {
      ...defaultConfig.app,
      url: 'https://staging.example.com',
    },
    api: {
      ...defaultConfig.api,
      baseUrl: 'https://staging-api.example.com',
      timeout: 15000,
    },
    database: {
      ...defaultConfig.database,
      ssl: true,
    },
    cache: {
      ...defaultConfig.cache,
      ttl: 60,
      strategy: 'redis',
    },
    features: {
      ...defaultConfig.features,
      analytics: true,
      debugMode: true,
    },
    security: {
      ...defaultConfig.security,
      corsOrigins: ['https://staging.example.com'],
    },
  },
  production: {
    app: {
      ...defaultConfig.app,
      url: 'https://www.example.com',
    },
    api: {
      ...defaultConfig.api,
      baseUrl: 'https://api.example.com',
      timeout: 10000,
      retryCount: 5,
    },
    database: {
      ...defaultConfig.database,
      poolMin: 5,
      poolMax: 20,
      ssl: true,
    },
    cache: {
      ...defaultConfig.cache,
      ttl: 600,
      maxItems: 10000,
      strategy: 'redis',
    },
    features: {
      ...defaultConfig.features,
      analytics: true,
    },
    security: {
      ...defaultConfig.security,
      corsOrigins: ['https://www.example.com', 'https://admin.example.com'],
      rateLimitMax: 50,
    },
    logging: {
      level: 'warn',
      format: 'json',
      destination: 'stdout',
    },
  },
};
 
// 設定のマージとエクスポート
function deepMerge<T extends Record<string, any>>(target: T, source: Partial<T>): T {
  const result = { ...target };
  for (const key in source) {
    if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
      result[key] = deepMerge(
        result[key] as Record<string, any>,
        source[key] as Record<string, any>
      ) as T[Extract<keyof T, string>];
    } else if (source[key] !== undefined) {
      result[key] = source[key] as T[Extract<keyof T, string>];
    }
  }
  return result;
}
 
const currentEnv = process.env.NODE_ENV || 'development';
const override = envOverrides[currentEnv] || {};
 
export const config: AppConfig = deepMerge(defaultConfig, override);
 
// 設定の凍結(実行時の書き換え防止)
Object.freeze(config);
Object.keys(config).forEach(key => {
  if (typeof (config as any)[key] === 'object') {
    Object.freeze((config as any)[key]);
  }
});

3.2 設定のDI(依存性注入)パターン

// ============================================
// 設定をDIコンテナで管理するパターン
// ============================================
 
// config/types.ts
export interface DatabaseConfig {
  host: string;
  port: number;
  database: string;
  username: string;
  password: string;
  ssl: boolean;
  poolSize: number;
}
 
export interface CacheConfig {
  driver: 'memory' | 'redis' | 'memcached';
  ttl: number;
  prefix: string;
  redis?: {
    url: string;
    password?: string;
  };
}
 
export interface EmailConfig {
  provider: 'sendgrid' | 'ses' | 'smtp';
  from: string;
  replyTo?: string;
  apiKey?: string;
  smtp?: {
    host: string;
    port: number;
    secure: boolean;
  };
}
 
// config/container.ts
import { DatabaseConfig, CacheConfig, EmailConfig } from './types';
 
class ConfigContainer {
  private configs = new Map<string, unknown>();
 
  register<T>(key: string, config: T): void {
    this.configs.set(key, Object.freeze(config));
  }
 
  get<T>(key: string): T {
    const config = this.configs.get(key);
    if (!config) {
      throw new Error(`設定 "${key}" が登録されていません`);
    }
    return config as T;
  }
 
  has(key: string): boolean {
    return this.configs.has(key);
  }
}
 
// シングルトンインスタンス
export const configContainer = new ConfigContainer();
 
// 初期化
export function initializeConfig(): void {
  configContainer.register<DatabaseConfig>('database', {
    host: process.env.DB_HOST || 'localhost',
    port: parseInt(process.env.DB_PORT || '5432'),
    database: process.env.DB_NAME || 'myapp',
    username: process.env.DB_USER || 'postgres',
    password: process.env.DB_PASSWORD || '',
    ssl: process.env.DB_SSL === 'true',
    poolSize: parseInt(process.env.DB_POOL_SIZE || '10'),
  });
 
  configContainer.register<CacheConfig>('cache', {
    driver: (process.env.CACHE_DRIVER as CacheConfig['driver']) || 'memory',
    ttl: parseInt(process.env.CACHE_TTL || '300'),
    prefix: process.env.CACHE_PREFIX || 'myapp:',
    redis: process.env.REDIS_URL
      ? { url: process.env.REDIS_URL, password: process.env.REDIS_PASSWORD }
      : undefined,
  });
 
  configContainer.register<EmailConfig>('email', {
    provider: (process.env.EMAIL_PROVIDER as EmailConfig['provider']) || 'sendgrid',
    from: process.env.EMAIL_FROM || 'noreply@example.com',
    replyTo: process.env.EMAIL_REPLY_TO,
    apiKey: process.env.SENDGRID_API_KEY,
  });
}
 
// 使用例
// import { configContainer, initializeConfig } from './config/container';
// initializeConfig();
// const dbConfig = configContainer.get<DatabaseConfig>('database');

4. Feature Flags

4.1 Feature Flags の概要と種類

Feature Flags(機能フラグ)は、コードの変更なしに機能の有効/無効を切り替える仕組みである。デプロイとリリースを分離し、段階的なロールアウトやA/Bテストを実現する。

Feature Flags の種類:
種類説明
Release Flag未完成の機能を隠すためのフラグ
開発完了後に削除する(短命)
例: 新しいダッシュボード UI
Experiment FlagA/Bテストやパーセンテージロールアウト用
実験完了後に削除する(中期)
例: 新しいチェックアウトフロー
Ops Flag運用上のトグル(メンテナンスモードなど)
長期的に維持する
例: 書き込み操作の一時停止
Permission Flagユーザーの権限やプランに基づく機能制御
永続的に維持する
例: プレミアムプラン限定機能
Feature Flags のライフサイクル:
  作成 → テスト → 段階的有効化 → 全体有効化 → フラグ削除

  注意: 不要になったフラグは必ず削除する(技術的負債の温床)

4.2 環境変数ベースの Feature Flags 実装

// ============================================
// lib/feature-flags.ts
// 環境変数ベースのシンプルな Feature Flags
// ============================================
 
// Feature Flag の定義
const FEATURE_FLAGS = {
  // Release Flags
  newDashboard: {
    envVar: 'NEXT_PUBLIC_FF_NEW_DASHBOARD',
    description: '新しいダッシュボード UI',
    defaultValue: false,
  },
  newCheckout: {
    envVar: 'NEXT_PUBLIC_FF_NEW_CHECKOUT',
    description: '新しいチェックアウトフロー',
    defaultValue: false,
  },
 
  // Ops Flags
  maintenanceMode: {
    envVar: 'NEXT_PUBLIC_FF_MAINTENANCE',
    description: 'メンテナンスモード',
    defaultValue: false,
  },
  readOnlyMode: {
    envVar: 'NEXT_PUBLIC_FF_READ_ONLY',
    description: '読み取り専用モード',
    defaultValue: false,
  },
 
  // Experiment Flags
  darkMode: {
    envVar: 'NEXT_PUBLIC_FF_DARK_MODE',
    description: 'ダークモード',
    defaultValue: false,
  },
 
  // Permission Flags
  betaFeatures: {
    envVar: 'NEXT_PUBLIC_FF_BETA',
    description: 'ベータ機能の表示',
    defaultValue: false,
  },
  premiumFeatures: {
    envVar: 'NEXT_PUBLIC_FF_PREMIUM',
    description: 'プレミアム機能',
    defaultValue: false,
  },
} as const;
 
type FeatureFlagName = keyof typeof FEATURE_FLAGS;
 
// Feature Flag の状態を取得
export function isFeatureEnabled(name: FeatureFlagName): boolean {
  const flag = FEATURE_FLAGS[name];
  const envValue = process.env[flag.envVar];
 
  if (envValue === undefined || envValue === '') {
    return flag.defaultValue;
  }
 
  return envValue === 'true' || envValue === '1';
}
 
// 全 Feature Flags の状態を取得
export function getAllFeatureFlags(): Record<FeatureFlagName, boolean> {
  const flags = {} as Record<FeatureFlagName, boolean>;
 
  for (const name of Object.keys(FEATURE_FLAGS) as FeatureFlagName[]) {
    flags[name] = isFeatureEnabled(name);
  }
 
  return flags;
}
 
// Feature Flag の説明を取得(管理画面用)
export function getFeatureFlagDescriptions(): Array<{
  name: string;
  description: string;
  enabled: boolean;
  envVar: string;
}> {
  return (Object.entries(FEATURE_FLAGS) as [FeatureFlagName, typeof FEATURE_FLAGS[FeatureFlagName]][])
    .map(([name, flag]) => ({
      name,
      description: flag.description,
      enabled: isFeatureEnabled(name as FeatureFlagName),
      envVar: flag.envVar,
    }));
}

4.3 React コンポーネントとしての Feature Flag

// ============================================
// components/FeatureFlag.tsx
// React コンポーネントとして Feature Flag を使う
// ============================================
import React, { createContext, useContext, ReactNode } from 'react';
import { getAllFeatureFlags, isFeatureEnabled } from '@/lib/feature-flags';
 
type FeatureFlagName = Parameters<typeof isFeatureEnabled>[0];
 
// ---- Context API による Feature Flags の提供 ----
interface FeatureFlagContextType {
  flags: Record<string, boolean>;
  isEnabled: (name: FeatureFlagName) => boolean;
}
 
const FeatureFlagContext = createContext<FeatureFlagContextType>({
  flags: {},
  isEnabled: () => false,
});
 
export function FeatureFlagProvider({ children }: { children: ReactNode }) {
  const flags = getAllFeatureFlags();
 
  const contextValue: FeatureFlagContextType = {
    flags,
    isEnabled: (name: FeatureFlagName) => flags[name] ?? false,
  };
 
  return (
    <FeatureFlagContext.Provider value={contextValue}>
      {children}
    </FeatureFlagContext.Provider>
  );
}
 
// ---- Hook ----
export function useFeatureFlag(name: FeatureFlagName): boolean {
  const context = useContext(FeatureFlagContext);
  return context.isEnabled(name);
}
 
export function useFeatureFlags() {
  return useContext(FeatureFlagContext);
}
 
// ---- 宣言的コンポーネント ----
interface FeatureFlagProps {
  name: FeatureFlagName;
  children: ReactNode;
  fallback?: ReactNode;
}
 
export function FeatureFlag({ name, children, fallback = null }: FeatureFlagProps) {
  const isEnabled = useFeatureFlag(name);
  return isEnabled ? <>{children}</> : <>{fallback}</>;
}
 
// ---- 使用例 ----
// function App() {
//   return (
//     <FeatureFlagProvider>
//       <Layout>
//         <FeatureFlag
//           name="newDashboard"
//           fallback={<OldDashboard />}
//         >
//           <NewDashboard />
//         </FeatureFlag>
//
//         <FeatureFlag name="darkMode">
//           <DarkModeToggle />
//         </FeatureFlag>
//       </Layout>
//     </FeatureFlagProvider>
//   );
// }
 
// ---- Hook の使用例 ----
// function NavigationMenu() {
//   const showBeta = useFeatureFlag('betaFeatures');
//   const showPremium = useFeatureFlag('premiumFeatures');
//
//   return (
//     <nav>
//       <Link href="/">Home</Link>
//       <Link href="/dashboard">Dashboard</Link>
//       {showBeta && <Link href="/beta">Beta Features</Link>}
//       {showPremium && <Link href="/premium">Premium</Link>}
//     </nav>
//   );
// }

4.4 サービスベースの Feature Flags(段階的ロールアウト)

// ============================================
// lib/feature-flags-service.ts
// 外部サービスを利用した高度な Feature Flags
// ============================================
 
// Feature Flag サービスの抽象インターフェース
interface FeatureFlagService {
  isEnabled(flagName: string, context?: EvaluationContext): Promise<boolean>;
  getVariant(flagName: string, context?: EvaluationContext): Promise<string | null>;
  getAllFlags(context?: EvaluationContext): Promise<Record<string, boolean>>;
}
 
interface EvaluationContext {
  userId?: string;
  email?: string;
  country?: string;
  plan?: 'free' | 'pro' | 'enterprise';
  percentile?: number; // 0-100 のハッシュ値
  attributes?: Record<string, string | number | boolean>;
}
 
// パーセンテージベースのロールアウト実装
class PercentageRollout {
  // ユーザーIDからハッシュ値を計算(0-100)
  static calculatePercentile(userId: string, flagName: string): number {
    const input = `${userId}:${flagName}`;
    let hash = 0;
    for (let i = 0; i < input.length; i++) {
      const char = input.charCodeAt(i);
      hash = ((hash << 5) - hash) + char;
      hash = hash & hash; // 32bit integer に変換
    }
    return Math.abs(hash) % 100;
  }
 
  // パーセンテージでの有効判定
  static isEnabledForUser(
    userId: string,
    flagName: string,
    percentage: number
  ): boolean {
    const userPercentile = this.calculatePercentile(userId, flagName);
    return userPercentile < percentage;
  }
}
 
// ローカル実装(外部サービスなし)
class LocalFeatureFlagService implements FeatureFlagService {
  private flags: Map<string, FlagConfig> = new Map();
 
  constructor(config: Record<string, FlagConfig>) {
    for (const [name, flagConfig] of Object.entries(config)) {
      this.flags.set(name, flagConfig);
    }
  }
 
  async isEnabled(
    flagName: string,
    context?: EvaluationContext
  ): Promise<boolean> {
    const flag = this.flags.get(flagName);
    if (!flag) return false;
 
    // グローバルに無効の場合
    if (!flag.enabled) return false;
 
    // コンテキストなしの場合はグローバル設定を返す
    if (!context) return flag.enabled;
 
    // ユーザーセグメントのチェック
    if (flag.allowedUsers?.includes(context.userId || '')) return true;
    if (flag.allowedEmails?.includes(context.email || '')) return true;
    if (flag.blockedUsers?.includes(context.userId || '')) return false;
 
    // プラン制限のチェック
    if (flag.requiredPlans && context.plan) {
      if (!flag.requiredPlans.includes(context.plan)) return false;
    }
 
    // パーセンテージロールアウト
    if (flag.rolloutPercentage !== undefined && context.userId) {
      return PercentageRollout.isEnabledForUser(
        context.userId,
        flagName,
        flag.rolloutPercentage
      );
    }
 
    return flag.enabled;
  }
 
  async getVariant(
    flagName: string,
    context?: EvaluationContext
  ): Promise<string | null> {
    const flag = this.flags.get(flagName);
    if (!flag?.variants || !context?.userId) return null;
 
    const percentile = PercentageRollout.calculatePercentile(
      context.userId,
      flagName
    );
 
    let cumulative = 0;
    for (const variant of flag.variants) {
      cumulative += variant.weight;
      if (percentile < cumulative) return variant.name;
    }
 
    return null;
  }
 
  async getAllFlags(
    context?: EvaluationContext
  ): Promise<Record<string, boolean>> {
    const result: Record<string, boolean> = {};
    for (const [name] of this.flags) {
      result[name] = await this.isEnabled(name, context);
    }
    return result;
  }
}
 
interface FlagConfig {
  enabled: boolean;
  description?: string;
  rolloutPercentage?: number; // 0-100
  allowedUsers?: string[];
  allowedEmails?: string[];
  blockedUsers?: string[];
  requiredPlans?: Array<'free' | 'pro' | 'enterprise'>;
  variants?: Array<{ name: string; weight: number }>;
}
 
// サービスのインスタンス作成
export const featureFlagService = new LocalFeatureFlagService({
  'new-checkout': {
    enabled: true,
    description: '新しいチェックアウトフロー',
    rolloutPercentage: 25, // 25% のユーザーに有効
  },
  'premium-analytics': {
    enabled: true,
    description: 'プレミアム分析ダッシュボード',
    requiredPlans: ['pro', 'enterprise'],
  },
  'ab-test-pricing': {
    enabled: true,
    description: '料金ページの A/B テスト',
    variants: [
      { name: 'control', weight: 50 },
      { name: 'variant-a', weight: 25 },
      { name: 'variant-b', weight: 25 },
    ],
  },
  'beta-ai-features': {
    enabled: true,
    description: 'AI 機能のベータテスト',
    allowedEmails: ['beta-tester@example.com'],
    rolloutPercentage: 5,
  },
});
 
// 使用例
// const isNewCheckout = await featureFlagService.isEnabled('new-checkout', {
//   userId: currentUser.id,
//   plan: currentUser.plan,
// });

4.5 Feature Flags サービスの比較

主要な Feature Flags サービスの比較:
サービス料金セグメントA/BテストOSS
LaunchDarkly有料高度対応×
Unleash無料+有料中程度対応
Flagsmith無料+有料中程度対応
PostHog無料+有料高度対応
ConfigCat無料+有料中程度限定的×
Split.io有料高度対応×
環境変数ベース無料なしなし-
Vercel Edge無料+有料地理・デバイス対応×
Config
選択の指針:
  - 小規模プロジェクト → 環境変数ベース or Unleash(セルフホスト)
  - 中規模プロジェクト → Flagsmith or PostHog
  - 大規模プロジェクト → LaunchDarkly or Split.io
  - A/Bテスト重視     → PostHog or LaunchDarkly
  - OSS 重視          → Unleash or Flagsmith

5. シークレット管理

5.1 シークレットの分類と管理方針

シークレットの分類:
分類管理方法
API キーStripe Secret Key環境変数 or Vault
データベース認証情報DB パスワード、接続URISecrets Manager
暗号化キーJWT Secret, AES KeyKMS or Vault
OAuth クレデンシャルClient SecretSecrets Manager
SSH キーデプロイキーCI/CD シークレット
TLS 証明書SSL 証明書・秘密鍵Certificate Mgr
Webhook シークレットStripe Webhook Secret環境変数
シークレット管理の原則:
  ① 最小権限の原則: 必要な最小限のアクセス権のみ付与
  ② ローテーション: 定期的にシークレットを更新する
  ③ 暗号化: 保存時も転送時も暗号化する
  ④ 監査: シークレットへのアクセスを記録・監視する
  ⑤ 分離: 環境ごとに異なるシークレットを使用する

5.2 各プラットフォームでのシークレット管理

シークレットの管理方法:

  ① 環境変数(基本):
     → Vercel: Project Settings > Environment Variables
        - Preview / Production / Development で分離可能
        - Sensitive フラグでログ出力を防止
     → AWS: Systems Manager Parameter Store
        - SecureString 型で暗号化保存
        - IAM ポリシーでアクセス制御
     → .env.local(ローカル開発のみ)
        - 絶対に Git にコミットしない

  ② シークレットマネージャー:
     → AWS Secrets Manager
        - 自動ローテーション対応
        - バージョン管理
        - クロスリージョンレプリケーション
     → Google Cloud Secret Manager
        - IAM による細かいアクセス制御
        - 自動レプリケーション
     → HashiCorp Vault
        - 動的シークレット生成
        - リース(有効期限)管理
        - 詳細な監査ログ
     → Azure Key Vault
        - HSM(ハードウェアセキュリティモジュール)対応
        - 証明書管理も統合

  ③ .env ファイルの暗号化:
     → dotenv-vault
        - .env ファイルを暗号化してコミット可能
        - チーム間で安全に共有
     → sops(Mozilla)
        - AWS KMS / GCP KMS / Azure Key Vault と統合
        - 部分暗号化(キーは平文、値のみ暗号化)
     → git-crypt
        - Git リポジトリ内のファイルを透過的に暗号化
        - GPG キーベースのアクセス制御

5.3 AWS Secrets Manager の実装例

// ============================================
// lib/secrets.ts
// AWS Secrets Manager を使ったシークレット取得
// ============================================
import {
  SecretsManagerClient,
  GetSecretValueCommand,
} from '@aws-sdk/client-secrets-manager';
 
const client = new SecretsManagerClient({
  region: process.env.AWS_REGION || 'ap-northeast-1',
});
 
// シークレットのキャッシュ(メモリ内)
const secretCache = new Map<string, { value: string; expiresAt: number }>();
const CACHE_TTL = 5 * 60 * 1000; // 5分
 
export async function getSecret(secretName: string): Promise<string> {
  // キャッシュを確認
  const cached = secretCache.get(secretName);
  if (cached && cached.expiresAt > Date.now()) {
    return cached.value;
  }
 
  try {
    const command = new GetSecretValueCommand({
      SecretId: secretName,
    });
 
    const response = await client.send(command);
    const secretValue = response.SecretString;
 
    if (!secretValue) {
      throw new Error(`Secret "${secretName}" has no value`);
    }
 
    // キャッシュに保存
    secretCache.set(secretName, {
      value: secretValue,
      expiresAt: Date.now() + CACHE_TTL,
    });
 
    return secretValue;
  } catch (error) {
    console.error(`Failed to retrieve secret "${secretName}":`, error);
    throw error;
  }
}
 
// JSON 形式のシークレットをパース
export async function getSecretJson<T>(secretName: string): Promise<T> {
  const secretString = await getSecret(secretName);
  return JSON.parse(secretString) as T;
}
 
// 使用例: データベース接続情報をシークレットから取得
interface DbCredentials {
  host: string;
  port: number;
  database: string;
  username: string;
  password: string;
}
 
export async function getDatabaseCredentials(): Promise<DbCredentials> {
  // 開発環境では環境変数から、本番ではシークレットマネージャーから取得
  if (process.env.NODE_ENV === 'development') {
    return {
      host: process.env.DB_HOST || 'localhost',
      port: parseInt(process.env.DB_PORT || '5432'),
      database: process.env.DB_NAME || 'myapp_dev',
      username: process.env.DB_USER || 'postgres',
      password: process.env.DB_PASSWORD || 'password',
    };
  }
 
  return getSecretJson<DbCredentials>('myapp/production/database');
}

5.4 シークレット漏洩の防止策

やってはいけないこと:
  ✗ シークレットをコードにハードコード
  ✗ .env.local を Git にコミット
  ✗ NEXT_PUBLIC_ にシークレットを入れる
  ✗ ログにシークレットを出力
  ✗ エラーメッセージにシークレットを含める
  ✗ シークレットをURL のクエリパラメータに含める
  ✗ フロントエンドの JavaScript にシークレットを埋め込む
  ✗ コメントやドキュメントに実際のシークレットを記載

.gitignore の設定:
  .env.local
  .env.*.local
  .env.development.local
  .env.production.local
  *.pem
  *.key
  *.p12
  credentials.json
  service-account.json
// ============================================
// pre-commit hook でシークレット漏洩を検出
// .husky/pre-commit
// ============================================
 
// package.json に追加するスクリプト
// {
//   "scripts": {
//     "check-secrets": "ts-node scripts/check-secrets.ts"
//   },
//   "lint-staged": {
//     "**/*": "ts-node scripts/check-secrets.ts"
//   }
// }
 
// scripts/check-secrets.ts
import { execSync } from 'child_process';
 
// 危険なパターンの定義
const SECRET_PATTERNS = [
  // AWS
  { pattern: /AKIA[0-9A-Z]{16}/g, description: 'AWS Access Key ID' },
  { pattern: /(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)\s*=\s*[\w/+=]{40}/g,
    description: 'AWS Secret Access Key' },
 
  // Stripe
  { pattern: /sk_live_[a-zA-Z0-9]{24,}/g, description: 'Stripe Live Secret Key' },
  { pattern: /rk_live_[a-zA-Z0-9]{24,}/g, description: 'Stripe Live Restricted Key' },
 
  // GitHub
  { pattern: /ghp_[a-zA-Z0-9]{36}/g, description: 'GitHub Personal Access Token' },
  { pattern: /ghs_[a-zA-Z0-9]{36}/g, description: 'GitHub App Installation Token' },
 
  // Google
  { pattern: /AIza[0-9A-Za-z\-_]{35}/g, description: 'Google API Key' },
 
  // 汎用
  { pattern: /-----BEGIN (?:RSA |EC )?PRIVATE KEY-----/g, description: 'Private Key' },
  { pattern: /password\s*=\s*['"][^'"]{8,}['"]/gi, description: 'Hardcoded Password' },
];
 
function checkForSecrets(filePaths: string[]): void {
  const violations: Array<{ file: string; line: number; description: string }> = [];
 
  for (const filePath of filePaths) {
    // バイナリファイルはスキップ
    if (/\.(png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$/.test(filePath)) {
      continue;
    }
 
    try {
      const content = require('fs').readFileSync(filePath, 'utf-8');
      const lines = content.split('\n');
 
      for (let lineNum = 0; lineNum < lines.length; lineNum++) {
        const line = lines[lineNum];
 
        for (const { pattern, description } of SECRET_PATTERNS) {
          // パターンをリセット(global フラグのため)
          pattern.lastIndex = 0;
          if (pattern.test(line)) {
            violations.push({
              file: filePath,
              line: lineNum + 1,
              description,
            });
          }
        }
      }
    } catch (error) {
      // ファイル読み込みエラーは無視
    }
  }
 
  if (violations.length > 0) {
    console.error('\nシークレット漏洩の可能性を検出しました:');
    for (const v of violations) {
      console.error(`  ${v.file}:${v.line} - ${v.description}`);
    }
    console.error('\nシークレットを削除してから再度コミットしてください。');
    process.exit(1);
  }
 
  console.log('シークレットチェック: 問題は検出されませんでした。');
}
 
// ステージングされたファイルを取得
const stagedFiles = execSync('git diff --cached --name-only --diff-filter=ACM')
  .toString()
  .trim()
  .split('\n')
  .filter(Boolean);
 
checkForSecrets(stagedFiles);

6. CI/CD パイプラインでの環境変数管理

6.1 GitHub Actions での環境変数設定

# ============================================
# .github/workflows/deploy.yml
# GitHub Actions での環境変数管理
# ============================================
name: Deploy
 
on:
  push:
    branches: [main, staging]
 
# 環境変数の設定方法:
# 1. Repository secrets: Settings > Secrets and variables > Actions
# 2. Environment secrets: Settings > Environments > [env] > Secrets
# 3. Organization secrets: Organization Settings > Secrets
 
jobs:
  deploy:
    runs-on: ubuntu-latest
 
    # 環境を指定(GitHub Environments)
    environment:
      name: ${{ github.ref == 'refs/heads/main' && 'production' || 'staging' }}
      url: ${{ steps.deploy.outputs.url }}
 
    # ジョブレベルの環境変数
    env:
      NODE_ENV: production
      NEXT_TELEMETRY_DISABLED: 1
 
    steps:
      - uses: actions/checkout@v4
 
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
 
      - name: Install dependencies
        run: npm ci
 
      - name: Run environment check
        run: npm run check-env
        env:
          # Repository secrets から注入
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
          JWT_SECRET: ${{ secrets.JWT_SECRET }}
          STRIPE_SECRET_KEY: ${{ secrets.STRIPE_SECRET_KEY }}
          STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }}
          SENDGRID_API_KEY: ${{ secrets.SENDGRID_API_KEY }}
          # Environment secrets から注入(環境ごとに異なる値)
          NEXT_PUBLIC_API_URL: ${{ vars.NEXT_PUBLIC_API_URL }}
          NEXT_PUBLIC_APP_URL: ${{ vars.NEXT_PUBLIC_APP_URL }}
 
      - name: Build
        run: npm run build
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL }}
          NEXT_PUBLIC_API_URL: ${{ vars.NEXT_PUBLIC_API_URL }}
          NEXT_PUBLIC_APP_URL: ${{ vars.NEXT_PUBLIC_APP_URL }}
          NEXT_PUBLIC_GA_ID: ${{ vars.NEXT_PUBLIC_GA_ID }}
          NEXT_PUBLIC_SENTRY_DSN: ${{ vars.NEXT_PUBLIC_SENTRY_DSN }}
          SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
 
      - name: Deploy to Vercel
        id: deploy
        run: |
          npx vercel deploy --prod --token=${{ secrets.VERCEL_TOKEN }}
        env:
          VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
          VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
 
      - name: Run smoke tests
        run: npm run test:smoke
        env:
          TEST_URL: ${{ steps.deploy.outputs.url }}
 
  # 環境変数の設定確認ジョブ
  verify-config:
    runs-on: ubuntu-latest
    needs: deploy
    steps:
      - name: Verify deployment config
        run: |
          RESPONSE=$(curl -s ${{ needs.deploy.outputs.url }}/api/health)
          echo "Health check response: $RESPONSE"
          if echo "$RESPONSE" | jq -e '.status == "ok"' > /dev/null 2>&1; then
            echo "Deployment verification passed"
          else
            echo "Deployment verification failed"
            exit 1
          fi

6.2 Vercel での環境変数設定

// ============================================
// vercel.json - Vercel プロジェクト設定
// ============================================
// {
//   "env": {
//     "CUSTOM_VAR": "value"
//   },
//   "build": {
//     "env": {
//       "BUILD_VAR": "build-value"
//     }
//   }
// }
 
// Vercel CLI での環境変数設定
// vercel env add DATABASE_URL production
// vercel env add DATABASE_URL preview
// vercel env add DATABASE_URL development
 
// 環境変数の一覧取得
// vercel env ls
 
// 環境変数の削除
// vercel env rm DATABASE_URL production
 
// ============================================
// Vercel の環境変数のスコープ
// ============================================
// Production:   本番デプロイ時のみ使用
// Preview:      プレビューデプロイ(PR ブランチ)で使用
// Development:  vercel dev 実行時に使用
//
// Sensitive:    ログやビルド出力に表示されない
// Plain:        通常の環境変数
 
// ============================================
// vercel.json で Preview 環境のブランチ固有設定
// ============================================
// {
//   "git": {
//     "deploymentEnabled": {
//       "feature/*": true,
//       "fix/*": true,
//       "main": true
//     }
//   }
// }

6.3 Docker での環境変数管理

# ============================================
# Dockerfile - マルチステージビルドでの環境変数
# ============================================
 
# ---- ビルドステージ ----
FROM node:20-alpine AS builder
 
WORKDIR /app
 
# ビルド時の環境変数(ARG)
# docker build --build-arg NEXT_PUBLIC_API_URL=https://api.example.com .
ARG NEXT_PUBLIC_API_URL
ARG NEXT_PUBLIC_APP_URL
ARG NEXT_PUBLIC_GA_ID
 
# ARG を ENV に変換(ビルドプロセスで使用)
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_APP_URL=$NEXT_PUBLIC_APP_URL
ENV NEXT_PUBLIC_GA_ID=$NEXT_PUBLIC_GA_ID
 
COPY package*.json ./
RUN npm ci
 
COPY . .
RUN npm run build
 
# ---- 実行ステージ ----
FROM node:20-alpine AS runner
 
WORKDIR /app
 
# 実行時の環境変数はここでは設定しない
# docker run -e DATABASE_URL=... で注入する
ENV NODE_ENV=production
 
# セキュリティ: 非 root ユーザーで実行
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
 
COPY --from=builder /app/public ./public
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
 
USER nextjs
 
EXPOSE 3000
 
CMD ["node", "server.js"]
# ============================================
# docker-compose.yml - 開発環境の構成
# ============================================
version: '3.8'
 
services:
  app:
    build:
      context: .
      dockerfile: Dockerfile.dev
      args:
        - NEXT_PUBLIC_API_URL=http://localhost:3001
    ports:
      - "3000:3000"
    environment:
      - NODE_ENV=development
      - DATABASE_URL=postgresql://postgres:password@db:5432/myapp_dev
      - REDIS_URL=redis://redis:6379
    env_file:
      - .env                    # 共通設定
      - .env.development        # 開発環境設定
      - .env.local              # ローカルオーバーライド(.gitignore)
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    volumes:
      - .:/app
      - /app/node_modules
 
  db:
    image: postgres:16-alpine
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
      POSTGRES_DB: myapp_dev
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 5s
      retries: 5
 
  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data
 
volumes:
  postgres_data:
  redis_data:

7. Kubernetes での設定管理

7.1 ConfigMap と Secret

Kubernetes では、設定データを ConfigMap と Secret という2つのリソースで管理する。ConfigMap は非機密データ、Secret は機密データを格納するために使う。

# ============================================
# k8s/configmap.yaml - 非機密設定
# ============================================
apiVersion: v1
kind: ConfigMap
metadata:
  name: myapp-config
  namespace: production
  labels:
    app: myapp
    environment: production
data:
  # 個別のキーバリュー
  NODE_ENV: "production"
  LOG_LEVEL: "warn"
  LOG_FORMAT: "json"
  CACHE_TTL: "600"
  ENABLE_ANALYTICS: "true"
  ENABLE_RATE_LIMIT: "true"
 
  # 設定ファイルとしてマウントすることも可能
  app-config.json: |
    {
      "api": {
        "baseUrl": "https://api.example.com",
        "timeout": 10000,
        "retryCount": 5
      },
      "cache": {
        "ttl": 600,
        "maxItems": 10000,
        "strategy": "redis"
      },
      "security": {
        "corsOrigins": [
          "https://www.example.com",
          "https://admin.example.com"
        ],
        "rateLimitMax": 50
      }
    }
# ============================================
# k8s/secret.yaml - 機密設定
# ============================================
apiVersion: v1
kind: Secret
metadata:
  name: myapp-secrets
  namespace: production
  labels:
    app: myapp
    environment: production
type: Opaque
# 値は Base64 エンコードが必要
# echo -n 'value' | base64
data:
  DATABASE_URL: cG9zdGdyZXNxbDovL3VzZXI6cGFzc0Bob3N0OjU0MzIvbXlkYg==
  JWT_SECRET: c3VwZXItc2VjcmV0LWtleS10aGF0LWlzLWF0LWxlYXN0LTMyLWNoYXJz
  STRIPE_SECRET_KEY: c2tfdGVzdF94eHh4eHh4eHh4eHg=
  REDIS_PASSWORD: cmVkaXMtcGFzc3dvcmQ=
 
# stringData を使えば Base64 エンコード不要(推奨)
# stringData:
#   DATABASE_URL: "postgresql://user:pass@host:5432/mydb"
#   JWT_SECRET: "super-secret-key-that-is-at-least-32-chars"
# ============================================
# k8s/deployment.yaml - Pod での環境変数注入
# ============================================
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: myapp:latest
          ports:
            - containerPort: 3000
 
          # 方法1: ConfigMap から個別の環境変数を注入
          env:
            - name: NODE_ENV
              valueFrom:
                configMapKeyRef:
                  name: myapp-config
                  key: NODE_ENV
            - name: DATABASE_URL
              valueFrom:
                secretKeyRef:
                  name: myapp-secrets
                  key: DATABASE_URL
            - name: JWT_SECRET
              valueFrom:
                secretKeyRef:
                  name: myapp-secrets
                  key: JWT_SECRET
 
          # 方法2: ConfigMap / Secret の全キーを一括注入
          envFrom:
            - configMapRef:
                name: myapp-config
            - secretRef:
                name: myapp-secrets
 
          # 方法3: 設定ファイルとしてマウント
          volumeMounts:
            - name: config-volume
              mountPath: /app/config
              readOnly: true
 
      volumes:
        - name: config-volume
          configMap:
            name: myapp-config
            items:
              - key: app-config.json
                path: app-config.json

7.2 External Secrets Operator

# ============================================
# External Secrets Operator で AWS Secrets Manager と連携
# ============================================
 
# SecretStore: AWS Secrets Manager への接続設定
apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: aws-secret-store
  namespace: production
spec:
  provider:
    aws:
      service: SecretsManager
      region: ap-northeast-1
      auth:
        jwt:
          serviceAccountRef:
            name: myapp-sa
 
---
# ExternalSecret: AWS Secrets Manager から Kubernetes Secret を自動生成
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: myapp-external-secrets
  namespace: production
spec:
  refreshInterval: 1h        # 1時間ごとに同期
  secretStoreRef:
    name: aws-secret-store
    kind: SecretStore
  target:
    name: myapp-secrets       # 生成される Secret の名前
    creationPolicy: Owner
  data:
    - secretKey: DATABASE_URL
      remoteRef:
        key: myapp/production/database
        property: url
    - secretKey: JWT_SECRET
      remoteRef:
        key: myapp/production/auth
        property: jwt_secret
    - secretKey: STRIPE_SECRET_KEY
      remoteRef:
        key: myapp/production/stripe
        property: secret_key

7.3 Sealed Secrets(暗号化された Secret を Git 管理)

Sealed Secrets の仕組み:

  開発者のマシン                    Kubernetes クラスタ
kubeseal CLISealed Secrets
Controller
Secret暗号化
(平文) ─────┼──────────>SealedSecret
Secret
(平文 - Pod に注入)
メリット:
  - 暗号化された SealedSecret を Git にコミットできる
  - GitOps ワークフローと相性が良い
  - クラスタの公開鍵でのみ復号可能
# Sealed Secrets の使い方
 
# 1. 通常の Secret を作成(ファイルとして)
kubectl create secret generic myapp-secrets \
  --from-literal=DATABASE_URL='postgresql://user:pass@host:5432/mydb' \
  --from-literal=JWT_SECRET='super-secret-key-that-is-at-least-32-chars' \
  --dry-run=client -o yaml > secret.yaml
 
# 2. kubeseal で暗号化
kubeseal --format yaml < secret.yaml > sealed-secret.yaml
 
# 3. 暗号化された SealedSecret を Git にコミット(安全)
git add sealed-secret.yaml
git commit -m "Add sealed secrets for production"
 
# 4. クラスタにデプロイ(Controller が自動で復号して Secret を作成)
kubectl apply -f sealed-secret.yaml

8. 設定管理のアンチパターンと対策

8.1 よくあるアンチパターン

アンチパターン一覧と対策:
アンチパターン対策
シークレットのハードコード環境変数 or Secrets Manager
if (env === 'prod') 分岐環境別設定ファイルで分離
環境変数の型変換忘れZod でスキーマバリデーション
デフォルト値に本番値を使用安全なデフォルト値を設定
.env をコミット.gitignore + pre-commit hook
NEXT_PUBLIC_ に機密情報プレフィックスの意味を理解する
設定のグローバル変数管理DI パターンまたはモジュール化
環境変数の過剰な使用設定ファイルとの適切な使い分け
テスト環境で本番シークレットを使用テスト用のモック/ダミー値を使用
設定の変更ログを残さない設定変更の監査ログを実装
Feature Flag の放置定期的なクリーンアップを実施
全環境で同一のシークレットを使用環境ごとにシークレットをローテート

8.2 条件分岐によるアンチパターンの詳細

// ============================================
// アンチパターン: コード内での環境分岐
// ============================================
 
// BAD: コードの中で環境を判定して分岐する
function getApiUrlBad(): string {
  if (process.env.NODE_ENV === 'production') {
    return 'https://api.example.com';
  } else if (process.env.NODE_ENV === 'staging') {
    return 'https://staging-api.example.com';
  } else {
    return 'http://localhost:3001';
  }
}
 
// BAD: 環境ごとに異なるサービスを直接切り替え
function createEmailServiceBad() {
  if (process.env.NODE_ENV === 'production') {
    return new SendGridEmailService(process.env.SENDGRID_API_KEY!);
  } else {
    return new ConsoleEmailService(); // コンソールに出力するだけ
  }
}
 
// GOOD: 環境変数から設定を読み取る
function getApiUrlGood(): string {
  const url = process.env.NEXT_PUBLIC_API_URL;
  if (!url) {
    throw new Error('NEXT_PUBLIC_API_URL is not defined');
  }
  return url;
}
 
// GOOD: DIパターンで環境に依存しない設計
interface EmailService {
  send(to: string, subject: string, body: string): Promise<void>;
}
 
function createEmailServiceGood(config: EmailConfig): EmailService {
  switch (config.provider) {
    case 'sendgrid':
      return new SendGridEmailService(config.apiKey!);
    case 'ses':
      return new SESEmailService(config);
    case 'smtp':
      return new SMTPEmailService(config.smtp!);
    default:
      throw new Error(`Unknown email provider: ${config.provider}`);
  }
}

8.3 安全なデフォルト値の設計

// ============================================
// デフォルト値の設計原則
// ============================================
 
// BAD: 危険なデフォルト値
const configBad = {
  debugMode: true,              // 本番で有効になる危険
  corsOrigin: '*',              // 全オリジン許可の危険
  rateLimit: false,             // レート制限なしの危険
  logLevel: 'debug',            // 本番で詳細ログが出る
  sessionSecret: 'default',     // 既知のシークレット
  ssl: false,                   // SSL なしの危険
};
 
// GOOD: 安全なデフォルト値(Secure by Default)
const configGood = {
  debugMode: false,             // デフォルトで無効
  corsOrigin: undefined,        // 明示的な設定が必要
  rateLimit: true,              // デフォルトで有効
  logLevel: 'warn',             // 本番向けのレベル
  sessionSecret: undefined,     // 必須(起動時エラー)
  ssl: true,                    // デフォルトで有効
};
 
// GOOD: 環境に応じたデフォルト値
function getDefaultConfig(env: string) {
  const isProduction = env === 'production';
 
  return {
    debugMode: !isProduction,
    logLevel: isProduction ? 'warn' : 'debug',
    ssl: isProduction, // 本番では必ず SSL
    cache: {
      ttl: isProduction ? 600 : 0,
      strategy: isProduction ? 'redis' : 'memory',
    },
  };
}

9. トラブルシューティング

9.1 よくある問題と解決策

問題1: 環境変数が undefined になる

  症状: process.env.MY_VAR が undefined
  原因と対策:

  チェックリスト:
  □ .env ファイルに変数が定義されているか
  □ .env ファイルの書式が正しいか(= の前後にスペースがないか)
  □ .env ファイルの文字コードが UTF-8 か
  □ 変数名にタイポがないか(大文字小文字の区別あり)
  □ クライアントサイドでアクセスする場合 NEXT_PUBLIC_ プレフィックスがあるか
  □ dotenv パッケージが正しく読み込まれているか
  □ Docker 環境の場合 env_file が正しく指定されているか

  デバッグコマンド:
  $ node -e "console.log(process.env.MY_VAR)"
  $ printenv | grep MY_VAR
  $ docker exec container_name printenv | grep MY_VAR


問題2: ビルド時と実行時で環境変数が異なる

  症状: ビルドしたアプリケーションが意図しない API を呼ぶ
  原因: NEXT_PUBLIC_ はビルド時にインライン化されるため、
        実行時に環境変数を変更しても反映されない

  対策:
  (1) ビルド時に正しい環境変数を設定する
     NEXT_PUBLIC_API_URL=https://api.example.com npm run build
  (2) Runtime Configuration を使う(Next.js)
     → serverRuntimeConfig / publicRuntimeConfig
  (3) __NEXT_DATA__ を使ったランタイム環境変数注入
     → getServerSideProps で環境変数を props として渡す


問題3: Docker コンテナ内で .env が読まれない

  症状: ローカルでは動くが Docker コンテナ内で環境変数が空
  原因: .env ファイルが .dockerignore に含まれている

  対策:
  (1) docker-compose の env_file を使う
  (2) docker run -e で個別に渡す
  (3) docker run --env-file .env で一括渡し
  (4) .env を .dockerignore に入れたまま、
     外部から注入する(推奨)


問題4: CI/CD で環境変数が設定されていない

  症状: CI パイプラインでビルドが失敗する
  原因: Repository secrets / Environment secrets が未設定

  対策:
  (1) GitHub Actions: Settings > Secrets に変数を追加
  (2) secrets コンテキストが正しく参照されているか確認
     ${{ secrets.MY_SECRET }}($ を忘れがち)
  (3) Environment protection rules が設定されていないか確認
  (4) Organization secrets の場合はリポジトリへのアクセスを許可


問題5: 環境変数の値に特殊文字が含まれる

  症状: パスワードに ! や $ が含まれると正しく読み取れない
  原因: シェルの文字列展開が干渉する

  対策:
  (1) .env ファイルではシングルクォートで囲む
     DATABASE_PASSWORD='p@ss!w0rd$123'
  (2) Docker では環境変数をシングルクォートで渡す
     docker run -e "DATABASE_PASSWORD='p@ss!w0rd\$123'"
  (3) Base64 エンコードして格納し、アプリ側でデコードする


問題6: Feature Flag が反映されない

  症状: 環境変数を変更したのに Feature Flag の状態が変わらない
  原因:
  (1) NEXT_PUBLIC_ の場合はビルドの再実行が必要
  (2) サーバーサイドの場合はサーバーの再起動が必要
  (3) CDN やブラウザのキャッシュが残っている

  対策:
  (1) サービスベースの Feature Flags を使う(再デプロイ不要)
  (2) サーバーサイドのみの Feature Flags はランタイムで評価
  (3) キャッシュのパージを実行

9.2 環境変数のデバッグツール

// ============================================
// lib/debug-env.ts
// 環境変数のデバッグヘルパー(開発環境のみ使用)
// ============================================
 
export function debugEnv(): void {
  if (process.env.NODE_ENV === 'production') {
    console.warn('debugEnv() は本番環境では使用しないでください');
    return;
  }
 
  console.log('\n========== 環境変数デバッグ情報 ==========');
  console.log(`NODE_ENV: ${process.env.NODE_ENV}`);
  console.log(`実行環境: ${typeof window === 'undefined' ? 'サーバー' : 'クライアント'}`);
 
  // 設定されている NEXT_PUBLIC_ 変数の一覧
  const publicVars = Object.entries(process.env)
    .filter(([key]) => key.startsWith('NEXT_PUBLIC_'))
    .map(([key, value]) => `  ${key}: ${value}`);
 
  console.log(`\nNEXT_PUBLIC_ 変数 (${publicVars.length}個):`);
  publicVars.forEach(v => console.log(v));
 
  // サーバーサイドの場合は機密変数の存在チェック(値は表示しない)
  if (typeof window === 'undefined') {
    const serverVars = [
      'DATABASE_URL',
      'JWT_SECRET',
      'STRIPE_SECRET_KEY',
      'STRIPE_WEBHOOK_SECRET',
      'SENDGRID_API_KEY',
      'REDIS_URL',
      'SENTRY_DSN',
    ];
 
    console.log('\nサーバー変数の設定状況:');
    serverVars.forEach(key => {
      const value = process.env[key];
      const status = value
        ? `設定済み (${value.length}文字)`
        : '未設定';
      console.log(`  ${key}: ${status}`);
    });
  }
 
  console.log('==========================================\n');
}
 
// 使用方法(開発環境のみ):
// import { debugEnv } from '@/lib/debug-env';
// if (process.env.NODE_ENV === 'development') debugEnv();
// ============================================
// app/api/debug/env/route.ts
// 環境変数のデバッグ API(開発環境のみ)
// ============================================
import { NextResponse } from 'next/server';
 
export async function GET() {
  // 本番環境では絶対にアクセスさせない
  if (process.env.NODE_ENV === 'production') {
    return NextResponse.json(
      { error: 'Not available in production' },
      { status: 403 }
    );
  }
 
  // デバッグ用の情報を返す(値は隠す)
  const envInfo = {
    nodeEnv: process.env.NODE_ENV,
    platform: process.platform,
    nodeVersion: process.version,
    variables: Object.keys(process.env)
      .sort()
      .reduce((acc, key) => {
        // 値は長さのみ表示(セキュリティ)
        const value = process.env[key] || '';
        acc[key] = {
          set: value.length > 0,
          length: value.length,
          // NEXT_PUBLIC_ は値も表示(クライアントに公開済みのため)
          value: key.startsWith('NEXT_PUBLIC_') ? value : '[hidden]',
        };
        return acc;
      }, {} as Record<string, any>),
  };
 
  return NextResponse.json(envInfo);
}

9.3 ヘルスチェックエンドポイントでの設定確認

// ============================================
// app/api/health/route.ts
// ヘルスチェック API(設定の検証含む)
// ============================================
import { NextResponse } from 'next/server';
 
interface HealthStatus {
  status: 'ok' | 'degraded' | 'error';
  timestamp: string;
  version: string;
  environment: string;
  checks: {
    name: string;
    status: 'pass' | 'fail' | 'warn';
    message?: string;
    duration?: number;
  }[];
}
 
async function checkDatabase(): Promise<{
  status: 'pass' | 'fail';
  message: string;
  duration: number;
}> {
  const start = Date.now();
  try {
    // Prisma の場合
    // await prisma.$queryRaw`SELECT 1`;
    // Drizzle の場合
    // await db.execute(sql`SELECT 1`);
    return {
      status: 'pass',
      message: 'Database connection OK',
      duration: Date.now() - start,
    };
  } catch (error) {
    return {
      status: 'fail',
      message: `Database connection failed: ${(error as Error).message}`,
      duration: Date.now() - start,
    };
  }
}
 
async function checkRedis(): Promise<{
  status: 'pass' | 'fail' | 'warn';
  message: string;
  duration: number;
}> {
  if (!process.env.REDIS_URL) {
    return { status: 'warn', message: 'Redis not configured', duration: 0 };
  }
 
  const start = Date.now();
  try {
    // Redis クライアントの PING
    // await redis.ping();
    return {
      status: 'pass',
      message: 'Redis connection OK',
      duration: Date.now() - start,
    };
  } catch (error) {
    return {
      status: 'fail',
      message: `Redis connection failed: ${(error as Error).message}`,
      duration: Date.now() - start,
    };
  }
}
 
function checkRequiredEnvVars(): {
  status: 'pass' | 'fail';
  message: string;
} {
  const required = ['DATABASE_URL', 'JWT_SECRET'];
  const missing = required.filter(key => !process.env[key]);
 
  if (missing.length > 0) {
    return {
      status: 'fail',
      message: `Missing required env vars: ${missing.join(', ')}`,
    };
  }
 
  return { status: 'pass', message: 'All required env vars are set' };
}
 
export async function GET() {
  const [dbCheck, redisCheck] = await Promise.all([
    checkDatabase(),
    checkRedis(),
  ]);
  const envCheck = checkRequiredEnvVars();
 
  const checks = [
    { name: 'database', ...dbCheck },
    { name: 'redis', ...redisCheck },
    { name: 'env_vars', ...envCheck },
  ];
 
  const hasError = checks.some(c => c.status === 'fail');
  const hasWarn = checks.some(c => c.status === 'warn');
 
  const health: HealthStatus = {
    status: hasError ? 'error' : hasWarn ? 'degraded' : 'ok',
    timestamp: new Date().toISOString(),
    version: process.env.NEXT_PUBLIC_APP_VERSION || '0.0.0',
    environment: process.env.NODE_ENV || 'unknown',
    checks,
  };
 
  return NextResponse.json(health, {
    status: hasError ? 503 : 200,
    headers: {
      'Cache-Control': 'no-cache, no-store, must-revalidate',
    },
  });
}

10. 設定管理のベストプラクティスチェックリスト

10.1 プロジェクト初期設定

プロジェクト立ち上げ時のチェックリスト:

  □ .env.example を作成し、全環境変数を記載する
  □ .gitignore に .env.local, .env.*.local を追加する
  □ Zod による環境変数バリデーションを実装する
  □ pre-commit hook でシークレット漏洩チェックを設定する
  □ README.md に環境変数のセットアップ手順を記載する
  □ 環境変数の命名規則をチーム内で合意する
  □ Feature Flags の管理方針を決める
  □ シークレットの管理方法(Secrets Manager / Vault)を決める
  □ CI/CD パイプラインでの環境変数の設定方法を確認する
  □ ヘルスチェックエンドポイントを実装する

10.2 コードレビュー時のチェックリスト

環境設定に関するコードレビュー観点:

  □ 新しい環境変数が追加された場合:
    ├── .env.example が更新されているか
    ├── Zod スキーマが更新されているか
    ├── 適切なデフォルト値が設定されているか
    ├── NEXT_PUBLIC_ の要否が正しいか
    └── CI/CD のシークレット設定手順が明記されているか

  □ セキュリティ:
    ├── 機密情報がクライアントに露出していないか
    ├── ハードコードされたシークレットがないか
    ├── ログ出力にシークレットが含まれていないか
    └── エラーメッセージにシークレットが含まれていないか

  □ 設定の分離:
    ├── 環境固有の値がコードにハードコードされていないか
    ├── if (env === 'production') のような分岐がないか
    └── 設定の変更にコード変更が不要か

  □ Feature Flags:
    ├── 不要になったフラグの削除 PR が計画されているか
    ├── デフォルト値が安全か(OFF がデフォルト)
    └── フラグの説明がコードやドキュメントにあるか

10.3 環境設定に関するセキュリティポリシー

セキュリティポリシーテンプレート:

  1. シークレットの分類と管理
     - Level 1(最高機密): データベースパスワード、暗号化キー
       → AWS Secrets Manager / HashiCorp Vault で管理
       → 90日ごとにローテーション
     - Level 2(機密): API キー、Webhook シークレット
       → 環境変数として CI/CD シークレットに格納
       → 180日ごとにローテーション
     - Level 3(内部利用): 内部サービスの URL、ポート番号
       → ConfigMap / 環境変数で管理
       → ローテーション不要

  2. アクセス制御
     - 本番環境のシークレットへのアクセスは、
       SRE チームと Tech Lead のみに限定
     - シークレットの変更は必ず2人以上の承認が必要
     - アクセスログを定期的に監査する

  3. インシデント対応
     - シークレット漏洩が疑われる場合:
       (1) 直ちに該当シークレットをローテーション
       (2) 影響範囲の調査(アクセスログの確認)
       (3) インシデントレポートの作成
       (4) 再発防止策の策定と実施

  4. 定期レビュー
     - 四半期ごとに環境変数の棚卸しを実施
     - 不要な環境変数の削除
     - シークレットのローテーション状況の確認
     - Feature Flags のクリーンアップ

10.4 シークレットローテーションの実装

// ============================================
// lib/secret-rotation.ts
// シークレットのローテーション支援ツール
// ============================================
 
interface RotationPolicy {
  secretName: string;
  maxAgeDays: number;
  lastRotated: Date;
  owners: string[];
}
 
const rotationPolicies: RotationPolicy[] = [
  {
    secretName: 'DATABASE_PASSWORD',
    maxAgeDays: 90,
    lastRotated: new Date('2025-12-01'),
    owners: ['sre-team@example.com'],
  },
  {
    secretName: 'JWT_SECRET',
    maxAgeDays: 90,
    lastRotated: new Date('2025-11-15'),
    owners: ['sre-team@example.com'],
  },
  {
    secretName: 'STRIPE_SECRET_KEY',
    maxAgeDays: 180,
    lastRotated: new Date('2025-10-01'),
    owners: ['payment-team@example.com'],
  },
  {
    secretName: 'SENDGRID_API_KEY',
    maxAgeDays: 180,
    lastRotated: new Date('2025-09-01'),
    owners: ['platform-team@example.com'],
  },
];
 
interface RotationCheckResult {
  secretName: string;
  daysSinceRotation: number;
  maxAgeDays: number;
  status: 'ok' | 'warning' | 'expired';
  owners: string[];
  message: string;
}
 
export function checkRotationStatus(): RotationCheckResult[] {
  const now = new Date();
 
  return rotationPolicies.map(policy => {
    const daysSince = Math.floor(
      (now.getTime() - policy.lastRotated.getTime()) / (1000 * 60 * 60 * 24)
    );
 
    const warningThreshold = policy.maxAgeDays * 0.8; // 80% で警告
 
    let status: 'ok' | 'warning' | 'expired';
    let message: string;
 
    if (daysSince > policy.maxAgeDays) {
      status = 'expired';
      message = `${daysSince - policy.maxAgeDays}日超過しています。直ちにローテーションしてください。`;
    } else if (daysSince > warningThreshold) {
      status = 'warning';
      message = `残り${policy.maxAgeDays - daysSince}日でローテーション期限です。`;
    } else {
      status = 'ok';
      message = `次のローテーションまで${policy.maxAgeDays - daysSince}日です。`;
    }
 
    return {
      secretName: policy.secretName,
      daysSinceRotation: daysSince,
      maxAgeDays: policy.maxAgeDays,
      status,
      owners: policy.owners,
      message,
    };
  });
}
 
// ローテーション状況のレポート出力
export function printRotationReport(): void {
  const results = checkRotationStatus();
 
  console.log('\n===== シークレットローテーション状況 =====\n');
 
  for (const result of results) {
    const icon =
      result.status === 'ok' ? '[OK]' :
      result.status === 'warning' ? '[WARN]' : '[EXPIRED]';
 
    console.log(`${icon} ${result.secretName}`);
    console.log(`  最終ローテーション: ${result.daysSinceRotation}日前`);
    console.log(`  有効期限: ${result.maxAgeDays}日`);
    console.log(`  状態: ${result.message}`);
    console.log(`  担当: ${result.owners.join(', ')}`);
    console.log('');
  }
 
  const expired = results.filter(r => r.status === 'expired');
  if (expired.length > 0) {
    console.log(`\n[ALERT] ${expired.length}件のシークレットが期限切れです!`);
  }
}

まとめ

概念 ポイント 推奨ツール
環境変数設計 SCREAMING_SNAKE_CASE、プレフィックスで分類 -
型安全性 Zod で起動時バリデーション、フェイルファスト Zod, @t3-oss/env-nextjs
環境分離 設定ファイルでオーバーライド、コードに環境分岐を書かない dotenv, direnv
Feature Flags 段階的ロールアウト、A/B テスト、不要フラグの削除 LaunchDarkly, Unleash
シークレット管理 .env.local は Git 管理外、Secrets Manager で暗号化保存 AWS Secrets Manager, Vault
CI/CD GitHub Environments でスコープ分離、Secrets で機密管理 GitHub Actions, Vercel
Kubernetes ConfigMap と Secret の分離、External Secrets Operator Sealed Secrets, ESO
セキュリティ pre-commit hook、Secure by Default、定期ローテーション husky, gitleaks
デバッグ ヘルスチェック API、環境変数チェックスクリプト -
設定管理の成熟度モデル:

  Level 1(基本):
    ├── .env ファイルで管理
    ├── .gitignore で機密ファイルを除外
    └── .env.example の作成

  Level 2(標準):
    ├── 環境変数のバリデーション(Zod)
    ├── 環境別設定ファイル
    ├── pre-commit hook でのシークレットチェック
    └── CI/CD でのシークレット管理

  Level 3(高度):
    ├── Secrets Manager / Vault の導入
    ├── Feature Flags サービスの導入
    ├── 自動ローテーション
    ├── 監査ログの実装
    └── Sealed Secrets / External Secrets Operator

  Level 4(エンタープライズ):
    ├── ゼロトラストの設定管理
    ├── HSM(ハードウェアセキュリティモジュール)
    ├── コンプライアンス対応(SOC2, ISO27001)
    ├── 自動化されたポリシーチェック
    └── 設定の変更管理プロセス(ITIL ベース)

よくある質問(FAQ)

Q1: 環境変数の安全な管理方法は?

基本原則:

  1. 絶対にコミットしない: .env.env.local.env.production などの実際の値を持つファイルは必ず .gitignore に追加
  2. テンプレートは共有する: .env.example には変数名とダミー値を記載してチームで共有
  3. 階層化して管理: 開発環境とは別に本番環境用のシークレット管理システムを導入

推奨ツール:

# ローカル開発
.env.local         # gitignore対象、ローカル固有の値
.env.example       # Git管理、変数名とダミー値のみ
 
# 本番環境
AWS Secrets Manager    # AWS環境
Vercel Environment Variables  # Vercel
GitHub Secrets        # GitHub Actions
HashiCorp Vault       # オンプレミス/マルチクラウド

実装例:

// env.ts - 起動時バリデーション
import { z } from 'zod';
 
const envSchema = z.object({
  DATABASE_URL: z.string().url(),
  API_KEY: z.string().min(32),
  NODE_ENV: z.enum(['development', 'production', 'test']),
});
 
// 起動時にバリデーション、失敗したら即座にエラー
export const env = envSchema.parse(process.env);

Q2: .envファイルの管理とgitignoreのベストプラクティスは?

ファイル構成の推奨パターン:

プロジェクトルート/
├── .env.example          # ✓ Git管理する(変数名のみ)
├── .env                  # ✗ gitignore(共通のデフォルト値)
├── .env.local            # ✗ gitignore(ローカル固有の値)
├── .env.development      # ✗ gitignore(開発環境用)
├── .env.production       # ✗ gitignore(本番環境用、通常は使わない)
└── .gitignore

.gitignore の設定:

# Environment variables
.env
.env.local
.env.*.local
.env.development
.env.production
.env.test
 
# ただし .env.example は除外しない(!で例外指定)
!.env.example

.env.example の書き方:

# .env.example - チームで共有するテンプレート
# データベース
DATABASE_URL=postgresql://user:password@localhost:5432/dbname
 
# 外部API
STRIPE_SECRET_KEY=sk_test_xxxxx
STRIPE_PUBLIC_KEY=pk_test_xxxxx
 
# サービス設定
NEXT_PUBLIC_API_URL=http://localhost:3000/api
NODE_ENV=development
 
# 認証
NEXTAUTH_SECRET=<generate-with-openssl-rand-base64-32>
NEXTAUTH_URL=http://localhost:3000

セキュリティチェック:

# pre-commit hookで機密情報の混入を防ぐ
npm install --save-dev husky @commitlint/cli
npx husky add .husky/pre-commit "npx gitleaks protect --staged"
 
# gitleaksで過去のコミット履歴もスキャン
docker run -v $(pwd):/path zricethezav/gitleaks:latest detect --source="/path" -v

Q3: ビルド時変数と実行時変数の違いは?

ビルド時変数(Build-time Variables):

// Next.js の例
// next.config.js
module.exports = {
  env: {
    BUILD_TIME: new Date().toISOString(),
    COMMIT_SHA: process.env.VERCEL_GIT_COMMIT_SHA,
  },
};
 
// コンポーネントで使用
export default function Footer() {
  return <p>Built at: {process.env.BUILD_TIME}</p>;
  // ビルド時に値が埋め込まれ、クライアント側でも利用可能
}

特徴:

  • ビルド時に値が確定し、バンドルに埋め込まれる
  • 変更するには再ビルドが必要
  • クライアント側でも利用可能(公開される)
  • NEXT_PUBLIC_* プレフィックスが付く変数

実行時変数(Runtime Variables):

// サーバーサイドでのみ利用(Server Components / API Routes)
export async function GET() {
  const dbUrl = process.env.DATABASE_URL; // 実行時に読み込まれる
  const client = new PrismaClient({ datasources: { db: { url: dbUrl } } });
  // ...
}

特徴:

  • サーバー起動時/リクエスト時に読み込まれる
  • 環境変数を変更したらアプリ再起動が必要(ビルドは不要)
  • サーバーサイドのみで利用可能(クライアントに公開されない)
  • シークレット情報はこちらで管理

使い分け:

用途 種類
APIのベースURL ビルド時 NEXT_PUBLIC_API_URL
Google Analytics ID ビルド時 NEXT_PUBLIC_GA_ID
データベース接続文字列 実行時 DATABASE_URL
APIシークレットキー 実行時 STRIPE_SECRET_KEY
ビルドバージョン情報 ビルド時 BUILD_ID, COMMIT_SHA

Vercel環境での挙動:

# ビルド時変数: Vercel UI で「Exposed to Client」をチェック
NEXT_PUBLIC_API_URL=https://api.example.com
 
# 実行時変数: デフォルト(チェックなし)
DATABASE_URL=postgresql://...

次に読むべきガイド


参考文献

  1. The Twelve-Factor App. "III. Config - Store config in the environment." 12factor.net, 2017.
  2. Next.js. "Environment Variables." nextjs.org/docs, 2024.
  3. Vercel. "Environment Variables." vercel.com/docs, 2024.
  4. Vite. "Env Variables and Modes." vitejs.dev/guide, 2024.
  5. AWS. "AWS Secrets Manager User Guide." docs.aws.amazon.com, 2024.
  6. HashiCorp. "Vault Documentation." developer.hashicorp.com/vault, 2024.
  7. Bitnami. "Sealed Secrets." github.com/bitnami-labs/sealed-secrets, 2024.
  8. External Secrets Operator. "Getting Started." external-secrets.io, 2024.
  9. LaunchDarkly. "Feature Flags Best Practices." launchdarkly.com/blog, 2024.
  10. Martin Fowler. "Feature Toggles (aka Feature Flags)." martinfowler.com, 2023.