PLAID Engineer Blog

PLAID Engineer Blog


KARTEを提供する株式会社プレイドのエンジニアブログです。プレイドのエンジニアのユニークなパーソナリティを知ってもらうため、エンジニアメンバーたちが各々執筆しています。

PLAID Engineer Blog

悩みに悩んだ Kubernetes Secrets の管理方法、External Secrets を選んだ理由

Kosuke OyaKosuke Oya

こんにちは、今年の4月に新卒で入社し、Kernel チーム[1]でエンジニアとして働いている大矢です。
Kernel チームでは、CXプラットフォーム「KARTE」[2]のデプロイシステムの改善を行ってきました。今回の記事では、Kubernetes の Secret 管理方法について比較検討したことや、External Secrets[3] に移行する際に注意したポイントなどについてお話しします。

1. 今までの Secret 管理

GitOps[4] を実現する際には、Secret の管理方法を考える必要があります。Kubernetes の Secret リソースは base64 でエンコードされているだけなので、そのまま Git リポジトリにコミットするべきではありません。直接 Secret のマニフェストを Git リポジトリにコミットすることなく Secret を利用する方法はいくつかあります。

1-1. Kubesec[5]

私たちが最初に利用していたのは、Kubesec というツールです。Kubesec は、Google Cloud KMS や Amazon KMS を利用して Secret の暗号化を行うことが可能です。ファイル全体を暗号化するのではなく、Secret の構造を保ったまま値だけ暗号化するため、可読性に優れています。

以前の私たちの、Secret に関するデプロイの流れは下記の通りでした。

  1. Google Cloud KMS を利用し、下記のようなコマンドを実行します。
    $ kubesec encrypt -i --key=gcp:<resource-id of Google Cloud KMS key> secret.yml --cleartext
    
  2. 下記のコマンドで secret.yml を復号します。
    $ kubesec -i decrypt secret.yml
    
  3. kustomize build を実行して manifest を生成し、kubectl apply を実行してデプロイします。

1 で作成された secret.yml を Git リポジトリにコミットすると、2、3 の手順が cloudbuild で実行されるという流れでした。

しかし、デプロイの仕組みが変わるタイミングで Kubesec は適さなくなりました[6]
Kubernetes の manifest を管理する Git リポジトリのディレクトリ構成は下記のようになっていました。

- generated/evaluation
- generated/production
- manifests/template

manifests/template には Kustomize を用いて生成される manifest ファイルの生成元が置かれています。また、generated ディレクトリの配下には、それぞれ環境ごとに、kustomize build を実行して生成された manifest ファイルがコミットされます。つまり、kustomize build をローカルで実行し、生成物をコミットするので、kustomize build を実行する前に復号を実行する必要のある Kubesec は、私たちのデプロイシステムには適しません。

1-2. Sealed Secrets[7]

次に私たちは、 Sealed Secrets を使ってみました。
Sealed Secrets は、公開鍵暗号方式を利用した Secret の暗号化の仕組みです。Secret を公開鍵で暗号化し、SealedSecret[8] リソースとして Git リポジトリにコミットすることができます。SealedSecret リソースは、クラスタに登録された際にSealed Secrets のコントローラによってクラスタの内部で復号化され、Secret リソースが作成されます。

image.png (97.1 kB) https://engineering.bitnami.com/articles/sealed-secrets.html より抜粋

これで、デプロイ時には SealedSecret の manifest を apply するだけで良くなったので、生成物をコミットする仕組みでもうまくいきました。

しかし、開発が進むにつれ、複数の Kubernetes クラスタに対して、同じマニフェストのテンプレートを利用したいケースが出てきました。例えばKARTE では、通常の管理画面用のクラスタと、固定 IP でジョブを実行するためのクラスタの、2つのクラスタを管理していました。また、今後 GKE on AWS[9] を使ってマルチクラウド構成にすることを検討しており[10]、同一の manifest から複数環境のクラスタにデプロイできる必要がありました。
Sealed Secrets は、クラスタ毎に異なる master key が管理されているため、そのまま同じ manifest を複数のクラスタで共有することはできませんでした。

そのため私たちは、同じ Secret の manifest を複数クラスタで共有する様々な方法を検討しました。

Sealed Secrets を使い続けるという選択肢もありました。異なるクラスタで同じ master key を共有できれば、同じ manifest を使うことができます。しかし、Sealed Secrets は編集がしづらかったり[11]と、使いづらい部分もあり、別の仕組みに置き換えることを検討しました。

1-3. Berglas

Berglas[12] は、Google Cloud でシークレット[13]を保存、取得するためのツールです。アプリケーションから、 Berglas を使って Google Secret Manager で管理された秘匿情報を取得することができます。また、Kubernetes では Mutating Webhook [14] を使うことにより、Pod の環境変数として埋め込んで利用することもできます[15]
すでに Kubernetes で使用する Secret は、Pod の環境変数として参照して使うようになっていたため、使用方法としては後者の方が適しています。私たちは、Mutating Webhook を利用する方法で、Berglas が使えないか検討しました。
この仕組みの簡単な説明は、下記の通りです。

実際に Sealed Secrets を、Berglas を使った方法に置き換えるまではしませんでしたが、下記のような pros & cons があると感じました。

結局、私たちは External Secrets を使うことにしました。

2. 現在の Secret 管理(External Secrets)

External Secrets は、Google Secret Manager や AWS Secret Manager 等のクラウドサービスに登録した秘匿情報を用いて、Secret リソースを生成するツールです。私たちは GKE を使用していますので、秘匿情報管理には Google Secret Manager を使うことにしました[16]

image.png (125.2 kB) https://github.com/godaddy/kubernetes-external-secrets より抜粋

例えば、下記のような ExternalSecret[17] リソースの manifest を apply すると、External Secrets のコントローラーが Google Secret Manager から hoge-token という名前のシークレットを取得し、それをもとに Secret リソースを生成します。

apiVersion: kubernetes-client.io/v1
kind: ExternalSecret
metadata:
  name: hoge-secret
spec:
  backendType: gcpSecretsManager
  projectId: $PROJECT_ID
  data:
    - key: hoge-token
      version: '1'
      name: hoge-token

External Secretsでは複数のクラスタで同じ Secret を共有できます。なぜなら、Sealed Secrets と違い manifest に暗号化された情報が含まれないため、External Secrets のコントローラーがクラスタで動いていれば、同じ manifest を利用することができるためです。また、Sealed Secrets はレビューの際に実際の Secret の値を確認することが困難でしたが、External Secrets を使うことで、Google Secret Manager の管理画面で Secret の値を確認することが可能になりました。シークレットの編集は GCP Secret Manager の管理画面上で行いますが、ExternalSecret では version を指定できるため、変更されたことがマニフェスト側でも確認できます。
一方で、External Secrets のコントローラが、Google Secret Manager に管理された全ての Secret にアクセスする必要があるため、Pod ごとに Secret へのアクセス権限を設定するといったことはできません。

2-1. External Secrets のコントローラーから Secret Manager へのアクセス権限の付与

External Secrets のコントローラーに Secret Manager の特定のキーへのアクセス権限を付与する必要があります。まずは、Workload Identity を使って、External Secrets のコントローラーにサービスアカウントを紐付けます。そして、そのサービスアカウントに、Secret Manager の特定のキーへのアクセス権限を付与します。
権限の付与については、Terraform などの IaC[18] で管理することが望ましいです。しかし、Terraform を使って管理しようとすると、マニフェスト用のリポジトリで ExternalSecret を追加した後、Terraform 用のリポジトリでも作業をする必要があります。それはそれで面倒だと考え、私たちは下記のような冪等性のあるスクリプトを マニフェスト用のリポジトリで管理し、External Secrets を追加した際にはこれを実行するというようにしました。

# Args: (project, gcp_sa_name, secrets=[])
function bind_sa_to_secrets () {
  if [ -z ${3} ]; then return; fi 

  SECRETS=$3
  for SECRET in ${SECRETS[@]}; do
    bind_sa_to_secret $1 $2 $SECRET
  done

  return
}

# Args: (project, gcp_sa_name, secret)
function bind_sa_to_secret () {
  gcloud beta secrets add-iam-policy-binding --project ${1} \
    --role roles/secretmanager.secretAccessor \
    --member serviceAccount:${2}@${1}.iam.gserviceaccount.com \
    ${3}

  return
}

SECRETS=(
  HOGE_KEY
  HUGA_TOKEN
)

bind_sa_to_secrets ${PROJECT} ${GCP_SA} ${SECRETS[@]}

2-2. Secret の key の命名の問題

Sealed Secret の場合には、リソース名、Secret の data の key の組み合わせが kubernetes の namaspace で一意であれば良かったのですが、Google Secret Manager はプロジェクト毎にシークレットの key を一意にする必要があります。
例えば、下記のような サービス A、サービス B で利用したいシークレットがそれぞれあった時、

apiVersion: v1
kind: Secret
metadata:
  name: secret-A
data:
  FOO_PASSWORD: xxx
---
apiVersion: v1
kind: Secret
metadata:
  name: secret-B
data:
  BAR_PASSWORD: xxx

Google Secret Manager 上では下記のようになります(つまり、secret-A に関するシークレットか、secret-B に関するシークレットか、という情報は保持されない)。

FOO_PASSWORD: xxx
BAR_PASSWORD: xxx

このため、どのサービスでこのシークレットが使われるのかわかりづらい、という問題が起こります。また、サービスAで利用するために登録したシークレットを、サービスB、サービスCでも利用してしまうという事態は避けたいです。
この問題の解決策は、いくつか考えられます。例えば、シークレットのキーにサービス名の prefix をつけることで、どのサービスで使われているか一目でわかるようになります。

A_FOO_PASSWORD: xxx
B_BAR_PASSWORD: xxx

また、シークレットのキーの名前を変更せずとも、シークレットに対してラベルを付与することもできます。

FOO_PASSWORD: xxx [service:A]
BAR_PASSWORD: xxx [service:B]

どちらの方法も、開発チームの中で規則を作ることはできますが、システム的な制約が与えられるわけではないという点に注意が必要です。例えば、[service:A] というラベルが付与されているからと言って、サービスBから参照できないわけではありません。もし上記のような規則を考えるとしたら、OpenPolicyAgent を使った Conftest[19] や Gatekeeper などを利用してそれが保証されるような仕組み作りをすることも、同時に必要だと考えています。
結局、今のところシークレットの管理については特に規則はなく、今後検討していく予定です。

2-3. Controller の Health Check

External Secrets を使い始めてしばらくすると、External Secrets のコントローラが動いてないことに気づきました。Secret リソースがすでに作成されていれば、アプリケーションが停止してしまうなどの問題は起きませんでしたが、Controller が起動してないため、新しく ExternalSecret を作成した際に Secret が生成されません。調べてみると、下記のような issue がありました。
The operator hangs and stops polling/upserting secrets · Issue #362 · godaddy/kubernetes-external-secrets

頻度は約3週間に1度くらいと少なかったのですが、1度停止してしまうと自動で復旧されないため、気づいたときに Pod を手動で削除して再起動させていました。
そこで、sync_calls をチェックしてコントローラが動いているかどうかを確認する livenessProbe を設定しました。

        livenessProbe:
          exec:
            command:
            - /bin/sh
            - -c
            - wget -q http://localhost:3001/metrics -O - | grep -E "^sync_calls.*status=\"success\"" > /tmp/stat.new; diff /tmp/stat.old /tmp/stat.new > /dev/null; ECODE=$?; mv /tmp/stat.new /tmp/stat.old; if [ $ECODE -eq 0 ]; then exit 1; fi;
          timeoutSeconds: 5
          successThreshold: 1
          failureThreshold: 3
          periodSeconds: 30

livenessProbe を設定してからは、コントローラが動かなくなる問題は起きなくなりました。

まとめ

この記事では、様々な Kubernetes の Secret の管理方法を紹介し、私たちがどのような経緯でその管理方法を実践するに至ったかについて書きました。どれも Pros/Cons があると思いますので、それぞれのデプロイシステムに合った方法を選択するのが良いと思います。
また、External Secrets を導入する上で遭遇したいくつかの課題とその解決策について触れました。External Secrets は複数クラスタでの共有もできますし、レビューや編集も簡単です。しかし、コントローラが停止するなど扱いづらい部分もありますので注意が必要です。

謝辞

今回の External Secrets への移行をするにあたって、青山真也さん(@amsy810)にご協力いただきました。ここに感謝申し上げます。

最後に

CX(顧客体験)プラットフォーム「KARTE」を運営するプレイドでは、KARTEを使ってこんなアプリケーションが作りたい! KARTE自体の開発に興味がある!というエンジニア(インターンも!)を募集しています。
詳しくは弊社採用ページまたはWantedlyをご覧ください。 もしくはお気軽に、下記の「話を聞きに行きたい」ボタンを押してください!


  1. 新しいシステム基盤を開発していくチームのことです。チーム全体のパフォーマンスと、プロダクトのパフォーマンスの両面を上げていくことを目的としています。 ↩︎

  2. CX(顧客体験)プラットフォーム KARTE(カルテ) ↩︎

  3. GitHub - godaddy/kubernetes-external-secrets: Integrate external secret management systems with Kubernetes ↩︎

  4. GitOps what you need to know
    Git リポジトリで manifest を管理することが前提となっています。 ↩︎

  5. GitHub - shyiko/kubesec: Secure Secret management for Kubernetes (with gpg, Google Cloud KMS and AWS KMS backends) ↩︎

  6. デプロイシステムの変遷については詳しくはこちらのブログを読んでみてください。
    PLAID Enginner Blog - Our seeking to stable k8s deployment. ↩︎

  7. GitHub - bitnami-labs/sealed-secrets: A Kubernetes controller and tool for one-way encrypted Secrets ↩︎

  8. リソースの kind を指す場合は「SealedSecret」、そのほかも含めた管理方法全体のことを指す場合に「Sealed Secrets」として区別しています。 ↩︎

  9. GKE on AWS architecture | Anthos GKE on AWS | Google Cloud ↩︎

  10. PLAID のマルチクラウドへの変遷についてはこちらのブログをぜひ見てみてください(発表動画へのリンクあり)。
    PLAID Enginner Blog - Google Cloud Day: Digital で Anthos と ML について話してきました ↩︎

  11. SealedSecret を Secret に復号し、Secret の base64 を復号し、編集してから、暗号化し、さらに符号する必要があります。 ↩︎

  12. GitHub - GoogleCloudPlatform/berglas: A tool for managing secrets on Google Cloud ↩︎

  13. リソースの kind を指す場合は 「Secret」、その他の一般的な秘匿情報(DBのパスワードなど)を指す場合は「シークレット」として区別しています。 ↩︎

  14. Dynamic Admission Control | Kubernetes ↩︎

  15. GitHub - berglas/examples/kubernetes at main · GoogleCloudPlatform/berglas ↩︎

  16. 図は AWS Secret Manager ですが。 ↩︎

  17. リソースの kind を指す場合は「ExternalSecret」、そのほかも含めた管理方法全体のことを指す場合に「External Secrets」として区別しています. ↩︎

  18. Infrastructure as Code。インフラの構成をコードにしておくこと。 ↩︎

  19. GitHub - open-policy-agent/conftest ↩︎

Kosuke Oya
Author

Kosuke Oya

Comments