kubectl apply -f dir/ が本番Secretを毎回上書きする事故パターン

CI/CD パイプラインの監査で「本番 Secret が毎回デプロイ時にリセットされている」と指摘された。原因は deploy/k8s/ ディレクトリに置いていた secret.example.yaml だった。これ、自分も同じミスをやらかしたことがある。

何が起きていたか

K3s クラスタへのデプロイに、こういう構成を使っていた。

deploy/k8s/
├── configmap.yaml
├── deployment.yaml
├── ingress.yaml
├── postgres-statefulset.yaml
├── scheduler-deployment.yaml
├── secret.example.yaml    # ← これが問題
└── service.yaml

CI/CD の deploy ジョブはこう書いてある。

# .gitlab-ci.yml (deploy stage)
k8s-deploy:
  stage: deploy
  script:
    - kubectl apply -f deploy/k8s/

kubectl apply -f <directory>/ はディレクトリ内の .yaml / .yml / .json ファイルを 全て 適用する。secret.example.yaml も例外ではない。

補足: --recursive-R)フラグを付けると サブディレクトリも再帰的に 適用される。deploy/examples/deploy/k8s/ の下に置いた場合、kubectl apply -Rf deploy/k8s/ で同じ問題が再発するため、今回の対策のようにディレクトリ自体を分離することが重要。

問題のファイルの中身はこれだった。

# deploy/k8s/secret.example.yaml
apiVersion: v1
kind: Secret
metadata:
  namespace: tkdev
  name: sheaf-task-server-secrets
type: Opaque
stringData:
  SHEAF_DATABASE_URL: postgresql+psycopg://sheaf:[REDACTED_PASSWORD]@sheaf-task-server-postgres:5432/sheaf
  POSTGRES_PASSWORD: [REDACTED_PASSWORD]

注意: `[REDACTED_PASSWORD]` のようなプレースホルダー文字列が実際にクラスタへ書き込まれると、アプリケーションはその文字列をパスワードとして使おうとして認証エラーになる。エラーログにプレースホルダー文字列が出力される場合、ログ監視ツールが「正常な文字列」として見逃すリスクもある。

name: sheaf-task-server-secrets が本番の Secret と同じ名前なので、デプロイのたびにパスワードが [REDACTED_PASSWORD] に戻る。Secret を手動で設定しても、次の CI パイプラインで上書きされる。

なぜ気づきにくいか

3つの要因が重なっている。

1. apply は成功する

kubectl apply は Secret の上書きをエラーにしない。出力は secret/sheaf-task-server-secrets configured と表示されるだけで、パイプラインは正常終了する。

2. Pod はすぐには落ちない

PostgreSQL Pod は既存のコネクションを使い続ける。Secret が変わっても、Pod が再起動するまで影響が出ない。問題が表面化するのは Pod の再起動やレプリカ数の変更のタイミングで、デプロイから時間差がある。

3. ファイル名が "example" なので見逃す

.example が付いていれば適用されないだろうと思い込んでいた。kubectl はファイル名の慣習を解釈しない。拡張子が .yaml なら中身を適用する。

対策

example ファイルを deploy/k8s/ の外に移動する。

mkdir -p deploy/examples
git mv deploy/k8s/secret.example.yaml deploy/examples/secret.example.yaml

変更後のディレクトリ構成:

deploy/
├── examples/
│   └── secret.example.yaml   # CI の apply 対象外
└── k8s/
    ├── configmap.yaml
    ├── deployment.yaml
    ├── ingress.yaml
    ├── postgres-statefulset.yaml
    ├── scheduler-deployment.yaml
    └── service.yaml

kubectl apply -f deploy/k8s/ のコマンドは変更不要。Secret がディレクトリから消えたので、apply されなくなる。

本番の Secret は kubectl create secret で手動設定するか、External Secrets Operator 等の外部シークレット管理ツールに任せる。

別の選択肢として Kustomize を使う方法もある。kustomization.yaml に適用対象ファイルを明示的に列挙(resources: フィールド)することで、ディレクトリ内の全ファイルを盲目的に適用する問題を根本から回避できる。kubectl apply -k deploy/k8s/ はそのリストに含まれないファイルを無視する。

同じパターンを防ぐルール

deploy/k8s/ ディレクトリに対して、CI で簡易チェックを入れることもできる。

# .gitlab-ci.yml
validate-manifests:
  stage: test
  script:
    - |
      if ls deploy/k8s/*example* deploy/k8s/*sample* 2>/dev/null; then
        echo "ERROR: example/sample files found in deploy/k8s/"
        echo "Move them to deploy/examples/ to prevent accidental apply"
        exit 1
      fi

あるいは .gitignore ではなく ディレクトリ設計のルール として「deploy/k8s/ に置くファイルは全て本番適用される」と明文化する。ツールの挙動に合わせて配置を決める——それが最もシンプルで確実な対策だ。