Tasuke Hubのロゴ

ITを中心に困っている人を助けるメディア

分かりやすく解決策を提供することで、あなたの困ったをサポート。 全ての人々がスムーズに生活できる世界を目指します。

Dockerコンテナ内Node.jsアプリの環境変数トラブル解決法

記事のサムネイル

Dockerコンテナ内で環境変数が読み込まれない原因

Dockerコンテナ内でNode.jsアプリケーションを実行する際、環境変数が正しく読み込まれないという問題は意外と多くの開発者が遭遇するトラブルです。環境変数は設定情報やAPIキーなどの機密情報を管理するために重要な仕組みですが、Docker環境では特有の問題が発生します。

主な原因として以下のようなケースが考えられます:

  1. 環境変数の設定場所の問題:

    • Dockerfileで設定した環境変数
    • docker-compose.ymlで設定した環境変数
    • ホストマシンの環境変数 これらが優先順位によって上書きされ、期待した値が適用されないことがあります。
  2. 環境変数の読み込みタイミング: Node.jsアプリケーションは起動時に環境変数を読み込みますが、コンテナ実行後に動的に変更された環境変数は自動的に反映されません。

  3. .envファイルの扱い方の誤解: 多くの開発者が.envファイルをDocker環境で正しく扱えていないことが問題を引き起こします。

実際に発生する典型的な問題のコード例を見てみましょう:

// app.js
console.log('DATABASE_URL:', process.env.DATABASE_URL);
console.log('API_KEY:', process.env.API_KEY);

// この環境変数が正しく設定されていないと、以下のコードは失敗します
const dbConnection = require('./database').connect(process.env.DATABASE_URL);

以下のようなDockerfileとdocker-compose.ymlの組み合わせで問題が発生することがあります:

# Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

# ここで環境変数を設定
ENV NODE_ENV=production
ENV DATABASE_URL=postgres://user:password@db:5432/mydb

CMD ["node", "app.js"]
# docker-compose.yml
version: '3'
services:
  app:
    build: .
    environment:
      - NODE_ENV=development
      # DATABASE_URLが指定されていない

この場合、docker-compose.ymlでNODE_ENVは上書きされますが、DATABASE_URLはDockerfileの値が使用されます。また、API_KEYは全く設定されていないため、アプリケーションからは参照できません。

これらの問題を解決するには、環境変数の取り扱いについて Docker と Node.js の両方の観点から正しく理解する必要があります。次のセクションでは、docker-compose.ymlでの効果的な環境変数設定の方法について詳しく解説します。

docker-compose.ymlでの環境変数設定と問題点

docker-compose.ymlファイルは、複数のDockerコンテナを一括で管理するための設定ファイルですが、環境変数の設定に関していくつか注意すべき点があります。

基本的な環境変数の設定方法

docker-compose.ymlで環境変数を設定する基本的な方法は以下の通りです:

version: '3'
services:
  node-app:
    build: .
    environment:
      - NODE_ENV=development
      - PORT=3000
      - DATABASE_URL=mongodb://mongo:27017/myapp
      - API_KEY=your_api_key_here

ただし、この方法にはいくつかの問題点があります:

  1. 環境変数の値がコード内に直接記述される: 機密情報がバージョン管理システムに保存されてしまう可能性があります。

  2. 環境ごとの切り替えが難しい: 開発環境、ステージング環境、本番環境で異なる値を使いたい場合に設定が煩雑になります。

変数の参照とデフォルト値

環境変数を安全に扱うために、docker-composeでは以下のようにホストマシンの環境変数を参照することができます:

version: '3'
services:
  node-app:
    build: .
    environment:
      - NODE_ENV=${NODE_ENV:-development}
      - PORT=${PORT:-3000}
      - DATABASE_URL=${DATABASE_URL}
      - API_KEY=${API_KEY}

この方法では、以下のような動作になります:

  • ${変数名}: ホストマシンの環境変数の値を使用
  • ${変数名:-デフォルト値}: ホストマシンの環境変数が設定されていない場合、デフォルト値を使用

この方法を使うと、機密情報をdocker-compose.ymlに直接記述せずに済みます。

実際のトラブルケースと解決策

よくある問題として、以下のようなケースがあります:

ケース1: 環境変数が空になる問題

# 問題のある設定
environment:
  - API_KEY  # 値が指定されていない!

この場合、API_KEYには空の値が設定されます。正しくは以下のようにします:

# 正しい設定
environment:
  - API_KEY=${API_KEY}

ケース2: 変数展開が正しく行われない問題

# 問題のある設定
environment:
  - CONNECTION_STRING=mongodb://${MONGO_USER}:${MONGO_PASS}@mongo:27017

変数の一部だけを置換する場合、以下のように単一引用符ではなく二重引用符を使用する必要があります:

# 正しい設定
environment:
  - "CONNECTION_STRING=mongodb://${MONGO_USER}:${MONGO_PASS}@mongo:27017"

ケース3: コンテナ間の環境変数共有

関連するコンテナ間で同じ環境変数を共有したい場合は、以下のようなアプローチが有効です:

version: '3'

# トップレベルで環境変数を定義
x-common-variables: &common-variables
  DATABASE_HOST: mongodb
  DATABASE_PORT: 27017
  NODE_ENV: ${NODE_ENV:-development}

services:
  node-app:
    build: .
    environment:
      <<: *common-variables  # 共通変数を展開
      APP_SECRET: ${APP_SECRET}
  
  worker:
    build: ./worker
    environment:
      <<: *common-variables  # 同じ共通変数を展開
      WORKER_THREADS: 4

このように、YAML のアンカーとエイリアス機能を使うことで、複数のサービス間で環境変数を共有できます。

.envファイルとの連携

docker-compose.ymlの環境変数をさらに効率的に管理するには、次のセクションで解説する.envファイルとの連携が重要になります。docker-composeは自動的にカレントディレクトリにある.envファイルを読み込み、そこで定義された変数を${変数名}の形式で参照できます。

次のセクションでは、.envファイルを使った環境変数管理の正しい方法について詳しく解説します。

.envファイルを使った環境変数管理の正しい方法

.envファイルは環境変数を管理するための便利な方法ですが、Docker環境で使用する場合には特有の注意点があります。このセクションでは、Node.jsアプリケーションをDockerコンテナで実行する際の.envファイルの正しい活用方法を解説します。

.envファイルの基本

.envファイルは、単純なキーと値のペアで環境変数を定義するテキストファイルです:

# .env ファイルの例
NODE_ENV=development
PORT=3000
DATABASE_URL=mongodb://localhost:27017/myapp
API_KEY=your_secret_api_key

このファイルは通常、プロジェクトのルートディレクトリに置かれます。

Node.jsでの.envファイルの読み込み

Node.jsアプリケーションから.envファイルを読み込むには、一般的にdotenvパッケージを使用します:

// app.js
require('dotenv').config();

console.log('NODE_ENV:', process.env.NODE_ENV);
console.log('API_KEY:', process.env.API_KEY);
# インストール
npm install dotenv

Docker環境での.envファイルの扱い方

Docker環境で.envファイルを使う場合、2つの主要なアプローチがあります:

1. docker-compose.yml で env_file オプションを使用する
version: '3'
services:
  node-app:
    build: .
    env_file:
      - ./.env

このアプローチでは、.envファイルに定義された全ての環境変数がコンテナ内に読み込まれます。

2. .envファイルをコンテナにコピーし、dotenvで読み込む
# Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

# .envファイルをコピー(開発環境用)
COPY .env.example ./.env

COPY . .

CMD ["node", "app.js"]

このアプローチでは、アプリケーションコード内でdotenvパッケージを使用して.envファイルを読み込みます。

よくある問題と解決策

問題1: .envファイルがGitにコミットされてしまう

機密情報を含む.envファイルをバージョン管理システムにコミットしてはいけません。

解決策:

  1. .gitignoreファイルに.envを追加する
# .gitignore
.env
.env.local
.env.*
!.env.example
  1. .env.exampleという名前のサンプルファイルを用意し、実際の値ではなくプレースホルダーを含めておく
# .env.example
NODE_ENV=development
PORT=3000
DATABASE_URL=mongodb://localhost:27017/myapp
API_KEY=your_api_key_here
問題2: Docker環境で.envファイルが読み込まれない

解決策:

  1. パス指定を確認する:
# docker-compose.yml
services:
  node-app:
    env_file:
      - ./.env  # 相対パスが正しいことを確認
  1. 複数の環境向けに.envファイルを使い分ける:
# 環境に応じて異なる.envファイルを使用
env_file:
  - ./.env.${NODE_ENV:-development}
  1. ボリュームマウントで.envファイルを共有する:
volumes:
  - ./.env:/app/.env
問題3: 優先順位の問題

環境変数の値が複数の場所で定義されている場合、どの値が使用されるかが混乱することがあります。

優先順位(高いものから順に):

  1. コマンドラインで設定された環境変数: docker-compose run -e API_KEY=value node-app
  2. docker-compose.ymlのenvironmentセクション
  3. docker-compose.ymlのenv_fileセクションで指定されたファイル
  4. DockerfileのENV命令
  5. アプリケーション内でdotenvが読み込む.envファイル

この優先順位を理解しておくことで、環境変数の値の予期しない上書きを防ぐことができます。

本番環境での.envファイルの扱い方

本番環境では、.envファイルの代わりに、より安全な環境変数の管理方法を検討すべきです:

  1. Kubernetes Secretsを使用する:
# k8s-deployment.yaml
apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
      - name: node-app
        envFrom:
        - secretRef:
            name: node-app-secrets
  1. Docker Swarm Secretsを使用する:
# docker-compose.yml (swarm mode)
services:
  node-app:
    secrets:
      - db_password
    environment:
      - DATABASE_PASSWORD_FILE=/run/secrets/db_password
  1. クラウドプロバイダの環境変数管理サービスを使用する:
    • AWS Parameter Store
    • Google Cloud Secret Manager
    • Azure Key Vault

これらの方法を使用することで、機密情報をファイルシステムに平文で保存することなく、安全に管理できます。

次のセクションでは、実行時に環境変数を注入する高度なテクニックについて説明します。

実行時の環境変数注入テクニック

環境変数を効果的に管理するためには、コンテナの実行時に適切な値を注入するテクニックも重要です。このセクションでは、Node.jsアプリケーションをDockerコンテナで実行する際に利用できる、実行時の環境変数注入テクニックを紹介します。

コマンドラインからの環境変数の注入

Docker実行時に環境変数を直接指定することで、Dockerfileや設定ファイルを変更せずに環境変数を上書きできます。

# docker run コマンドで環境変数を指定
docker run -e NODE_ENV=production -e API_KEY=secret123 my-node-app

# docker-compose run での環境変数指定
docker-compose run -e NODE_ENV=production -e API_KEY=secret123 node-app

# docker-compose up での環境変数指定
NODE_ENV=production API_KEY=secret123 docker-compose up

この方法は、一時的な設定変更やテスト時に特に便利です。

シェルスクリプトを使った環境変数の動的設定

より複雑な環境変数の設定が必要な場合、シェルスクリプトを使うことができます:

#!/bin/bash
# run-with-env.sh

# 現在の日時をタイムスタンプとして設定
export BUILD_TIMESTAMP=$(date +%Y%m%d%H%M%S)

# 環境に応じた設定ファイルの読み込み
if [ "$ENV" = "production" ]; then
  source .env.production
else
  source .env.development
fi

# 追加の計算された環境変数を設定
export DATABASE_URL="mongodb://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}"

# Dockerコンテナを起動
docker-compose up

この方法では、複数の環境変数を事前に処理し、その結果をコンテナに渡すことができます。

起動時に環境変数を処理するエントリポイントスクリプト

Docker起動時に環境変数を処理するためのエントリポイントスクリプトを使用することもできます:

# Dockerfile
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

COPY . .

# エントリポイントスクリプトをコピー
COPY docker-entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/docker-entrypoint.sh

# エントリポイントを設定
ENTRYPOINT ["docker-entrypoint.sh"]

# デフォルトコマンド
CMD ["node", "app.js"]

エントリポイントスクリプトの例:

#!/bin/sh
# docker-entrypoint.sh

set -e

# データベースURLが指定されていない場合は構築する
if [ -z "$DATABASE_URL" ] && [ ! -z "$DB_HOST" ]; then
  export DATABASE_URL="mongodb://${DB_USER}:${DB_PASSWORD}@${DB_HOST}:${DB_PORT}/${DB_NAME}"
  echo "DATABASE_URL constructed from individual variables: $DATABASE_URL"
fi

# API_KEYが指定されておらず、ファイルパスがある場合は読み込む
if [ -z "$API_KEY" ] && [ ! -z "$API_KEY_FILE" ] && [ -f "$API_KEY_FILE" ]; then
  export API_KEY=$(cat $API_KEY_FILE)
  echo "API_KEY loaded from file: $API_KEY_FILE"
fi

# NODE_ENVに基づいて追加の設定を行う
if [ "$NODE_ENV" = "production" ]; then
  export LOG_LEVEL=${LOG_LEVEL:-"info"}
  export ENABLE_CACHE=${ENABLE_CACHE:-"true"}
else
  export LOG_LEVEL=${LOG_LEVEL:-"debug"}
  export ENABLE_CACHE=${ENABLE_CACHE:-"false"}
fi

# コマンドラインから渡された引数で元のコマンドを実行
exec "$@"

このスクリプトでは、コンテナの起動時に以下のような処理を行っています:

  • 個別の変数から複合的な接続文字列の構築
  • ファイルからの機密情報の読み込み
  • 環境に応じた設定の自動調整

Node.jsアプリケーション内での環境変数の検証と設定

Node.jsアプリケーション自体で環境変数を検証し、適切なデフォルト値を設定することも重要です:

// config.js
const dotenv = require('dotenv');

// .envファイルを読み込む(存在する場合)
dotenv.config();

// 必須の環境変数を検証
const requiredEnvVars = ['DATABASE_URL', 'API_KEY'];
const missingEnvVars = requiredEnvVars.filter(
  envVar => !process.env[envVar]
);

if (missingEnvVars.length > 0) {
  throw new Error(`Missing required environment variables: ${missingEnvVars.join(', ')}`);
}

// 設定オブジェクトを作成
const config = {
  nodeEnv: process.env.NODE_ENV || 'development',
  port: parseInt(process.env.PORT || '3000', 10),
  databaseUrl: process.env.DATABASE_URL,
  apiKey: process.env.API_KEY,
  logLevel: process.env.LOG_LEVEL || (process.env.NODE_ENV === 'production' ? 'info' : 'debug'),
  enableCache: process.env.ENABLE_CACHE === 'true',
  timeout: parseInt(process.env.TIMEOUT || '5000', 10),
};

// 設定値のサニタイズと変換
if (config.databaseUrl.includes('@') && config.databaseUrl.includes(':')) {
  // ログ表示時にパスワードをマスク
  const maskedUrl = config.databaseUrl.replace(
    /(mongodb:\/\/[^:]+:)([^@]+)(@.+)/,
    '$1*****$3'
  );
  console.log('Using database URL:', maskedUrl);
}

// 不正な設定値をチェック
if (config.port < 1024 || config.port > 65535) {
  throw new Error(`Invalid port: ${config.port}. Must be between 1024 and 65535.`);
}

// オブジェクトを凍結して変更を防止
module.exports = Object.freeze(config);

この方法では、アプリケーションの起動時に環境変数の存在と値を検証し、問題がある場合は早期に失敗させることができます。また、型変換やデフォルト値の設定も一元的に管理できます。

環境変数を使ったスケーラブルな設定管理

大規模なアプリケーションでは、環境変数を階層的に整理することで、より管理しやすくなります:

// config/database.js
module.exports = {
  url: process.env.DATABASE_URL,
  host: process.env.DB_HOST || 'localhost',
  port: parseInt(process.env.DB_PORT || '27017', 10),
  name: process.env.DB_NAME || 'myapp',
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
  ssl: process.env.DB_SSL === 'true',
  maxPoolSize: parseInt(process.env.DB_MAX_POOL_SIZE || '10', 10),
  connectionTimeout: parseInt(process.env.DB_CONNECTION_TIMEOUT || '30000', 10),
};

// config/api.js
module.exports = {
  key: process.env.API_KEY,
  baseUrl: process.env.API_BASE_URL || 'https://api.example.com',
  timeout: parseInt(process.env.API_TIMEOUT || '5000', 10),
  version: process.env.API_VERSION || 'v1',
};

// config/index.js
module.exports = {
  env: process.env.NODE_ENV || 'development',
  port: parseInt(process.env.PORT || '3000', 10),
  database: require('./database'),
  api: require('./api'),
  logging: {
    level: process.env.LOG_LEVEL || 'info',
    format: process.env.LOG_FORMAT || 'json',
  },
};

このアプローチを使うと、設定が論理的なグループに整理され、アプリケーションの成長に合わせて拡張しやすくなります。

これらのテクニックを組み合わせることで、Dockerコンテナ内のNode.jsアプリケーションの環境変数を柔軟かつ堅牢に管理できます。次のセクションでは、本番環境と開発環境での環境変数の使い分けについて詳しく説明します。

本番環境と開発環境での環境変数の使い分け

開発環境と本番環境では、異なる設定が必要になることがほとんどです。このセクションでは、Docker環境でNode.jsアプリケーションを実行する際に、開発環境と本番環境で環境変数を効果的に使い分ける方法を解説します。

環境変数を使った環境の識別

アプリケーションがどの環境で実行されているかを判断するために、通常はNODE_ENV環境変数を使用します:

// app.js
const isDevelopment = process.env.NODE_ENV !== 'production';
const isProduction = process.env.NODE_ENV === 'production';
const isStaging = process.env.NODE_ENV === 'staging';

// 環境に応じた設定
if (isDevelopment) {
  console.log('Running in development mode');
  // 開発環境固有の設定
} else if (isProduction) {
  console.log('Running in production mode');
  // 本番環境固有の設定
} else if (isStaging) {
  console.log('Running in staging mode');
  // ステージング環境固有の設定
}

環境別の.envファイル

異なる環境に合わせて複数の.envファイルを用意することができます:

# .env.development
NODE_ENV=development
PORT=3000
DATABASE_URL=mongodb://localhost:27017/myapp_dev
LOG_LEVEL=debug
ENABLE_CACHE=false

# .env.production
NODE_ENV=production
PORT=80
DATABASE_URL=mongodb://db.example.com:27017/myapp_prod
LOG_LEVEL=info
ENABLE_CACHE=true

# .env.staging
NODE_ENV=staging
PORT=8080
DATABASE_URL=mongodb://db-staging.example.com:27017/myapp_staging
LOG_LEVEL=debug
ENABLE_CACHE=true

そして、Docker Composeで環境に合わせたファイルを選択します:

# docker-compose.yml
version: '3'
services:
  node-app:
    build: .
    env_file:
      - ./.env.${NODE_ENV:-development}

実行時に環境を指定するには:

# 開発環境(デフォルト)
docker-compose up

# 本番環境
NODE_ENV=production docker-compose up

# ステージング環境
NODE_ENV=staging docker-compose up

環境別のDocker Composeファイル

より複雑な環境の違いを管理するには、環境ごとに異なるDocker Composeファイルを使用することもできます:

# docker-compose.yml(基本設定)
version: '3'
services:
  node-app:
    build: .
    ports:
      - "${PORT:-3000}:3000"
    environment:
      - NODE_ENV=${NODE_ENV:-development}
# docker-compose.dev.yml(開発環境の追加設定)
version: '3'
services:
  node-app:
    volumes:
      - .:/app
      - /app/node_modules
    environment:
      - LOG_LEVEL=debug
      - ENABLE_CACHE=false
    command: npm run dev
# docker-compose.prod.yml(本番環境の追加設定)
version: '3'
services:
  node-app:
    restart: always
    deploy:
      replicas: 3
    environment:
      - LOG_LEVEL=info
      - ENABLE_CACHE=true
    command: npm start

そして、これらのファイルを組み合わせて使用します:

# 開発環境
docker-compose -f docker-compose.yml -f docker-compose.dev.yml up

# 本番環境
docker-compose -f docker-compose.yml -f docker-compose.prod.yml up

環境ごとに異なるDockerfile

ビルド時の違いが大きい場合は、環境ごとに異なるDockerfileを用意することもできます:

# Dockerfile.dev
FROM node:18-alpine

WORKDIR /app

COPY package*.json ./
RUN npm install

# 開発ツールをインストール
RUN npm install -g nodemon ts-node

# ホットリロード用の設定
ENV CHOKIDAR_USEPOLLING=true

# ソースコードはボリュームマウントで提供されるため、コピーは不要

CMD ["npm", "run", "dev"]
# Dockerfile.prod
FROM node:18-alpine as builder

WORKDIR /app

COPY package*.json ./
RUN npm ci --only=production

COPY . .

# TypeScriptをコンパイル
RUN npm run build

# 本番用イメージを軽量化
FROM node:18-alpine

WORKDIR /app

COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./

# 本番環境変数を設定
ENV NODE_ENV=production

CMD ["npm", "start"]

そして、docker-compose.ymlでビルドに使用するDockerfileを指定します:

# docker-compose.yml
services:
  node-app:
    build:
      context: .
      dockerfile: ${DOCKERFILE:-Dockerfile.dev}

以下のように実行します:

# 開発環境(デフォルト)
docker-compose up

# 本番環境
DOCKERFILE=Dockerfile.prod docker-compose up

環境変数をパラメータ化したk8s設定

Kubernetesを使用する場合は、ConfigMapsとSecretsを使って環境変数を管理できます:

# configmap-dev.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: node-app-config
  labels:
    env: development
data:
  NODE_ENV: "development"
  PORT: "3000"
  LOG_LEVEL: "debug"
  ENABLE_CACHE: "false"
# configmap-prod.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: node-app-config
  labels:
    env: production
data:
  NODE_ENV: "production"
  PORT: "80"
  LOG_LEVEL: "info"
  ENABLE_CACHE: "true"
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: node-app
spec:
  replicas: 3
  template:
    spec:
      containers:
      - name: node-app
        image: my-node-app:latest
        envFrom:
        - configMapRef:
            name: node-app-config
        - secretRef:
            name: node-app-secrets

パラメータ化された秘密情報の管理

機密情報を環境ごとに管理する方法も重要です:

# 開発環境の秘密情報を管理
./scripts/create-secrets.sh development

# 本番環境の秘密情報を管理
./scripts/create-secrets.sh production

スクリプトの例:

#!/bin/bash
# create-secrets.sh

ENV=$1

case "$ENV" in
  "development")
    kubectl create secret generic node-app-secrets \
      --from-literal=API_KEY=dev_api_key \
      --from-literal=DATABASE_URL=mongodb://localhost:27017/myapp_dev
    ;;
  "production")
    # 本番環境では外部のシークレット管理ツールから値を取得
    API_KEY=$(aws secretsmanager get-secret-value --secret-id my-app/api-key --query SecretString --output text)
    DB_URL=$(aws secretsmanager get-secret-value --secret-id my-app/database-url --query SecretString --output text)
    
    kubectl create secret generic node-app-secrets \
      --from-literal=API_KEY="$API_KEY" \
      --from-literal=DATABASE_URL="$DB_URL"
    ;;
  *)
    echo "Usage: $0 [development|production]"
    exit 1
    ;;
esac

これらの方法を組み合わせることで、開発環境から本番環境まで一貫性のある方法で環境変数を管理できます。また、各環境の特性に合わせた設定を柔軟に行うことができます。

次のセクションでは、環境変数のセキュリティを確保するためのベストプラクティスについて説明します。

環境変数のセキュリティを確保するベストプラクティス

Dockerコンテナ内でNode.jsアプリケーションを実行する際、環境変数を通じて機密情報を扱うことになるため、セキュリティの確保は非常に重要です。このセクションでは、環境変数のセキュリティを保護するためのベストプラクティスを紹介します。

機密情報をDockerイメージに含めない

機密情報(APIキー、パスワードなど)をDockerfileに直接含めると、そのイメージを持つ誰もがその情報にアクセスできてしまいます。

やめるべき方法:

# 危険な方法
ENV API_KEY=1234567890abcdef
ENV DATABASE_PASSWORD=super_secret_password

推奨される方法:

  1. 実行時に環境変数を注入する:
docker run -e API_KEY=$(aws secretsmanager get-secret-value --secret-id api-key --query SecretString --output text) my-app
  1. シークレット管理ツールを使う:
# docker-compose.yml
services:
  app:
    environment:
      - API_KEY=${API_KEY}
      - DATABASE_PASSWORD=${DATABASE_PASSWORD}

そして、これらの値を安全に管理・設定します。

イメージ履歴からの情報漏洩を防ぐ

Dockerイメージの履歴には、以前のレイヤーで設定された環境変数が残ります。

問題のある方法:

# 最初に機密情報を設定
ENV SECRET_KEY=my_secret_key

# 後で変更しても、履歴に残る
ENV SECRET_KEY=placeholder

推奨される方法: マルチステージビルドを使って履歴をリセットする:

FROM node:18-alpine AS builder
# ビルド処理

FROM node:18-alpine
# 必要なファイルだけをコピー
COPY --from=builder /app/dist /app/dist
# ランタイム設定のみを行う

実行時の環境変数の保護

コンテナ実行中の環境変数も保護する必要があります:

  1. proc情報の保護:
# コンテナ内での/proc/self/environへのアクセスを制限
RUN chmod 700 /proc/self/environ
  1. 非rootユーザーでの実行:
# 専用ユーザーを作成
RUN addgroup -g 1001 nodejs && \
    adduser -u 1001 -G nodejs -s /bin/sh -D nodejs

# アプリケーションディレクトリの所有権を変更
WORKDIR /app
RUN chown -R nodejs:nodejs /app

# 非rootユーザーに切り替え
USER nodejs

暗号化キーと認証情報の管理

特に重要な暗号化キーや認証情報は、より強固な保護が必要です:

  1. 外部シークレット管理ツールの使用:

    • AWS Secrets Manager
    • HashiCorp Vault
    • Google Cloud Secret Manager
  2. シークレットをファイルとして提供:

# docker-compose.yml
services:
  app:
    volumes:
      - ./secrets:/run/secrets
    environment:
      - API_KEY_FILE=/run/secrets/api_key

アプリケーションコード:

// ファイルから機密情報を読み込む
const fs = require('fs');
const apiKey = fs.readFileSync(process.env.API_KEY_FILE, 'utf8').trim();

環境変数の検証とサニタイズ

環境変数の値は常に検証し、サニタイズすることが重要です:

// 環境変数の検証関数
function validateEnv(name, pattern, defaultValue = null) {
  const value = process.env[name];
  
  // 値が存在するか確認
  if (!value) {
    if (defaultValue !== null) {
      return defaultValue;
    }
    throw new Error(`Required environment variable ${name} is missing`);
  }
  
  // パターンに一致するか確認
  if (pattern && !pattern.test(value)) {
    throw new Error(`Environment variable ${name} does not match required pattern`);
  }
  
  return value;
}

// 使用例
const port = parseInt(validateEnv('PORT', /^\d+$/, '3000'), 10);
const apiKey = validateEnv('API_KEY', /^[a-zA-Z0-9]{32}$/);
const dbUri = validateEnv('DATABASE_URI', /^mongodb:\/\/.+/);

ロギングと出力での機密情報の保護

環境変数をログに出力する際は、センシティブな情報をマスクする必要があります:

// 安全なログ出力関数
function safeLog(obj) {
  // 機密情報フィールドのリスト
  const sensitiveFields = ['password', 'secret', 'key', 'token', 'auth'];
  
  // オブジェクトをディープコピー
  const safeObj = JSON.parse(JSON.stringify(obj));
  
  // 機密情報をマスク
  function maskSensitiveInfo(obj, parentKey = '') {
    Object.keys(obj).forEach(key => {
      const fullKey = parentKey ? `${parentKey}.${key}` : key;
      
      if (sensitiveFields.some(field => fullKey.toLowerCase().includes(field))) {
        // 機密情報をマスク
        obj[key] = typeof obj[key] === 'string' ? '******' : null;
      } else if (obj[key] && typeof obj[key] === 'object') {
        // ネストされたオブジェクトを再帰的に処理
        maskSensitiveInfo(obj[key], fullKey);
      }
    });
  }
  
  maskSensitiveInfo(safeObj);
  console.log(safeObj);
  return safeObj;
}

// 使用例
safeLog({
  apiEndpoint: 'https://api.example.com',
  apiKey: 'secret-key-12345',
  config: {
    databasePassword: 'db-password',
    adminToken: 'admin-token'
  }
});
// 出力: { apiEndpoint: 'https://api.example.com', apiKey: '******', config: { databasePassword: '******', adminToken: '******' } }

コンテナ間通信の保護

複数のコンテナ間で環境変数を安全に共有する方法:

  1. Docker Networkを使用:
# docker-compose.yml
services:
  app:
    networks:
      - backend
  db:
    networks:
      - backend
networks:
  backend:
    driver: bridge
  1. 環境固有のコンテナ間認証:
// アプリケーションレベルでの認証
const internalServiceToken = process.env.INTERNAL_SERVICE_TOKEN;
const headers = {
  'Authorization': `Bearer ${internalServiceToken}`
};
fetch('http://internal-service/api/data', { headers });

セキュリティ監査とスキャン

定期的なセキュリティチェックも重要です:

  1. イメージスキャン:
# Dockerイメージの脆弱性スキャン
docker scan my-node-app:latest
  1. 依存関係チェック:
# Node.jsの依存関係の脆弱性チェック
npm audit
  1. シークレットスキャン:
# gitリポジトリ内のシークレットをスキャン
git-secrets --scan

CI/CDパイプラインでのセキュリティ対策

CI/CDパイプラインでも環境変数のセキュリティを考慮する必要があります:

# .gitlab-ci.yml の例
stages:
  - build
  - test
  - deploy

build:
  stage: build
  script:
    - docker build -t my-node-app .

test:
  stage: test
  script:
    - docker run --rm my-node-app npm test

deploy:
  stage: deploy
  environment:
    name: production
  script:
    # 安全にシークレットを取得
    - API_KEY=$(aws secretsmanager get-secret-value --secret-id api-key --query SecretString --output text)
    # シークレットを使用してデプロイ
    - docker run -e API_KEY="$API_KEY" my-node-app

これらのベストプラクティスを組み合わせることで、Dockerコンテナ内のNode.jsアプリケーションにおける環境変数のセキュリティを大幅に向上させることができます。また、定期的にセキュリティを見直し、最新の脅威に対応することも重要です。

環境変数の安全な管理は、アプリケーションのセキュリティ全体の重要な一部です。これらのプラクティスを実施し、チームのセキュリティ意識を高めることで、より安全なDockerベースのNode.jsアプリケーションを構築・運用できるでしょう。

TH

Tasuke Hub管理人

東証プライム市場上場企業エンジニア

情報系修士卒業後、大手IT企業にてフルスタックエンジニアとして活躍。 Webアプリケーション開発からクラウドインフラ構築まで幅広い技術に精通し、 複数のプロジェクトでリードエンジニアを担当。 技術ブログやオープンソースへの貢献を通じて、日本のIT技術コミュニティに積極的に関わっている。

🎓情報系修士🏢東証プライム上場企業💻フルスタックエンジニア📝技術ブログ執筆者

このトピックはこちらの書籍で勉強するのがおすすめ!

この記事の内容をさらに深く理解したい方におすすめの一冊です。実践的な知識を身につけたい方は、ぜひチェックしてみてください!

おすすめ記事

おすすめコンテンツ