2-4. 環境変数とシークレット - 12 Factor App と漏えい対策
所要時間: 40-50分 ゴール: 12 Factor App の Config 原則を理解し、開発と本番でシークレットを安全に投入できる コミット内容:
~/learn/infra/day11/の.env.example、シークレット投入 compose、.gitignoreの設定例
このレッスンのゴール
- 12 Factor App の Config 原則を1分で説明できる
-
environmentとenv_fileを場面別に使い分けられる - 環境変数で secret を渡すリスク(
ps,inspect,env dump)を語れる - Docker Secrets / AWS Secrets Manager / Vault の使い所を把握する
-
.envを.gitignore確実に弾く運用ができる
なぜ学ぶか(実務悩みベース)
- 「AWS キーを GitHub に commit」→ 数分で不正利用される事故を確実に防ぐ
- 開発・ステージング・本番で接続先を切り替える設計が必要
- セキュリティレビューで「シークレット平文書き」を指摘されない
- ローテーション運用が回るチームになる
前章とのつながり
2-3 で外部公開ポートを絞った。今回は 設定情報をどう安全にコンテナへ渡すか の話。ネットワーク層と認証情報層、両方を締めて初めて「本番に出せる」状態になる。
大前提: シークレット漏えいは1度で会社が傾く
「うっかり API キーを Git に commit した」「Dockerfile に DB パスワードをハードコード」これは 新人がやらかす定番事故 であり、ベテランも油断するとやる事故 でもある。
実際の事故例:
- AWS のアクセスキーを GitHub に push → 数分で発見され、不正な EC2 が大量起動。数十万円の請求 が来る
- Stripe の本番キーが Public リポジトリに → 偽決済を仕込まれて返金祭り
- パスワードを
DockerfileのENVに書いてイメージレジストリへ push → 全社員+外部委託先が閲覧可能に
これらを防ぐには 「シークレットはコードから完全に切り離す」 という原則を最初から守ること。後から付け加えるのは非常に難しい。今日はその原則と実装パターンを学ぶ。
本日の到達点
- 12 Factor App の III. Config を理解(環境変数で設定を渡す)
environmentとenv_fileの使い分け.envを.gitignoreで確実に弾く- 環境変数で secret を渡すリスク(プロセス一覧、ログ、env dump)
- Docker Secrets による安全な投入
- AWS Secrets Manager / HashiCorp Vault / GitHub Actions Secrets への入口
セッション①: 12 Factor App と環境変数(25-30分)
0. 録画スタート&作業ディレクトリ
mkdir -p ~/log ~/learn/infra/day11
cd ~/learn/infra/day11
script ~/log/infra_day11.log1. 12 Factor App の III. Config
コードに混ぜていいもの:
- ビジネスロジック
- 構造(テーブル、API スキーマ)
- フレームワーク選定
コードに混ぜてはいけないもの:
- DB 接続情報
- API キー、シークレット
- 環境固有の値(本番 URL、リージョン)
- 機能フラグの状態
12 Factor App とは
2011年に Heroku が公開した、現代的な SaaS アプリの12個の設計原則。Docker、Kubernetes、サーバーレスの設計思想の 共通基盤 になっている。
原文: 12factor.net
12項目のうち、コンテナ運用で特に重要なのが III. Config:
設定 (Config) を環境変数に格納する Config はコードから厳密に分離されているべきである。 … コードに混ぜたら最後、新しいリリース毎にビルドし直さなければならず、本番と開発で同じコードが使えなくなる
なぜ環境変数かというと:
- すべての OS / 言語 / フレームワークでサポートされている共通インターフェース
- イメージは1つで環境ごとに変えられる(コンテナの利点を最大化)
- 設定変更でリビルドが不要
設定 = どこに置くべきか
種類 例 どこに置く 機密でない環境固有値 LOG_LEVEL=info、PORT=8080環境変数( .env、composeenvironment)機密値(DB パスワード、APIキー) STRIPE_API_KEY=sk_live_...Secret 管理ツール(後述) ビジネスロジック バリデーションルール、計算ロジック コードに書く 機能フラグ A/B テストの分岐 機能フラグサービス(LaunchDarkly 等) 「環境変数に何でも入れる」のも雑。機密 vs 非機密 で分けるのが基本。
2. environment と env_file
services:
api:
image: myapi:1.0
# 方法 A: 直接書く
environment:
LOG_LEVEL: info
PORT: "8080"
DATABASE_URL: "app:apppass@tcp(db:3306)/app"
# 方法 B: ファイルから
env_file:
- .env
- .env.production
# 方法 C: ホストの環境変数を渡す
environment:
STRIPE_KEY: ${STRIPE_KEY} # シェルの環境変数を参照environment vs env_file の使い分け
方法 メリット デメリット 使い所 environment直書きYAML 1枚で完結、見やすい シークレットを書くと git に commit される 非機密な設定のみ env_fileシークレットを別ファイルへ分離できる ファイルの所在管理が必要 開発環境のシークレット ${VAR}補間CI / シェルから動的注入 シェルの状態に依存 CI / 本番デプロイ 実務での組み合わせ
environment: LOG_LEVEL: info # 非機密、直書き DATABASE_URL: ${DATABASE_URL} # シェルから注入 env_file: - .env.local # 開発時のローカルオーバーライド
Compose の変数補間の仕組み
${VAR}は Compose が YAML を読む時に展開する。services: api: image: myapi:${TAG:-latest} # TAG が未設定なら "latest" を使う environment: DATABASE_URL: ${DATABASE_URL} LOG_LEVEL: ${LOG_LEVEL:-info} # デフォルト値変数の取得元(優先順):
- シェル環境変数(
export DATABASE_URL=...).envファイル(プロジェクトルートの.envを自動で読む)**「.env ファイルが自動で読まれる」**のは Compose の親切機能。
env_file:を書かなくても、compose.ymlと同じディレクトリの.envは変数補間に使われる。
3. .env と .env.example
# .env.example (リポジトリに commit する。雛形)
DATABASE_URL=app:CHANGE_ME@tcp(db:3306)/app
STRIPE_API_KEY=sk_test_xxxxx
LOG_LEVEL=info
# .env (個人のローカル設定。絶対 commit しない)
DATABASE_URL=app:my_real_dev_password@tcp(db:3306)/app
STRIPE_API_KEY=sk_test_51A2B3C...
LOG_LEVEL=debug# .gitignore (絶対必須)
.env
.env.local
.env.*.local
*.pem
*.key.env と .env.example のセット運用
.env.example: 「こういう変数を埋めてください」のテンプレート。リポジトリに含める。
.env: 個人の実際の値。Git で無視する。
開発者のオンボーディング:
git clone myrepo cd myrepo cp .env.example .env # コピーして vim .env # 個別に値を埋める docker compose up -dこれにより:
- 「どの環境変数が必要か」がコードベースで自明
- 値の漏えいは起きない(.env は git にない)
- チームで形式が揃う
アンチパターン:
.envを git commitgit add .env git commit -m "add env" git push origin mainなぜ最悪か:
- 公開リポジトリなら全世界に漏えい
- プライベートリポジトリでも、退職した社員、外部委託先、ハッキングされた開発者 PC から流出可能
- git の歴史から消すのは事実上不可能(
git filter-branchでも、すでに clone した人の手元には残る)やってしまった場合の対応:
- その秘密を 即座にローテーション(漏えいした前提で)
- インシデント記録
- リポジトリのアクセスログ確認
- 検知のため GitGuardian や
git-secrets導入予防策:
.gitignoreに.env*を入れる + pre-commit hook で検査。
.gitignore は「過去には遡らない」
.gitignoreに.envを追加しても、既に追跡済みの.envは追跡され続ける。追跡解除:
git rm --cached .env git commit -m "remove .env from tracking"ただしこれでも 過去のコミットには残る。本当に漏えいしたなら必ずローテーション。
4. Dockerfile に ENV でシークレットを焼くな
# ----- NG -----
FROM golang:1.23
ENV STRIPE_API_KEY=sk_live_51A2B3C4D... # NG
ENV DATABASE_URL=mysql://... # NGアンチパターン: Dockerfile ENV にシークレット
なぜNGか:
- イメージレイヤーに 永久に書き込まれる
docker history myimageで全員見られる- イメージレジストリにあがれば、レジストリへのアクセス権を持つ全員に晒される
- レイヤーキャッシュとして他人のビルドキャッシュに混入する可能性
正解: シークレットは 実行時に注入 する。
services: api: image: myapi:1.0 environment: STRIPE_API_KEY: ${STRIPE_API_KEY} # 起動時に注入ビルド時にだけ必要なシークレット(プライベートリポジトリの認証情報など)は BuildKit Secrets で:
# syntax=docker/dockerfile:1.7 RUN --mount=type=secret,id=github_token \ GITHUB_TOKEN=$(cat /run/secrets/github_token) go build ...これならビルド成果物にトークンは残らない。
セッション②: シークレット投入の正しいやり方(25-30分)
5. 環境変数で渡すリスク
# あるユーザーが ps で見れる
ps eww
# PID TTY STAT TIME COMMAND
# 123 ? Sl 0:00 ./api STRIPE_API_KEY=sk_live_xxx DATABASE_URL=...
# /proc から覗ける
cat /proc/123/environ | tr '\0' '\n'
# STRIPE_API_KEY=sk_live_xxx
# DATABASE_URL=mysql://...環境変数経由のシークレットは漏えい経路がある
「環境変数なら安全」ではない。次のルートで漏れる可能性がある:
- プロセス一覧: 一部の OS / 設定では
ps -eで見える/proc/<PID>/environ: 同じユーザーのプロセスから読める- エラーログでの全環境変数 dump: フレームワークがクラッシュ時に env を吐く実装がある(Rails の
RAILS_ENV=productionでも本番事故が起きる)- コアダンプ: クラッシュ時のメモリダンプに残る
docker inspect <container>: コンテナの env がすべて表示される- CI ログ: ビルドログにうっかり出力
対策の方向
- 環境変数より ファイル経由(
/run/secrets/...を読む)の方が安全- 環境変数を使うなら、エラーログの環境変数フィルタを必ず入れる
- シークレットは短期トークン化してローテーション
6. Docker Secrets
services:
api:
image: myapi:1.0
secrets:
- db_password
- stripe_key
environment:
# シークレットを指すファイル名を渡す
DATABASE_PASSWORD_FILE: /run/secrets/db_password
STRIPE_KEY_FILE: /run/secrets/stripe_key
secrets:
db_password:
file: ./secrets/db_password.txt
stripe_key:
file: ./secrets/stripe_key.txt// アプリ側で _FILE 経由で読み込む
func mustLoadSecret(envKey string) string {
if path := os.Getenv(envKey + "_FILE"); path != "" {
b, err := os.ReadFile(path)
if err != nil {
log.Fatalf("read secret: %v", err)
}
return strings.TrimSpace(string(b))
}
return os.Getenv(envKey) // フォールバック
}
dbPass := mustLoadSecret("DATABASE_PASSWORD")Docker Secrets の本質
シークレットを ファイルとして コンテナ内の
/run/secrets/にマウントする仕組み。環境変数経由より安全な理由:
- プロセス環境変数に出てこない →
ps、env dumpで漏れないtmpfs(メモリ上のファイルシステム)にマウントされる → ディスクに残らない- ファイル権限を絞れる → 他ユーザーから読めない
歴史的経緯: もともと Docker Swarm のための機能。Swarm モードでないと暗号化された秘密管理にはならない。素の Compose(v2)で書いても、内部的には bind mount に近い扱い。
厳密なシークレット管理が必要なら Kubernetes Secrets + 外部 Secret 管理(External Secrets Operator)か AWS Secrets Manager 直接連携。
_FILEパターンは公式イメージで広く採用MySQL、PostgreSQL、Redis、Elasticsearch などの公式イメージは
<VAR>_FILE環境変数で値の場所を指定する パターンに対応している。services: db: image: mysql:8.0 environment: MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password secrets: - db_root_password secrets: db_root_password: file: ./secrets/db_root_password.txtこれで
MYSQL_ROOT_PASSWORDを環境変数として渡さなくて済む。エントリーポイントスクリプトがファイルから読んで使ってくれる。
7. 本番のシークレット管理
開発 ステージング 本番
───────── ──────────── ────────
.env ファイル CI Secrets Secrets Manager
Vault
KMS
本番のシークレット管理の選択肢
サービス 提供元 特徴 AWS Secrets Manager AWS 自動ローテーション、IAM 連携 AWS Systems Manager Parameter Store AWS 安価、KMS で暗号化 GCP Secret Manager GCP 自動レプリケーション Azure Key Vault Azure HSM 対応 HashiCorp Vault OSS / HCP クラウド非依存、動的シークレット Doppler SaaS 多環境一元管理 UI 1Password / Bitwarden Secrets Manager SaaS 開発者体験重視 基本パターン: シークレットマネージャから起動時に取得 → アプリのメモリに保持 → 定期的にリフレッシュ。
AWS なら IAM Role でアクセス → コードにキーすら持たせない。
GitHub Actions Secrets
CI/CD でシークレットを扱う最も身近な仕組み。
# .github/workflows/deploy.yml jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Deploy env: DATABASE_URL: ${{ secrets.DATABASE_URL }} STRIPE_KEY: ${{ secrets.STRIPE_KEY }} run: ./scripts/deploy.sh
- GitHub のリポジトリ設定 > Secrets で登録
- workflow ログには
***でマスクされる(ベストエフォート)- 環境ごとに「Environment Secrets」を分けられる(staging / production)
注意: secrets は workflow の
echoやcatで出力すると そのままログに残る(マスクされない場合もある)。echo $SECRETでデバッグするのは絶対 NG。
8. シークレットローテーション
ローテーション戦略
「シークレットは漏えいする前提」で運用する。1度漏れても被害が最小限になるように。
ローテーションの自動化
- AWS Secrets Manager: Lambda で自動ローテーション(RDS のパスワード等)
- HashiCorp Vault: 動的シークレット(DB に都度ユーザーを作成)
- GitHub Actions: 定期的に
gh secret setを走らせるアプリの対応
- シークレットの変更を検知して再読込(SIGHUP で reload する Unix の流儀)
- 短命トークンを使う(OAuth2 の access token + refresh token)
- 起動時だけでなく 稼働中も 最新の値を取りに行く設計
9. 実例: 開発と本番でシークレットを切り替える
ディレクトリ:
~/learn/infra/day11/
├── compose.yml # 共通
├── compose.override.yml # 開発用(git管理)
├── compose.prod.yml # 本番用(git管理)
├── .env.example # 雛形(git管理)
├── .env # 開発の実値(gitignore)
├── secrets/ # 開発のシークレット(gitignore)
│ ├── db_password.txt
│ └── stripe_key.txt
└── .gitignore
.gitignore:
.env
.env.local
.env.*.local
secrets/
*.pem
*.key.env.example:
COMPOSE_PROJECT_NAME=myapp_dev
LOG_LEVEL=debug
DATABASE_URL=app:CHANGE_ME@tcp(db:3306)/app
compose.yml:
services:
api:
build: ./api
environment:
LOG_LEVEL: ${LOG_LEVEL:-info}
DATABASE_PASSWORD_FILE: /run/secrets/db_password
STRIPE_KEY_FILE: /run/secrets/stripe_key
secrets:
- db_password
- stripe_key
depends_on:
db:
condition: service_healthy
db:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD_FILE: /run/secrets/db_root_password
MYSQL_DATABASE: app
MYSQL_USER: app
MYSQL_PASSWORD_FILE: /run/secrets/db_password
volumes:
- db_data:/var/lib/mysql
secrets:
- db_password
- db_root_password
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-p$$(cat /run/secrets/db_root_password)"]
interval: 5s
retries: 10
start_period: 20s
volumes:
db_data:
secrets:
db_password:
file: ./secrets/db_password.txt
db_root_password:
file: ./secrets/db_root_password.txt
stripe_key:
file: ./secrets/stripe_key.txt本番では secrets: の file: を外部のシークレットマネージャから生成したファイルパスに差し替える(CI が aws secretsmanager get-secret-value で取ってきて、ローカルファイルに書く → compose に渡す → 終了後に削除)。
この構成のポイント
- シークレットは 環境変数として直接渡さない(
_FILEで示す)- 開発はローカルファイル、本番は CI 経由でシークレットマネージャから取得
.envには機密値を入れない(非機密設定のみ).gitignoreでsecrets/ディレクトリごと弾く
練習課題
mkdir -p ~/learn/infra/day11/secrets
cd ~/learn/infra/day11
script ~/log/infra_day11.log
# .gitignore を設定
cat > .gitignore <<'EOF'
.env
.env.local
secrets/
EOF.env.exampleを作成し、必要な変数を列挙cp .env.example .envでローカル設定 → 中身を埋めるsecrets/db_password.txtなどを作成し、chmod 600 secrets/*で権限を絞る- compose を組み立て、
docker compose up -dで起動 docker compose exec api cat /run/secrets/db_passwordで読めることを確認docker compose exec api envで DATABASE_PASSWORD が環境変数に出ていない ことを確認git statusでsecrets/と.envが untracked のままなことを確認exitで script 終了
考察課題
- チームメンバーが新しくジョインしたとき、シークレットをどうやって安全に共有する?(メールは NG、Slack DM は議論の余地あり、1Password でチーム共有等)
- 退職者が出たとき、その人がアクセスしていたシークレットをどう扱う?
締め: git で証跡を残す
cd ~/learn/infra/day11
git init -q 2>/dev/null || true
git add .gitignore .env.example compose.yml api/
# .env と secrets/ は .gitignore で除外されるので git add しても無視される(ことを確認)
git status # untracked に .env / secrets/ が出ていないことを確認
git commit -m "feat(infra): シークレットを Docker Secrets で投入する構成"チェックリスト
- 12 Factor App の Config 原則を口頭で説明できる
-
environmentとenv_fileを使い分けられる -
.envと.env.exampleのセット運用ができる -
.gitignoreで.envを確実に弾ける - Dockerfile に ENV でシークレットを焼くと何が起きるか説明できる
- 環境変数経由のシークレットの漏えい経路を3つ以上挙げられる
- Docker Secrets /
_FILEパターンで投入できる - 本番でのシークレット管理選択肢(Secrets Manager / Vault / etc.)を知っている
詰まった時のチートシート
| やりたいこと | 方法 |
|---|---|
| 開発の設定をローカル分離 | .env を作る(自動で読まれる) |
| 環境ごとに分ける | --env-file production.env |
| シェル変数で上書き | DATABASE_URL=xxx docker compose up |
| .env を git から除外 | .gitignore に .env* を追加 |
| ENV を確認 | docker compose exec <svc> env |
| シークレットファイルを確認 | docker compose exec <svc> ls /run/secrets/ |
| 設定を YAML に展開して確認 | docker compose config |
やらかし事例: シークレット漏えいの定番
事例1:
.envを git に commit
.gitignoreに書き忘れて commit → push。履歴に永遠に残る。git filter-repoで削除しても他のクローンに残る可能性大。ローテーション必須。
事例2: Dockerfile に
ENV PASSWORD=xxxイメージレジストリに push →
docker historyで誰でも見える。
事例3:
docker inspectで素通り
environmentで渡したパスワードはdocker inspect <container>で平文で出る。プロセス一覧ps -efにも漏れることがある。Secrets を使う。
事例4: ログにシークレットがダンプ
アプリの起動時に
log.Printf("env: %v", os.Environ())でデバッグ → 本番ログにシークレット流出。ログ整形時にマスクする習慣。
事例5: ローテーション無し運用
5年前の DB パスワードがそのまま、退職者の AWS キーが活きている。年1回のローテーション + 退職時の即時失効。
対比表: シークレット投入方式の選択
| 方式 | セキュリティ | 用途 | 注意点 |
|---|---|---|---|
Dockerfile ENV | × 最悪 | 使ってはいけない | イメージに焼き込まれる |
environment: | △ | 開発のみ | inspect/env dump で漏れる |
env_file: | △ | 開発の .env | gitignore 必須 |
| Docker Secrets | ○ | 本番(Swarm/Compose) | tmpfs マウントで安全 |
_FILE パターン | ○ | 本番 | アプリ実装が必要 |
| Vault / Secrets Manager | ◎ | 本番、マルチクラウド | 運用コスト |
| BuildKit secret mount | ○ | ビルド時のみ | レイヤーに残らない |
「実務OK」基準
- シークレットを git に入れない癖が体に染み付く:
git add .envを手が打たない - アプリ起動時にシークレットを動的に取得する設計ができる:
_FILEパターン、Vault 連携 - CI/CD でシークレットを安全に流せる: GitHub Actions Secrets、AWS Secrets Manager から取得
- 漏えい時の対応が言える: 即ローテーション、インシデント記録、原因分析
自己評価チェックリスト
知識レベル
- 12 Factor App の III. Config を語れる
-
environmentと Docker Secrets の差を理解した -
_FILEパターンの目的を即答できる - 漏えい時の対応フロー(ローテ・通知・原因分析)が言える
実行レベル
-
.envを.gitignoreで確実に弾く設定をした - Docker Secrets で平文を排した compose を書いた
- git-secrets / GitGuardian を組み込んだ
メタ認知
- 自分のリポジトリ履歴に過去のシークレットが残っていないかチェックした
- チームのシークレット運用ルールを言語化した
さらに深掘るなら
- 12 Factor App: III. Config - 公式日本語訳
- Docker docs: Secrets in Compose - 公式 secrets リファレンス
- BuildKit secret mounts - ビルド時シークレット
- AWS Secrets Manager User Guide - 本番運用の鉄板
- HashiCorp Vault - クラウド非依存のシークレット管理
- ツール: git-secrets - commit 前にシークレットを検知
- ツール: GitGuardian - リポジトリ全体のシークレットスキャン
- 書籍: 「Securing DevOps」(Julien Vehent) - シークレット管理の章が秀逸
次のレッスン
2-5_ヘルスチェック.md: HEALTHCHECK、/healthz vs /readyz、restart policy、Kubernetes の readiness/liveness probe の元祖を学ぶ。