Dockerにおける環境変数の極意:設定とシークレットの使い分け

環境変数をマスターして、安全で柔軟なDockerデプロイを実現しましょう。この包括的なガイドでは、一般的なアプリケーション設定に環境変数を使用する場合と、APIキーやパスワードなどの機密データを安全に管理する場合の重要な違いを明確にします。非機密設定を渡す実用的な方法を学び、環境変数を介してシークレットを公開する深刻なリスクを理解し、Docker SecretsとComposeを活用した堅牢で暗号化されたシークレット管理の方法を発見してください。Dockerの知識を高め、アプリケーションを保護しましょう。

Dockerにおける環境変数の極意:設定とシークレットの使い分け

環境変数はDockerにおいて便利です。なぜなら、同じイメージを開発、ステージング、本番環境で異なる設定で実行できるからです。しかし、パスワード、署名キー、APIトークンをログレベルやポート番号と同じ場所に置くと、その便利さがリスクに変わります。

明確な考え方はシンプルです。環境変数は機密性のないランタイム設定には適しています。シークレットは、アクセスを制限しローテーションプランを持つシークレットストアまたはマウントされたシークレットファイルから取得する必要があります。

設定のための環境変数を理解する

環境変数は、Dockerコンテナで実行されるアプリケーションを含む、実行時設定を渡すための直接的で広く採用されている方法です。これにより、Dockerイメージを再構築することなくアプリケーションの動作を変更できるため、コンテナの柔軟性と移植性が向上します。これは、アプリケーションのポート番号、デバッグフラグ、サードパーティサービスのURLなど、機密性のない動的な設定に最適です。

設定変数を渡す方法

Dockerには、コンテナに環境変数を定義して注入するいくつかの方法があります。

1. DockerfileのENV命令

ENV命令は、コンテナ実行時に利用可能になるデフォルトの環境変数を設定します。これは、変更される可能性が低い変数や、アプリケーションに適切なデフォルト値を提供する場合に適しています。

FROM alpine:latest

ENV APP_PORT=8080
ENV DEBUG_MODE=false

COPY ./app /app
WORKDIR /app
CMD ["/app/start.sh"]

ヒント: ENVはデフォルト値を設定しますが、実行時に上書きできます。

2. docker runでの-eまたは--envフラグ

単一のコンテナを起動する場合、-eまたは--envフラグを使用して環境変数を直接渡すことができます。これは、アドホックなテストや、Dockerfileのデフォルトとは異なる特定の設定を提供する場合によく使用されます。

docker run -d -p 80:8080 --name my_app_instance \
  -e APP_PORT=80 \
  -e DEBUG_MODE=true \
  my_app_image:latest

3. Docker Composeのenv_file

複数の環境変数を管理する場合、特にdocker-compose.ymlファイルで定義された複数のサービスにわたる場合、env_fileオプションは非常に便利です。これにより、1つ以上の.envファイルから変数を読み込むことができ、docker-compose.ymlをよりクリーンに保つことができます。

docker-compose.yml:

version: '3.8'
services:
  webapp:
    image: my_app_image:latest
    ports:
      - "80:8080"
    env_file:
      - ./config/app.env

./config/app.env:

APP_PORT=8080
DEBUG_MODE=false
API_ENDPOINT=https://api.example.com/v1

4. Docker Composeのenvironmentキー

または、docker-compose.ymlのサービスのenvironmentセクション内で環境変数を直接定義することもできます。これは、変数の数が少ない場合や、単一のサービスに固有の変数がある場合によく使用されます。

version: '3.8'
services:
  webapp:
    image: my_app_image:latest
    ports:
      - "80:8080"
    environment:
      APP_PORT: 8080
      DEBUG_MODE: false

シークレットに環境変数を使用する際の落とし穴

環境変数は設定には優れていますが、データベースパスワード、APIキー、秘密SSHキーなどの機密データ(シークレット)を管理するには、基本的に安全ではありません。これは、特に開発環境で見落とされがちな重大なセキュリティ脆弱性です。

環境変数がシークレットに安全でない理由:

  1. docker inspectによる可視性: Dockerホストにアクセスできる人は誰でも、docker inspect <container_id>を使用して実行中のコンテナの環境変数を簡単に表示できます。つまり、シークレットが平文で見えてしまいます。

    # シークレットを公開する例(本番環境では絶対に行わないでください)
    docker run -d -e DB_PASSWORD=mysecretpassword --name insecure_app nginx:latest
    
    # 誰でもパスワードを見ることができます
    docker inspect insecure_app | grep DB_PASSWORD
    
  2. プロセスのスヌーピング: コンテナ内では、他のプロセスやユーザー(複数のユーザーが存在する場合)が、特にアプリケーションがrootとして実行されている場合や昇格された権限を持っている場合に、環境変数を読み取ることができる可能性があります。

  3. ログと履歴: 環境変数が誤ってログ、CI/CDパイプライン履歴、またはシェル履歴に含まれ、偶発的な公開につながる可能性があります。

  4. イメージレイヤー: Dockerfileでシークレットを含むENVを使用すると、そのシークレットはイメージレイヤーに焼き付けられ、後続のレイヤーでunsetしようとしても残ります。これにより、イメージ自体からシークレットを取得できるようになります。

  5. 偶発的な共有: シークレットを含む.envファイルやdocker-compose.ymlファイルがバージョン管理システムにコミットされたり、不適切に共有されたりして、広範囲に公開されることがよくあります。

警告: 機密情報を通常の環境変数として扱うことは、一般的なセキュリティ上の誤りです。環境変数はホスト上およびコンテナ内で公開されていると常に想定してください。

Dockerでシークレットを安全に管理する

機密データに対する環境変数のセキュリティ上の欠点に対処するために、Dockerは専用のシークレット管理機能を提供しています。主にDocker Secrets(Docker Swarm用)と、secrets機能を備えたDocker Compose(Docker Swarmシークレットを活用したり、単にファイルをマウントしたりできます)などの外部ツールです。

Docker Secrets(Docker Swarmモード)

Docker Secretsは、Docker Swarmモードに統合された機能で、サービスの機密データを安全に送信および保存する方法を提供します。シークレットは以下の特徴があります。

  • SwarmマネージャーのRaftログで暗号化されて保存されます。
  • 承認されたサービスタスクに安全に送信されます。
  • 環境変数として公開されるのではなく、コンテナのファイルシステム内のメモリ内ファイル(通常は/run/secrets/<secret_name>)としてマウントされます。
  • 明示的にアクセスを許可されたサービスのみがアクセスできます。

Docker Secretsの使用方法(Swarmモード)

  1. Swarmを初期化する(まだの場合):
    
    

docker swarm init ```

  1. シークレットを作成する: シークレットはファイルまたは標準入力から作成されます。
    
    

echo "my_secure_db_password" | docker secret create db_password_secret - echo "SG.your_api_key_here" | docker secret create sendgrid_api_key - ```

  1. シークレットを使用してサービスをデプロイする: サービスは名前でシークレットを参照します。Dockerはシークレットをコンテナにマウントします。
    
    

docker service create --name my-webapp
--secret db_password_secret
--secret sendgrid_api_key
my_app_image:latest ```

  1. コンテナ内でシークレットにアクセスする: アプリケーションはマウントされたファイルパスからシークレットを読み取ります。
    
    

Pythonアプリケーションコード内(他の言語も同様)

with open('/run/secrets/db_password_secret', 'r') as f: db_password = f.read().strip()

with open('/run/secrets/sendgrid_api_key', 'r') as f: sendgrid_key = f.read().strip() ```

Docker Composeとシークレット(シングルホストまたはSwarm用)

Docker Composeバージョン3.1以降では、secretsセクションが導入され、docker-compose.yml内でシークレットを定義および参照できるようになりました。Swarmモードで実行する場合、ComposeはDocker Swarmのネイティブシークレットを活用します。Swarmモードなしでシングルホストで実行する場合でも、Composeはホストからコンテナにファイルを安全にマウントすることでシークレットをサポートしますが、Swarmが提供する保存時の暗号化はありません。

docker-compose.ymlでのsecretsの使用

  1. シークレットを定義する: 外部ファイルを参照するか、外部シークレット(事前に作成されたSwarmシークレット)にすることでシークレットを定義できます。

    # docker-compose.yml
    version: '3.8'
    
    services:
      webapp:
        image: my_app_image:latest
        ports:
          - "80:8080"
        secrets:
          - db_password
          - sendgrid_api_key
    
    secrets:
      db_password:
        file: ./secrets/db_password.txt # パスワードを含むホスト上のファイルへのパス
      sendgrid_api_key:
        external: true                   # 'sendgrid_api_key'という名前の既存のDocker Swarmシークレットを参照します
    
  2. ローカルシークレットファイルを作成する(fileを使用する場合):

    
    

mkdir secrets echo "my_local_db_password" > ./secrets/db_password.txt ```

  1. Composeでデプロイする: docker compose up -dでサービスをデプロイすると、コンテナ内の/run/secrets/<secret_name>でシークレットが利用可能になります。

    # コンテナ内では、./secrets/db_password.txtの内容は次の場所にあります:
    # /run/secrets/db_password
    

適切なツールの選択: 設定とシークレット

設定に環境変数を使用するか、専用のシークレット管理ソリューションを使用するかの決定は、1つの主要な質問に帰着します。

データは機密ですか?

  • はい(機密データ)の場合: Docker Secrets(Swarmを使用)または同様のシークレット管理システム(例:Kubernetes Secrets、HashiCorp Vault)を使用します。シングルホストのComposeセットアップの場合は、secretsセクションを使用してファイルを安全にマウントします。
  • いいえ(機密性のない設定)の場合: 環境変数(DockerfileのENV-eフラグ、env_file、またはComposeのenvironmentを介して)を使用します。
機能 環境変数(設定用) Docker Secrets(機密データ用)
目的 機密性のないアプリケーション設定 パスワードやAPIキーなどの機密データ
可視性 docker inspectやプロセス検査で表示可能 ファイルとしてマウントされ、通常の環境値としては表示されない
セキュリティ 機密データには不適切 より強力な処理。Swarmシークレットは保存時に暗号化され、ローカルComposeファイルシークレットはホストファイルの保護に依存する
アプリでのアクセス os.environなどから読み取る /run/secrets/<secret_name>ファイルから読み取る
管理元 Dockerランタイム、Docker Compose Docker Swarm、Docker Compose、または外部シークレットマネージャー
ユースケース ポート番号、デバッグフラグ、機密性のないURL データベースパスワード、APIトークン、秘密鍵

両方のベストプラクティス

設定(環境変数)の場合:

  • DockerfileでENVを使用して適切なデフォルト値を提供します。これにより、イメージをすぐに実行可能になり、期待される変数が明確に文書化されます。
  • 可能な場合は設定を外部化します。大規模なデプロイメントには、docker compose.envファイルを使用するか、外部設定サービスを使用します。
  • すべての設定オプションとその期待される値を、おそらくREADME.mdやアプリケーションドキュメントに文書化します。
  • 環境(開発、ステージング、本番)間で変更される可能性のある値をハードコーディングしないでください。

シークレット(Docker Secretsなど)の場合:

  • シークレット(例:シークレットを含む.envファイル、db_password.txt)をGitなどのバージョン管理システムに決してコミットしないでください。
  • 定期的にシークレットをローテーションします。これにより、シークレットが漏洩した場合の露出期間を最小限に抑えます。
  • 最小権限を付与します。サービスに、絶対に必要なシークレットへのアクセスのみを許可します。
  • シークレット値のログ記録を避ける。アプリケーションとインフラストラクチャのログがシークレットの内容を出力しないようにしてください。
  • 大規模なエンタープライズグレードのデプロイメントには、HashiCorp VaultAWS Secrets ManagerAzure Key Vaultなどの専用のシークレット管理ソリューションを検討してください。これらは、監査、動的シークレット生成、IDおよびアクセス管理(IAM)との統合などのより高度な機能を提供します。

何をどこに配置するかを決定するための実用的なルール

値をenvironmentに追加する前に、チームメイトがそれをサポートチケットに貼り付けたらどうなるかを自問してください。答えが「深刻なことは何も起こらない」であれば、おそらく設定です。答えが「認証情報をローテーションする必要がある」であれば、それはシークレットです。

適切な環境変数:

APP_ENV=production
LOG_LEVEL=info
PUBLIC_BASE_URL=https://example.com
FEATURE_SIGNUP_ENABLED=false
REDIS_HOST=redis

不適切な環境変数:

DATABASE_PASSWORD=...
STRIPE_SECRET_KEY=...
JWT_SIGNING_KEY=...
AWS_SECRET_ACCESS_KEY=...
PRIVATE_SSH_KEY=...

グレーゾーンもあります。データベースのホスト名は通常、設定です。ユーザー名とパスワードを含む完全なデータベースURLはシークレットです。公開分析キーはブラウザアプリケーションでは安全かもしれませんが、同じベンダーのプライベートAPIトークンは安全ではありません。疑わしい場合は、反証できるまで値を機密として扱ってください。

Composeの.envファイルは誤解されやすい

Docker Composeは.envを2つの異なる方法で使用するため、人々はしばしば混同します。

まず、Composeはcompose.yml内の変数置換のためにプロジェクトレベルの.envファイルを読み取ります:

services:
  web:
    image: "${APP_IMAGE}"
    ports:
      - "${HOST_PORT}:8080"

次に、env_fileは変数をコンテナに渡します:

services:
  web:
    image: my-app
    env_file:
      - ./app.env

これらのファイルは似ているように見えますが、目的は異なります。前者はComposeが設定をレンダリングするのに役立ちます。後者はコンテナ内のランタイム環境になります。プロジェクトの.envの値が、明示的に渡さない限り自動的にコンテナ内に現れるとは想定しないでください。

ローカル開発では、チェックインされたサンプルファイルが役立ちます:

# .env.example
APP_ENV=development
LOG_LEVEL=debug
PUBLIC_BASE_URL=http://localhost:3000

次に、実際の.envをGitの管理外に置きます:

.env
*.env.local
secrets/

サンプルファイルは、プライベートな値を公開せずにアプリが期待するものを文書化します。

アプリケーションでファイルベースのシークレットを読み取る

多くのアプリケーションはすでに環境変数にシークレットがあることを期待しています。しばらくの間両方のパターンをサポートすれば、ファイルベースのシークレットへの移行は簡単です。

例えば、Node.jsのヘルパー:

import fs from "node:fs";

function readSecret(name) {
  const filePath = process.env[`${name}_FILE`];
  if (filePath) {
    return fs.readFileSync(filePath, "utf8").trim();
  }
  return process.env[name];
}

const databasePassword = readSecret("DATABASE_PASSWORD");

次に、Composeファイルでマウントされたシークレットファイルをポイントできます:

services:
  web:
    image: my-app
    environment:
      DATABASE_PASSWORD_FILE: /run/secrets/db_password
    secrets:
      - db_password

secrets:
  db_password:
    file: ./secrets/db_password.txt

このパターンは、本番環境をファイルマウントシークレットに移行している間、アプリケーションが古い環境でも引き続き実行できるため、うまく機能します。多くの公式イメージは、この理由で_FILEで終わる変数をすでにサポートしています。

ビルド引数にもシークレットを入れないでください

環境変数だけが罠ではありません。プライベートパッケージを取得したりリポジトリをクローンしたりするためにビルド引数を使用すると、ビルド引数も漏洩する可能性があります:

ARG NPM_TOKEN
RUN npm config set //registry.npmjs.org/:_authToken=$NPM_TOKEN

最終的なコンテナにNPM_TOKENが表示されなくても、ビルド履歴と中間レイヤーは予想以上に多くの情報を公開する可能性があります。BuildKitでは、ビルド時のシークレットにシークレットマウントを使用します:

# syntax=docker/dockerfile:1.7
FROM node:22-slim
WORKDIR /app
COPY package.json package-lock.json ./
RUN --mount=type=secret,id=npm_token \
  NPM_TOKEN="$(cat /run/secrets/npm_token)" npm ci

次のようにビルドします:

docker build \
  --secret id=npm_token,src=.npm-token \
  -t my-app .

これにより、トークンがDockerfileから保護され、通常のレイヤーに焼き付けられるのを防ぎます。ローカルの.npm-tokenファイルとCIシークレットストレージは引き続き保護する必要があります。

Kubernetes、クラウドシークレットマネージャー、およびDocker

Docker SecretsはSwarmで役立ち、Composeシークレットはローカルまたはシングルホストのセットアップに役立ちます。Kubernetesでは、通常、Kubernetes Secrets、外部シークレットオペレーター、またはクラウドシークレットマネージャー統合を使用します。AWSでは、チームはAWS Secrets ManagerまたはSystems Manager Parameter Storeを使用することがよくあります。Azureでは、Azure Key Vaultが一般的です。Google Cloudでは、Secret Managerが同じ役割を果たします。

原則はプラットフォーム間で同じです:

  • 機密値をシークレット用に設計されたシステムに保存します。
  • ランタイムIDに、必要なシークレットへのアクセスのみを許可します。
  • ランタイム時にシークレットをマウントまたは注入します。
  • イメージを再構築せずにシークレットをローテーションします。
  • シークレットをソース管理、イメージレイヤー、ログ、ダッシュボードから遠ざけます。

Kubernetes Secretsはデフォルトでエンコードされており、すべてのクラスター構成で自動的に暗号化されるわけではありません。多くのマネージドクラスターは保存時の暗号化をサポートしていますが、想定するのではなく実際のクラスター設定を確認してください。リスクの高い認証情報には、監査ログとローテーションサポートを備えたクラウドシークレットマネージャーまたは専用ツールを使用します。

ローテーションは設計の一部です

ローテーションできないシークレット戦略は未完成です。本番環境の前に次の質問をしてください:

  • イメージを再構築せずにデータベースパスワードを変更できますか?
  • ロールアウト中に2つの有効な認証情報が重複する可能性がありますか?
  • アプリケーションはシークレットを再読み取りしますか、それとも再起動が必要ですか?
  • 古い認証情報はどこに記録、キャッシュ、または保存されていますか?
  • シークレットが変更された場合、誰に通知されますか?

データベースの場合、ローテーションは多くの場合、2番目の認証情報を作成し、新しい認証情報でアプリケーションをデプロイし、トラフィックを確認してから古いものを取り消すことを意味します。APIキーの場合、プロバイダーによって異なります。一部のサービスは複数のアクティブキーを許可します。他のサービスはカットオーバーを強制します。最も柔軟性の低い依存関係に基づいてデプロイプロセスを設計します。

偶発的な公開をクリーンアップする

シークレットがすでにGitにコミットされたり、イメージに焼き付けられたりしている場合、行を削除するだけでは不十分です。公開されたものとして扱います。

通常の対応は次のとおりです:

  1. 認証情報を取り消すかローテーションします。
  2. 現在のコードまたはイメージから削除します。
  3. CIログ、イメージレジストリ、課題トラッカー、チャットメッセージにコピーがないか確認します。
  4. 組織が調整を処理する準備ができている場合にのみ、Git履歴を書き換えます。ローテーションは引き続き必要です。
  5. スキャンまたはプリコミットチェックを追加して、繰り返しのミスを減らします。

ツールは役立ちますが、習慣に取って代わるものではありません。シークレットファイルに明確に名前を付け、Gitで無視し、起動時に設定オブジェクトをまとめて出力しないようにします。

実用的なパターン

アプリがこの環境でどのように実行されるべきかを説明する値(ポート、ログレベル、機能フラグ、サービスホスト名、機密性のないURL)には環境変数を使用します。IDを証明したりアクセスを許可したりする値(パスワード、トークン、署名キー、秘密鍵、プロバイダー認証情報)にはシークレットを使用します。

クリーンなDockerイメージは環境間で同じです。開発、ステージング、本番はランタイムで動作を変更します。設定は環境変数として移動できます。シークレットは、アクセスが制限されたシークレットストアまたはマウントされたシークレットファイルから取得する必要があります。この分離により、すべてのコンテナ検査、ログ行、イメージレイヤーを認証情報漏洩に変えることなく、デプロイの柔軟性を維持できます。