Workload Identityの実基盤への導入

本記事は リクルートライフスタイル Advent Calendar 2019 23日目の記事です。

CETチーム の龍野です。

我々のチームでは GKE (Google Kubernetes Engine) を利用した基盤開発が盛んに行われております。 Kubernetes は可用性の高さや複数アプリケーションの実行など、多くのメリットがあるのですが、一方で、 GKE 内で多くのサービスが走ることによって、個々の権限管理などが煩雑になってきます。

そこで、本記事では、 GKE 内のサービスでの権限管理をより楽に、堅牢に行うことのできる Workload Identity という仕組みについて紹介します。チーム内での利用事例や、実際に Workload Identity を導入する上での注意点などを中心にお話できればと思っております。19/12/23 時点で、この機能はベータ版なので、GA した際と仕様が異なる恐れがあります。そこだけ注意して読んでいただければと思います。

想定読者

  • Kubernetes について多少の理解がある人
  • GKE でサービスを運用していて、鍵管理など煩雑だなと感じている人
  • Workload Identity について存在は知っているけどなかなか導入に踏み出せない人

背景

まずはじめに、そもそもの背景から。
GKE で何らかサービスを作る際に、GCP の他のサービスを使いたいという場面は数多く存在します。

例えば、GKE の中のサービスから

  1. Cloud Pub/Sub を叩きたい
  2. GCS からファイルをダウンロードしたい

などなど、挙げればキリがありません。このような場合、GKE 内の Workload が上記の操作を実行します。

このような、GKE 内のサービスが GKE 外の GCP のサービスと連携する際には今までは2つの方法がありました。

  1. サービスアカウントのキーをダウンロードしてきて使用

    • pros: サービスごとに権限を制御できる。
    • cons: キーを JSON 形式で発行し、必要な Workload ごとにコピーする必要があるため管理がしづらい。また、ダウンロードしたキーの有効期間はデフォルトで10年に設定されており、更新しないと失効する (参考)。
  2. GKE の裏側にある GCE インスタンスのサービスアカウントをそのまま利用する

    • pros: 一括で権限付与するため管理が楽
    • cons: node 内の Workload 全てに同じ権限が付与される

基本的に、1つの GKE で複数マイクロサービスを運用したい場合、全てに共通の権限を持たせるのはさすがに過剰なので、1の選択肢をとることが多いと思います(我々のチームでもこちらを採用しています)。

しかし、問題は cons に書かれている点です。

まず、キーを JSON 形式で発行し、必要な Workload ごとにコピーする必要がある点について。この際は、 gsutil やら secrets やらで頑張ってキーを Kubernetes から見える場所に配置し、そのキーを参照して各 Workload が GCP との認証を行うかと思います。しかし、キーを発行・コピーするというのはそれだけで漏洩のリスクもあったり、一手間増える分作業者側も面倒だったりします。

さらに、ただでさえその Service account にどの権限が紐づいているのかというのを管理するのですら一手間なのに、サービスアカウントの使われ先を完全に管理するのはなかなかに困難です。特に、common-batch みたいな名前の Service account が一度出回ったりすると、もう追いかけるのは大変です。それっぽいところで鍵が使われ、本来の用途は分からなくなり、とりあえずこの鍵つけとけば大丈夫…みたいな風潮になり。

そして担当者が変わり、時が移ろい10年経つと鍵が失効します。失効すると当然 GCP とのやり取りができなくなるため、そこの部分のサービスが崩壊します。そして「本番環境でやらかしちゃった人 Advent Calendar 2029」に記事が投稿されます。10年後にどこで鍵が使われているかを把握して、それを全て更新しないといけないというのに気づいたところで、本番で動いているサービスでその作業を安全に行うのは相当の苦労がかかるだろうと推察されます(特に10年持ったサービスならなおさら)。

ということでまとめましょう。

今までの方法で、GKE 内で GCP Service account の鍵を管理するのには以下の大きな問題があります。

  1. Workload ごとに GCP の権限を絞ろうとすると、鍵のコピーが必須
  2. 鍵の漏洩リスクが生じる上に、どこでどの鍵が使われているのかを適切に把握するのが困難
  3. 10年後に鍵が失効するため、それを担当者が把握し、更新しないといけない

Workload Identity

前項で説明したこうした問題を解決するために、 Cloud Next ‘19 で発表された機能が Workload Identity です。

Workload Identity とは一言で言うと

「Kubernetes の Service account と GCP の Service account を紐づけられるようにする機能」

です。

これでしっくり来る人も来ない人もいると思うので、もう少し詳しく説明していきます。

2種類の Service account の対応

なんか Service account が二つ出現して紛らわしくなってきましたね。便宜上、以後は Kubernetes Service account を KSA、GCP Service account を GSA と略します。

そもそも、 Service account とは何かというと、一般的には User account と区別され、アプリケーションや VM に紐づくアカウントのことです。 GSA は GCP 内の各サービスの利用の権限、KSA は Kubernetes 内での疎通などの権限をそれぞれ持っています。そのため、各 Workload は KSA と GSA を個別に指定して認証を行う必要があります。

authentication_before

しかしよくよく考えてみると、KSA も GSA も2つともアプリケーションが利用する点では共通しており、これらの Service account の対応がとれていれば、わざわざ鍵を発行・コピーする必要はないのではないかというのが Workload Identity の発想の元です。

これらを図で表現するとこのようになります。

authentication_after

こうして見てみると、Kubernetes からは GSA を通じて、GCP の各サービス が一つのリソースのようにみることができます。

このような、Kubernetes と GCP の世界観の統合というのが、 Workload Identity のもたらすもっとも大きな意味なのかなと思います。

Workload Identity の仕組み

上述した通り、Workload Identity は GSA と KSA を紐づけるものでした。それでは具体的にどのようにして紐づけているのでしょう。

簡単に言うと、GSA と KSA それぞれでお互いを紐づけるような処理をします。

まず、KSA 側からはアノテーションを行い、どの GSA と紐づけるかを指定します。具体的にはこのようになります。パラメータは $VALUE のように記載することとします。

1
2
3
4
5
6
7
apiVersion: v1
kind: ServiceAccount
metadata:
  name: $KSA_NAME
  namespace: $K8S_NAMESPACE
  annotations:
    iam.gke.io/gcp-service-account: "$GSA_NAME@$PROJECT_ID.iam.gserviceaccount.com"

一方、GSA からは権限の付与という形で紐づけを行います。 gcloud コマンドでの紐付けの例がこちらになります。

1
2
3
4
$ gcloud iam service-accounts add-iam-policy-binding \
    --role roles/iam.workloadIdentityUser \
    --member "serviceAccount:$PROJECT_ID.svc.id.goog[$K8S_NAMESPACE/$KSA_NAME]" \
    "$GSA_NAME@$PROJECT_ID.iam.gserviceaccount.com"

少し変わった書き方に感じるかもしれませんが、これは KSA が各 namespace 内でユニークなため、$K8S_NAMESPACE/$KSA_NAME とすることで、一意に定まります。

こうすることで、 KSA と GSA 間でお互いに識別できるようになり、Workload Identity が有効化されます。

これを図で表すとこのようになります。

workload_identity

図を見ると分かるように、1つの GSA に対して複数 KSA が紐づいています。このように、GSA と KSA は1:Nの関係性となっています。

Workload Identity がもたらすもの

Workload Identity 導入によって、前述した課題はどのように解決されるのでしょうか。

大きく2つのメリットがあります。

  1. 鍵の発行ないし、コピーが不要
  2. GSA に紐づく KSA が IAM Role Binding により紐づくので、それぞれの KSA が権限一覧として取得可能

鍵の発行・コピーが不要になることで、漏洩リスクや鍵のローテーションの必要がなくなります。加えて、GSA に紐づく KSA が一覧取得できることにより、権限が GSA を元に一元管理することができ、管理者が KSA それぞれの権限を格段に把握しやすくなります。

表立って、機能がX倍になったという大きな変化がみられるものではありませんが、インフラを管理する視点からいうと格段に権限管理の利便性が向上するのではないかと思われます。

実基盤での導入

さて、それでは実基盤での Workload Identity の導入の実際のフローについて解説していきます。この時、基本的に複数サービスが既に稼働している GKE 上での移行を考えます。

Workload Indentity 導入時の環境

我々のチームでは GCP のリソース管理には Terraform を利用しているので、Terraform を前提とした説明を行います。

GCP 公式ドキュメントは基本的に gcloud コマンドでの方法を書いておりますが、Terraform を使うことでより Infrastructre as Code にもなり全体のインフラ管理が行いやすくなるので、非常にオススメです。

今回の導入を行なった環境は以下のとおりです。

  • GKE version: 1.12.10-gke.17
  • Terraform: 0.12.16

Workload Identity 導入手順概要

概要自体はこちらを読むと大変分かりやすいです。

まず最初に、Workload Identity を利用するためにはクラスタで Workload Identity を有効化する必要があります。

この有効化の方法は3つあります。

  1. 新規クラスタ作成時に Workload Identity を有効化する方法
  2. 既存のクラスタの node を Workload Identity を有効化するようにアップデートする方法
  3. 既存のクラスタ内で Workload Identity を有効化した node を作成し、pod を全てそちらに移す方法

基本的にこれからクラスタを立てる場合は1で良いはずで、移行の場合は他のnamespaceに影響を与えないように3を実施するのがベターと思われます。node poolのアップデート中は該当ノードが停止するので、無停止で実施したいなら3一択のように思われます(3ならいざという時の切り戻しも可能です)。

では、3を選んだ上で、実際の移行フローを見てみましょう。

実際の移行手順はこのようになります。

  1. クラスタでWorkload Identity を有効化
  2. Workload Identity を有効化したノードプールを作成
  3. KSA の作成とアノテーションの付与
  4. 新しいノードプールへの移行と KSA の付与
  5. GSA に KSA を Binding
  6. Workload Identity が適切に設定できたかの紐付けチェック
  7. GSA を参照している部分を削除
  8. 旧ノードプールの削除

1, 2, 8がクラスタごとに必要な作業で、 3-7が Workload ごとに必要な手順となります。こうしてみると長く感じるかもしれませんが、やっていることの本体( KSA と GSA の紐付け)は3, 4, 5 だけとなります。

しかし、実作業ではこの辺のちゃんと移行できるかの検証もしないと危険なので、もう少し詳細の手順が必要になります。 具体的には、ちゃんと Binding ができているかどうかの確認、及び最後に今までの GSA 認証フローを削除する部分です。

頑張ってみていきましょう。

1. クラスタでWorkload Identityを有効化

$PROJECT_ID.svc.id.goog という名前を identity_namespace に設定する必要があります。下記のように、tf ファイルの container cluster 内でworkload_identity_config を指定すればOKです。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
data "google_project" "current" {}
resource "google_container_cluster" "sample_cluster" {
  // (途中略)
  workload_identity_config {
    identity_namespace = local.gke_identity_namespace
  }
}
locals {
 gke_identity_namespace = "${data.google_project.project_id}.svc.id.goog"
}

これを適用させてクラスタをアップデートしましょう。クラスタのアップデートは master のアップデートなので、サービス自体は無停止で更新することが可能です。

2. Workload Identityを有効化したノードプールを作成

続いて、新しい node pool を作ります。node pool 作成時はこのように node_metadata"GKE_METADATA_SERVER" を代入します。

1
2
3
4
5
6
7
8
9
10
11
resource "google_container_node_pool" "sample_node_pool" {
  // (途中略)
  node_config {
    workload_metadata_config {
      node_metadata = "GKE_METADATA_SERVER"
    }
  }
}

3. KSAの作成とアノテーションの付与

ここから、Workload ごとの話になります。KSA にアノテーションを付与します。もし、アノテーションをつける KSA が存在しなかった場合( default を使っていた場合)は、この際に KSA も作成してしまいましょう。 

1
2
3
4
5
6
7
apiVersion: v1
kind: ServiceAccount
metadata:
  name: $KSA_NAME
  namespace: $K8S_NAMESPACE
  annotations:
    iam.gke.io/gcp-service-account: "$GSA_NAME@$PROJECT_ID.iam.gserviceaccount.com"

4. 新しいノードプールへの移行と KSA の付与

それぞれの Workload をまず新しい node に振り分けられるように変更します。Workload Identity を有効化した node pool は既存の node pool と差分がないはずなので問題なく動くはずです。

それぞれの Deployment ファイルに下記のような nodeSelector を追記します。この時、Deployment ファイルに KSA が指定されていない場合はまとめて追加してしまいましょう。簡単に書くと以下のようになります。

1
2
3
4
5
6
7
8
9
10
apiVersion: apps/v1
kind: Deployment
# (途中略)
spec:
  template:
    spec:
      serviceAccountName: $KSA_NAME
      # (途中略)
      nodeSelector:
        iam.gke.io/gke-metadata-server-enabled: "true" # <- trueのダブルクォーテーションがないと失敗するので注意

もし、これを一つ一つやるのが面倒であれば、元の node をすべて cordon してから順に drain していけば一気に全ての pod が新しい node pool に移ります。しかし、我々の場合は、Workload Identity がまだベータ版というのと、クラスタに乗っているサービス数も少なくなかったので、確実に一つ一つを新しい node に移していくという選択をしました。

3と4自体は場合によっては一緒にやってしまっても良いかもしれません。

5. GSA に KSA を Binding

まず、./modules/workload_identity_iam/ 配下に Workload Identity 用の module を作ります。 main.tf として以下のようなファイルを作成します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
data "google_project" "project" {
}
locals {
  annotations = formatlist(
    "serviceAccount:${data.google_project.project.project_id}.svc.id.goog[%s]",
    var.k8s_service_accounts,
  )
}
data "google_service_account" "serviceaccount" {
  account_id = var.gcp_service_account_id
}
resource "google_service_account_iam_binding" "wi_iam_binding" {
  service_account_id = data.google_service_account.serviceaccount.name
  role               = "roles/iam.workloadIdentityUser"
  members            = local.annotations
}

GSA と KSA が1:Nの関係なので上記のような書き方になっております。

また、同じ階層に変数用の variables.tf を配置します。

1
2
3
4
5
6
7
8
9
10
11
variable "gcp_service_account_id" {
  type        = string
  default     = ""
  description = "GCP service account ID (before `@`)"
}
variable "k8s_service_accounts" {
  type        = list(string)
  default     = []
  description = "Kubernetes service accounts in the format of `$ns/$sa_name`"
}

最後に ルート module から 上記で作った module を呼び出して使用します。

1
2
3
4
5
6
7
8
9
module "workload_identity_sample" {
  source = "./modules/workload_identity_iam"
  gcp_service_account_id = "$GSA_NAME"
  k8s_service_accounts = [
    "$K8S_NAMESPACE/$KSA_NAME",
  ]
}

適用させたら下記のコマンドで実際に Binding ができているかどうかを確認することができます。

1
$ gcloud iam service-accounts get-iam-policy $GSA_NAME@$PROJECT_ID.iam.gserviceaccount.com

このように Binding が表示されていればOKです。

1
2
3
4
5
6
bindings:
- members:
  - serviceAccount:$PROJECT_ID.svc.id.goog[$K8S_NAMESPACE/$KSA_NAME]
  role: roles/iam.workloadIdentityUser
etag: XXXXX
version: 1

6. Workload Identity が適切に設定できたかの紐付けチェック

ここまでの手順がうまくいってれば、namespace 内で KSA を指定したサービスでは GSA の権限がそのまま使えるようになります。

では実際に、新しく立てた pod で GSA 認証ができているかをチェックしましょう。ここでは、namespace 内に一時的に gcloud の入った pod を立てて、Binding した GSA が pod 内部から正しく認識されているかを確認します。

1
2
3
4
5
6
7
$ kubectl run \
  --generator run-pod/v1 \
  --namespace $K8S_NAMESPACE \
  --serviceaccount $KSA_NAME \
  --image google/cloud-sdk:alpine \
  --overrides '{"spec":{"nodeSelector":{"iam.gke.io/gke-metadata-server-enabled":"true"}}}' \
  --rm -it wi-test

実際にこの pod が作成されたら gcloud コマンドを打ってみて、紐づけた GSA が表示されていれば問題ありません。

1
$ gcloud auth list
1
2
3
4
5
6
Credentialed Accounts
ACTIVE  ACCOUNT
*       $GSA_NAME@$PROJECT_ID.iam.gserviceaccount.com
To set the active account, run:
    $ gcloud config set account `ACCOUNT`

もし、適切に GSA 設定ができていない場合は下記のように表示されます。

1
2
3
4
5
6
Credentialed Accounts
ACTIVE  ACCOUNT
*       $PROJECT_ID.svc.id.goog
To set the active account, run:
    $ gcloud config set account `ACCOUNT`

こちらが表示された場合は、紐付けがどこかおかしいので、今までの手順を見直してみましょう。

7. GSA を参照している部分を削除

一番ドキドキの瞬間です。ちゃんと6までが設定されていれば、ここで GSA を削除しても正しく動作するはずです。

適切に設定されていないと即障害になってしまうので、紐付けチェックを正しく行い、dev 環境などで削除時の動作を試してから本番に適用させましょう。

8. 旧ノードプールの削除

7までを全ての workload で実施した後は実質古い node pool を参照する resource がいなくなります。そのため、移行が全て終わった後はこれらの node pool を削除しておきましょう。

お疲れ様でした 🎉🎉🎉

Workload Identity導入後のメリデメ

で、 Workload Identity 導入してどうよ実際…というところですが

個々のサービス的には

  • いちいち鍵を発行・コピーする必要がなくなった
  • 副次的だが、チームで運用している Airflow などではジョブ毎に鍵の認証をする手順がなくなり、実行時間が早くなった

といったメリットがありました。

また、組織的には

  • 上述した common-batch のような汎用権限が徐々に移り変わっている(これはまだ完了までは行ってないですが…)
  • GSA がどの GKE でどんな用途で利用されているか明確になった

というのも良い点だと思います。

一方で、現在だと

  • KSA と GSA を紐づけるのに KSA の manifest ファイルと GSA 用の tf ファイルというように設定がいろいろなところに点在している
  • 新しい GSA を作成して Workload に紐づける際に上記3-5の手順を再度踏まないといけないため、やや煩雑になっている

といったデメリットはあります。この辺の話も、一括で権限を管理する方法など検討中ですが、まだまだ今後の課題としては考えられるかと思います。

まとめ

今回は、GKE での権限管理を楽にする Workload Identity と、実際の導入例・手順について紹介いたしました。個人的にはこの Workload Identity は、マイクロサービスの浸透などにより Service account などの管理が大変になっていく中、今後、GKE を使っていく上では必須になっていくのではないかと思われます。この記事をみて、Workload Identity を導入する一つのきっかけや参考にでもなれば幸いです。

弊社、特に CET チームではアプリケーションエンジニア・インフラエンジニア・機械学習エンジニア等々、Kubernetes を使った基盤を開発し、ビジネスを加速していく仲間を絶賛募集しています。

ご興味をお持ちいただけましたら、お気軽に @sh_tatsuno までお声がけいただければと思います!