Closed9

Terraform で GitLab CI と Google Cloud の Workload Identity

9sako69sako6

やりたいこと

GItLab CI にて、

  • Firebase Emulator 上の Cloud Functions と BigQuery の疎通

現在、ローカル環境では Firebase Emulator を使用し、Emulator 上の Cloud Functions から本物の BigQuery へ読み書きしている。

ローカルで開発する分には Application Default Credentials (ADC) によって動作するので問題ないが、CI 上では BigQuery へ読み書きするために Workload Identity 連携によって権限を取得する必要があるので、これを実現したい。

参考資料

https://cloud.google.com/iam/docs/workload-identity-federation?hl=ja
https://docs.gitlab.com/ee/ci/cloud_services/google_cloud/
https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/iam_workload_identity_pool
https://www.youtube.com/watch?v=Psfy3dIa6w8

9sako69sako6

サービスアカウントへのなりすましとアクセストークンの発行

下記の設定でリソースを作るとアクセストークンの生成が CI 上でできるようになった。iamcredentials.googleapis.com を有効にしておく必要があります。

main.tf
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
9sako69sako6

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
9sako69sako6

(おまけ)アクセストークンを発行するスクリプト版。

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'
)"
9sako69sako6

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 の関数の環境変数には機密情報を含めない

https://firebase.google.com/support/guides/security-checklist?hl=ja#never_put_sensitive_information_in_a_cloud_function’s_environment_variables

9sako69sako6

サービスアカウントに BigQuery への読み書き権限を追加する必要もある。

9sako69sako6
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 も必要だった。

9sako69sako6

下記権限を足して、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}"
}

このスクラップは5ヶ月前にクローズされました