オンプレKubernetesからGCPのWorkload Identity連携を使えるようにした話

## はじめに

前回は GCP のリソースを Terraform で管理できるようにしました。今回は、GCP のリソースを Kubernetes 上の Pod から、サービスアカウントキーを使わずに利用できるようにしていきます。

## 動機

GCP のリソースにアクセスする方法で、最もシンプルなのはサービスアカウントキーを発行する方法です。
しかし、サービスアカウントキーは次のようなデメリットがあります。

- 漏洩のリスクがある
- 有効期限切れになる前に再発行する必要があり、管理が大変

そこで、GCP の Workload Identity 連携を使って、Kubernetes 上の Pod から GCP のリソースにアクセスできるようにしようと思いました。

## 実装

### Kubernetes の情報を取得

Kubernetes に接続して、issuer URL を確認します。

```sh
kubectl get --raw /.well-known/openid-configuration | jq -r .issuer
```

クラスタの JSON Web Key Set (JWKS) を確認します。

```sh
kubectl get --raw /openid/v1/jwks > cluster-jwks.json
```

ダウンロードして、terraform プロジェクトのディレクトリに配置します。

### GCP の設定

基本的には[公式ドキュメント](https://cloud.google.com/iam/docs/workload-identity-federation-with-kubernetes?hl=ja)に従って設定します。

まず、まだ作成していない場合は Workload Identity Pool を作成します。

```tf
resource "google_iam_workload_identity_pool" "default" {
  workload_identity_pool_id = "pool"
}
```

先程ダウンロードした JWKS を terraform から使えるようにします。

```tf
data "local_file" "cluster_jwks" {
  filename = "${path.module}/cluster-jwks.json"
}
```

Kubernetes の Workload Identity Pool Provider を作成します。

```tf
resource "google_iam_workload_identity_pool_provider" "home_kubernetes" {
  workload_identity_pool_id          = google_iam_workload_identity_pool.default.workload_identity_pool_id.workload_identity_pool_id
  workload_identity_pool_provider_id = "home-kubernetes"
  display_name                       = "Home Kubernetes"
  description                        = "for Kubernetes in the home"
  attribute_mapping = {
    "google.subject" = "assertion.sub"
  }
  oidc {
    issuer_uri = "{issuer URL}"
    jwks_json  = data.local_file.cluster_jwks.content
  }
}
```

使用するサービスアカウントを作成します。

```tf
resource "google_service_account" "sa" {
  account_id                   = "sa"
  create_ignore_already_exists = true
}
```

与えたいロールを作成します。Workload Identity 連携をするためには、`roles/iam.workloadIdentityUser` が必要です。

```tf
resource "google_project_iam_member" "clubroom_storage_object_user" {
  project = var.project
  member  = "serviceAccount:${google_service_account.clubroom.email}"
  role    = "roles/storage.objectUser"
}
resource "google_project_iam_member" "clubroom_service_account_token_creator" {
  project = var.project
  member  = "serviceAccount:${google_service_account.clubroom.email}"
  role    = "roles/iam.workloadIdentityUser"
}
```

最後に、サービスアカウントを Kubernetes から使用できるようにします。

```
resource "google_service_account_iam_member" "ksa_workload_identity_user" {
  service_account_id = google_service_account.clubroom.id
  role               = "roles/iam.workloadIdentityUser"
  member             = "principal://iam.googleapis.com/projects/${var.project_number}/locations/global/workloadIdentityPools/${google_iam_workload_identity_pool.default.workload_identity_pool_id}/subject/system:serviceaccount:default:{ksa-name}"
}
```

### Kubernetes 側の設定

まず、次のような JSON を作成します。

```json
{
  "universe_domain": "googleapis.com",
  "type": "external_account",
  "audience": "//iam.googleapis.com/projects/{project_number}/locations/global/workloadIdentityPools/{pool_id}/providers/{provider_id}",
  "subject_token_type": "urn:ietf:params:oauth:token-type:jwt",
  "token_url": "https://sts.googleapis.com/v1/token",
  "credential_source": {
    "file": "/var/run/service-account/token",
    "format": {
      "type": "text"
    }
  },
  "service_account_impersonation_url": "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/{gsa_email}:generateAccessToken"
}
```

この JSON をもとに ConfigMap を作成します。

```yaml
# kustomization.yaml
- name: workload-identity-credential-configuration
  files:
    - gcp-credential-configuration.json
```

Deployment に以下の 2 つの Volume を作成します。

```yaml
volumes:
  - name: token
    projected:
      sources:
        - serviceAccountToken:
            audience: https://iam.googleapis.com/projects/{project_number}/locations/global/workloadIdentityPools/{pool_id}/providers/{provider_id}
            expirationSeconds: 3600
            path: token
  - name: workload-identity-credential-configuration
    configMap:
      name: workload-identity-credential-configuration
```

Volume をマウントします。

```yaml
volumeMounts:
  - name: token
    mountPath: /var/run/service-account
    readOnly: true
  - name: workload-identity-credential-configuration
    mountPath: /etc/workload-identity
    readOnly: true
```

環境変数で作成した設定へのパスを指定します。

```yaml
env:
  - name: GOOGLE_APPLICATION_CREDENTIALS
    value: /etc/workload-identity/gcp-credential-configuration.json
```

以上で、Kubernetes 上の Pod から GCP のリソースにアクセスできるようになります。

## まとめ

今回は、GCP のリソースにアクセスする方法として、Workload Identity 連携を使う方法を紹介しました。
サービスアカウントキーを発行せずに GCP を使えるようになって、かなり便利になったと感じています。

今後は Kubernetes 側の設定を自動でやってくれる Custom Controller なんかを作ってみたいなと感じています。