KubernetesへのHashiCorp Vaultデプロイ:動的シークレットとSidecar Injector実践ガイド

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

クイックスタート(5分):Sidecar Injector付きVaultの立ち上げ

Base64でエンコードしただけの静的なデータベースパスワードをKubernetes Secretに保存していると、情報漏洩のリスクが高まり、監査が複雑化するだけでなく、手動ローテーションの失念による深夜の障害発生を招きかねません。HashiCorp Vaultはこの課題を根本から解決します。Vaultは中央ブローカーとして機能し、必要に応じて短命でローテーション可能なトークンをマイクロサービスへオンデマンドで配布します。公式Helmチャートを利用すれば、ローカル開発クラスターを5分足らずで立ち上げることができます。

まず、HashiCorpのリポジトリを登録し、Agent Injectorとともに開発モードのVaultインスタンスをデプロイします。

# HashiCorpのHelmリポジトリを追加
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update

# Agent Injectorを有効にしてVaultをインストール
helm install vault hashicorp/vault \
  --namespace vault \
  --create-namespace \
  --set "server.dev.enabled=true" \
  --set "injector.enabled=true"

両方のPodがRunning状態になっていることを確認します。

kubectl get pods -n vault

vault-0(1/1)およびvault-agent-injector-*(1/1)が稼働していることを確認してください。開発モードでは、Vaultは初期rootトークンrootで自動的にアンシール(unseal)された状態で起動します。インメモリで高速に動作し、すぐに検証を始められます。

徹底解説:Kubernetes認証と動的シークレットエンジン

静的な共有パスワードをアプリケーションコンテナに直接配置すべきではありません。その代わりに、実行時にKubernetesによって自動的にマウントされる短命なJSON Web Token(JWT)をPodが提示します。Vaultはこのトークンを読み取り、KubernetesのTokenReview APIを呼び出してPodのNamespaceとServiceAccountを検証した上で、そのIDをVaultポリシーにマッピングし、有効期限付きのリースを返却します。

実際にVaultのKubernetes認証バックエンドを設定し、一時的なPostgreSQLデータベースロールを連携させてみましょう。

1. VaultでのKubernetes認証の設定

Vaultコンテナ内で対話型シェルを開きます。

kubectl exec -it vault-0 -n vault -- /bin/sh

認証エンジンを有効化し、ローカルクラスターのAPIを指定します。

# Kubernetes認証を有効化
vault auth enable kubernetes

# VaultがKubernetes APIと通信できるように設定
vault write auth/kubernetes/config \
    kubernetes_host="https://kubernetes.default.svc.cluster.local:443"

2. ポリシーとロールの作成

まずは最小権限のポリシーから作成します。以下のスニペットは、database/creds/app-role配下の一時的な認証情報に対する読み取り専用アクセス権をPodに付与します。

vault policy write app-policy - <<EOF
path "database/creds/app-role" {
  capabilities = ["read"]
}
EOF

次に、そのポリシーをdefault Namespace内のpayment-service ServiceAccountに紐付けます。

vault write auth/kubernetes/role/payment-role \
    bound_service_account_names=payment-service \
    bound_service_account_namespaces=default \
    policies=app-policy \
    ttl=1h

3. 動的データベースシークレットエンジンの有効化

動的シークレットを利用することで、アプリケーション間で認証情報が共有されることはなくなります。Vaultは管理者権限でPostgreSQLに接続し、v-token-paym-abc123-1718000000のような一意のユーザー名をオンザフライでプロビジョニングします。60分のTTL(有効期間)が切れると、Vaultはそのデータベースロールを自動的に破棄します。

# データベースシークレットエンジンをマウント
vault secrets enable database

# PostgreSQLの接続設定
vault write database/config/my-postgres \
    plugin_name=postgresql-database-plugin \
    allowed_roles="app-role" \
    connection_url="postgresql://{{username}}:{{password}}@postgres.database.svc:5432/orders?sslmode=disable" \
    username="vaultadmin" \
    password="AdminSecretPasswordHere"

# 一時データベースユーザーを作成するロールを定義
vault write database/roles/app-role \
    db_name=my-postgres \
    creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
        GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
    default_ttl="1h" \
    max_ttl="24h"

応用編:Vault Agent Sidecar Injectorの実践活用

Vault SDKを組み込むために既存アプリケーションのコードを書き直すのは現実的ではない場合がほとんどです。Vault Agent Injectorはこの問題をスマートに解決します。KubernetesのMutating WebhookがPodの作成を監視し、initコンテナと軽量なバックグラウンドサイドカーを自動注入して、/vault/secretsに共有インメモリボリュームをマウントします。サイドカーが認証を処理して認証情報を取得し、期限切れになる前にバックグラウンドで静かに更新します。

サイドカー用アノテーションを使用したアプリケーションのデプロイ

以下は、アノテーション内にConsul Template構文を設定した、本番環境に対応可能なDeploymentマニフェストです。

apiVersion: v1
kind: ServiceAccount
metadata:
  name: payment-service
  namespace: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: payment-service
  namespace: default
  labels:
    app: payment
spec:
  replicas: 2
  selector:
    matchLabels:
      app: payment
  template:
    metadata:
      labels:
        app: payment
      annotations:
        vault.hashicorp.com/agent-inject: "true"
        vault.hashicorp.com/role: "payment-role"
        vault.hashicorp.com/agent-inject-secret-database-creds.txt: "database/creds/app-role"
        vault.hashicorp.com/agent-inject-template-database-creds.txt: |
          {{- with secret "database/creds/app-role" -}}
          DB_USER="{{ .Data.username }}"
          DB_PASSWORD="{{ .Data.password }}"
          {{- end -}}
    spec:
      serviceAccountName: payment-service
      containers:
      - name: payment-api
        image: alpine:3.19
        command: ["/bin/sh", "-c"]
        args:
          - "while true; do if [ -f /vault/secrets/database-creds.txt ]; then cat /vault/secrets/database-creds.txt; fi; sleep 10; done"
        resources:
          limits:
            memory: "128Mi"
            cpu: "100m"

ここでは起動順序が重要になります。まずvault-agent-initコンテナが実行され、database-creds.txtが生成されるまでメインコンテナの起動をブロックします。Podが立ち上がると、次にバックグラウンドでvault-agentコンテナが動作し、60分の有効期限のおよそ3分の2(約40分経過時)のタイミングでリースを自動更新します。

実践的なヒント:運用フェーズ(Day-2)とトラブルシューティング

概念実証(PoC)から本番環境への移行には、見落としがちなエッジケースが伴います。運用にあたっては以下のポイントに留意してください。

  • コネクションプールのローテーション対策: HikariCPやGORMなどの多くのORMは、開いた接続を無期限に保持し続けます。Vaultが期限切れのデータベース認証情報を削除すると、プールされたアクティブな接続が認証エラーでクラッシュする可能性があります。ファイルの変更を検知してシークレットを再読み込みするようアプリを実装するか、vault.hashicorp.com/agent-inject-commandアノテーションを追加して更新時にプロセスへSIGHUPシグナルを送信するように設定してください。
  • Mutating Webhookのネットワーク確認: 新しいPodがPending状態のままスタックしたり、受付(Admission)処理中にクラッシュしたりする場合は、Injector Podのポート8080または8443を確認してください。ファイアウォールやクラウドのセキュリティグループによって、APIサーバーからワーカーノードのオーバーレイネットワークへの直接トラフィックがブロックされていることがよくあります。
  • JWTトークンの安全な検証: Kubernetes認証のデバッグでは、/var/run/secrets/kubernetes.io/serviceaccount/tokenに配置されたプロジェクションServiceAccountトークンのデコードが必要になることがよくあります。稼働中のトークンをサードパーティのクラウドツールに貼り付けてはいけません。代わりにToolCraftのJWT Decoderを使用してください。ブラウザ上の完全なクライアントサイドでトークンが処理されるため、クラスターのクレームや署名が外部に漏れることなく安全にデバッグできます。
  • 適用前のテンプレート構文バリデーション: YAMLの複数行ブロック内のインデントミスは、Consul Templateのパーサーエラーを簡単に引き起こします。Gitへプッシュする前に、ToolCraft JSON Formatterで中間ペイロードをサニタイズするか、ToolCraft YAML to JSON Converterを使用してマニフェストを検証してください。
  • 本番ストレージの堅牢化: 本番環境で開発モードを絶対に使用しないでください。3台または5台などの奇数ノード構成で統合Raftストレージ(Integrated Raft Storage)を用いた高可用性(HA)モードでVaultを稼働させます。クラウドのAuto-unseal(AWS KMS、GCP Cloud KMS、またはAzure Key Vault)を設定し、チームのロールが確立されたら初期rootトークンを速やかに失効させてください。
Share: