Terraform で GitLab CI と Google Cloud の Workload Identity
やりたいこと
GItLab CI にて、
- Firebase Emulator 上の Cloud Functions と BigQuery の疎通
現在、ローカル環境では Firebase Emulator を使用し、Emulator 上の Cloud Functions から本物の BigQuery へ読み書きしている。
ローカルで開発する分には Application Default Credentials (ADC) によって動作するので問題ないが、CI 上では BigQuery へ読み書きするために Workload Identity 連携によって権限を取得する必要があるので、これを実現したい。
参考資料
サービスアカウントへのなりすましとアクセストークンの発行
下記の設定でリソースを作るとアクセストークンの生成が CI 上でできるようになった。iamcredentials.googleapis.com
を有効にしておく必要があります。
terraform {
required_version = "~> 1.8.0"
required_providers {
google = {
source = "hashicorp/google"
version = "~> 5.27"
}
}
}
data "google_project" "project" {}
resource "google_project_service" "default" {
service = "iamcredentials.googleapis.com"
}
resource "google_iam_workload_identity_pool" "gitlab" {
workload_identity_pool_id = "gitlab"
display_name = "GitLab"
description = "Workload Identity Pool for GitLab"
disabled = false
project = data.google_project.project.project_id
}
resource "google_iam_workload_identity_pool_provider" "gitlab" {
workload_identity_pool_id = google_iam_workload_identity_pool.gitlab.workload_identity_pool_id
workload_identity_pool_provider_id = "gitlab-pipeline"
oidc {
issuer_uri = "https://gitlab.com/"
allowed_audiences = [
"https://gitlab.com"
]
}
# https://docs.gitlab.com/ee/integration/google_cloud_iam.html#oidc-custom-claims
attribute_mapping = {
"google.subject" = "assertion.sub",
"attribute.project_id" = "assertion.project_id",
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/143091
"attribute.user_access_level" = "assertion.user_access_level"
}
attribute_condition = <<-EOT
attribute.project_id == "${var.gitlab_project_id}" &&
attribute.user_access_level in ["owner", "maintainer", "developer"]
EOT
}
resource "google_service_account" "gitlab" {
account_id = "gitlab"
display_name = "GitLab CI/CD"
project = data.google_project.project.project_id
description = "Service Account for GitLab CI/CD"
}
resource "google_service_account_iam_member" "gitlab" {
service_account_id = google_service_account.gitlab.name
role = "roles/iam.workloadIdentityUser"
member = "principalSet://iam.googleapis.com/projects/${data.google_project.project.number}/locations/global/workloadIdentityPools/${google_iam_workload_identity_pool.gitlab.workload_identity_pool_id}/*"
}
gcp-auth:
stage: setup
image: google/cloud-sdk:slim
id_tokens:
GITLAB_OIDC_TOKEN:
aud: https://gitlab.com
script:
- echo ${GITLAB_OIDC_TOKEN} > .ci_job_jwt_file
- gcloud iam workload-identity-pools create-cred-config projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/gitlab/providers/gitlab-pipeline
--service-account=$SERVICE_ACCOUNT
--output-file=credentials.json
--credential-source-file=.ci_job_jwt_file
- gcloud auth login --cred-file=credentials.json
- gcloud auth list
- cat credentials.json
- gcloud auth print-access-token $SERVICE_ACCOUNT
developer 以上の権限があるユーザーが実行したジョブの場合のみアクセストークンを発行できるように条件を書いた。
GitLab のドキュメントには、attribute として developer_access
という developer 権限を少なくとも持っているかどうかのブーリアンがあると記されているが、JWT トークンに含まれていなくて諦めた。
ちなみに JWT トークンの確認方法ですが、JWT トークンを CI 上で echo
してもマスクして表示されるので、ファイルに書き出してアーティファクトとして出力するといいです。
id_tokens:
GITLAB_OIDC_TOKEN:
aud: https://gitlab.com
script:
- echo ${GITLAB_OIDC_TOKEN} > .ci_job_jwt_file
artifacts:
paths:
- .ci_job_jwt_file
expire_in: 15 mins
(おまけ)アクセストークンを発行するスクリプト版。
PAYLOAD="$(
cat <<EOF
{
"audience": "//iam.googleapis.com/projects/$PROJECT_NUMBER/locations/global/workloadIdentityPools/$POOL_ID/providers/$PROVIDER_ID",
"grantType": "urn:ietf:params:oauth:grant-type:token-exchange",
"requestedTokenType": "urn:ietf:params:oauth:token-type:access_token",
"scope": "https://www.googleapis.com/auth/cloud-platform",
"subjectTokenType": "urn:ietf:params:oauth:token-type:jwt",
"subjectToken": "$GITLAB_OIDC_TOKEN"
}
EOF
)"
FEDERATED_TOKEN="$(
curl --fail "https://sts.googleapis.com/v1/token" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--data "$PAYLOAD" |
jq -r '.access_token'
)"
ACCESS_TOKEN="$(
curl --fail "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/$SERVICE_ACCOUNT_EMAIL:generateAccessToken" \
--header "Accept: application/json" \
--header "Content-Type: application/json" \
--header "Authorization: Bearer $FEDERATED_TOKEN" \
--data '{"scope": ["https://www.googleapis.com/auth/cloud-platform"]}' |
jq -r '.accessToken'
)"
CI ジョブから BigQuery への接続
CI 上では Firebase Emulator を実行し、Cloud Functions は本物の BigQuery に接続しにいく。
そのために、gcloud iam workload-identity-pools create-cred-config
コマンドで credentials ファイルを生成する先ほどのジョブから、FIrebase Emulator をビルドするジョブへ credentials ファイルを渡す必要がある。
GitLab CI ではアーティファクト経由で渡すのがよさそう。アーティファクトの寿命は短めに、expire_in: 15 mins
にしている。Job の実行終了後に消せたら一番理想的なんだが。
Cloud Functions の関数の環境変数には機密情報を含めない
サービスアカウントに BigQuery への読み書き権限を追加する必要もある。
Error: The file at .ci_job_jwt_file does not exist, or it is not a file. ENOENT: no such file or directory, lstat '/app/backend/.ci_job_jwt_file'
たしかに、credentials.json が参照している .ci_job_jwt_file も必要だった。
ApiError: Access Denied: Project xxx: User does not have bigquery.jobs.create permission in project xxx.
job の作成権限も必要。roles/bigquery.jobUser
が妥当そう。
下記権限を足して、GitLab CI 上の Firebase Emulator から本物の BigQuery への読み書きに成功した。
resource "google_bigquery_dataset_iam_member" "gitlab_bigquery_data_editor" {
dataset_id = var.bigquery_dataset_id
role = "roles/bigquery.dataEditor"
member = "serviceAccount:${google_service_account.gitlab.email}"
}
resource "google_project_iam_member" "gitlab_bigquery_job_user" {
project = data.google_project.project.project_id
role = "roles/bigquery.jobUser"
member = "serviceAccount:${google_service_account.gitlab.email}"
}