前回の Workload Identityを用いてGitLab CIからCloud Runへデプロイする という記事ではGoogle Cloudの構築をgcloudコマンドをShellにしていました。
今くらいシンプルだとあれでも良いですが、拡張性を考えるとIaCに寄せた方がいいと思うのでTerraformで書くことにしました。
プロジェクト構成
今回は勉強も兼ねているので最小限のプロジェクトにしました。
├── cloudbuild.yaml
├── Dockerfile
├── index.js
├── package-lock.json
├── package.json
├── README.md
└── terraform
├── main.tf
├── terraform.tfvars
└── variables.tf
1. Terraformのリソースの作成
仮にGoogle Cloud内にawesome-project-12345というプロジェクトを作り、リージョンはasia-northeast1で進めていきたいと思います。
main.tf
これがメイン実行ファイルになります。
terraform {
required_providers {
google = {
source = "hashicorp/google"
version = ">= 4.50.0"
}
google-beta = {
source = "hashicorp/google-beta"
version = ">= 4.50.0"
}
}
}
provider "google" {
project = var.project_id
region = var.region
}
provider "google-beta" {
project = var.project_id
region = var.region
}
# --- データソース: 既存リソースの情報を読み込む ---
data "google_project" "project" {}
data "google_compute_default_service_account" "default" {}
# --- ローカル変数: IAMロールのリストをここで一元管理する ---
locals {
gitlab_ci_sa_roles = toset([
"roles/cloudbuild.builds.builder",
"roles/iam.serviceAccountUser",
"roles/logging.viewer",
"roles/viewer",
"roles/storage.objectAdmin",
])
compute_sa_roles = toset([
"roles/storage.objectViewer",
"roles/artifactregistry.createOnPushRepoAdmin",
"roles/run.admin",
])
}
# --- 1. 必要なAPIを有効化 ---
resource "google_project_service" "apis" {
for_each = toset([
"run.googleapis.com",
"artifactregistry.googleapis.com",
"cloudbuild.googleapis.com",
"iamcredentials.googleapis.com",
"iam.googleapis.com",
])
service = each.key
disable_dependent_services = true
}
# --- 2. Artifact Registryリポジトリを作成 ---
resource "google_artifact_registry_repository" "repo" {
provider = google-beta
repository_id = var.artifact_repo_name
location = var.region
format = "DOCKER"
description = "Docker repository for ${var.service_name}"
depends_on = [google_project_service.apis]
}
# --- 3. Cloud Runサービスを作成 ---
resource "google_cloud_run_v2_service" "default" {
name = var.service_name
location = var.region
ingress = "INGRESS_TRAFFIC_ALL"
deletion_protection = false
template {
containers {
image = "us-docker.pkg.dev/cloudrun/container/hello"
}
}
lifecycle {
# CI/CDや手動での変更によって変更される可能性があるため、
# Terraformの管理対象から以下の属性を意図的に除外する。
ignore_changes = [
template, # コンテナイメージ、環境変数、Cloud SQL接続など
ingress, # 公開/非公開の認証設定
]
}
depends_on = [google_project_service.apis]
}
# 4b. Cloud Runサービスに誰でもアクセスできるようにIAMからallUsersに設定 ---
resource "google_cloud_run_v2_service_iam_member" "noauth" {
location = google_cloud_run_v2_service.default.location
name = google_cloud_run_v2_service.default.name
# サービスを外部公開するために、全ユーザにCloud Run起動元のIAMロールを付与
role = "roles/run.invoker"
member = "allUsers"
}
# --- 5. CI/CD用のリソース ---
# 5a. CI/CD用サービスアカウントを作成
resource "google_service_account" "ci_cd_sa" {
account_id = var.ci_cd_service_account_name
display_name = "GitLab CI/CD Service Account"
depends_on = [google_project_service.apis]
}
# 5b. Workload Identity プールとプロバイダを作成
resource "google_iam_workload_identity_pool" "gitlab_pool" {
workload_identity_pool_id = var.workload_pool_id
display_name = "GitLab Pool"
description = "Pool for GitLab CI/CD"
}
# 5b. Workload Identity プロバイダの設定
resource "google_iam_workload_identity_pool_provider" "gitlab_provider" {
workload_identity_pool_id = google_iam_workload_identity_pool.gitlab_pool.workload_identity_pool_id
workload_identity_pool_provider_id = var.workload_provider_id
display_name = "GitLab Provider"
description = "OIDC provider for gitlab.com"
attribute_condition = "assertion.namespace_path == '${var.gitlab_group_path}'"
attribute_mapping = {
"google.subject" = "assertion.sub",
"attribute.project_path" = "assertion.project_path"
"attribute.namespace_path" = "assertion.namespace_path"
}
oidc {
issuer_uri = "https://gitlab.com"
allowed_audiences = [
"projects/${data.google_project.project.number}/locations/global/workloadIdentityPools/${google_iam_workload_identity_pool.gitlab_pool.workload_identity_pool_id}/providers/${var.workload_provider_id}"
]
}
}
# --- 6. 権限の付与 ---
# 6a. CI/CD用サービスアカウントに必要な権限を付与
resource "google_project_iam_member" "ci_cd_sa_roles" {
for_each = local.gitlab_ci_sa_roles
project = var.project_id
role = each.key
member = "serviceAccount:${google_service_account.ci_cd_sa.email}"
}
# 6d. GitLabリポジトリとサービスアカウントを紐付け
resource "google_service_account_iam_member" "gitlab_wif_user" {
service_account_id = google_service_account.ci_cd_sa.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_pool.workload_identity_pool_id}/attribute.project_path/${var.gitlab_project_path}"
}
# 6c. Compute Engine Defaultサービスアカウントに必要な権限を付与 ---
resource "google_project_iam_member" "compute_sa_project_roles" {
for_each = local.compute_sa_roles
project = var.project_id
role = each.key
member = "serviceAccount:${data.google_compute_default_service_account.default.email}"
}
# 6d. Compute Engine Defaultサービスアカウントが自身として振る舞うことを許可
resource "google_service_account_iam_member" "compute_sa_act_as_self" {
service_account_id = data.google_compute_default_service_account.default.name
role = "roles/iam.serviceAccountUser"
member = "serviceAccount:${data.google_compute_default_service_account.default.email}"
}
# 6e. Cloud Buildサービスアカウントが自身として振る舞うことを許可
resource "google_service_account_iam_member" "cloudbuild_sa_act_as_run_sa" {
service_account_id = data.google_compute_default_service_account.default.name
role = "roles/iam.serviceAccountUser"
member = "serviceAccount:${data.google_project.project.number}@cloudbuild.gserviceaccount.com"
}
# --- 6. 出力 ---
output "cloud_run_url" {
value = google_cloud_run_v2_service.default.uri
}
output "project_id" {
value = var.project_id
}
output "artifact_repo_name" {
value = var.artifact_repo_name
}
output "service_name" {
value = var.service_name
}
Cloud Runの公開範囲をallUsersにすると以下のような警告みたいなものが出るので、ここは適時「公開アクセスを許可する」にしておけばいいと思います。

variables.tf
変数を定義します。後述するterraform.tfvarsに定義されなければdefaultの値が入ります。(厳密に言えばコマンドラインでの-varや-var-fileオプションがこれらを上書きする権限を持っています)
variable "project_id" {
type = string
description = "Google CloudのプロジェクトID"
}
variable "region" {
type = string
description = "リソースを作成するリージョン"
default = "asia-northeast1"
}
variable "service_name" {
type = string
description = "Cloud Runのサービス名"
default = "■■■■■" # 任意のサービス名
}
variable "artifact_repo_name" {
type = string
description = "Artifact Registryのリポジトリ名"
default = "■■■■■-repo" # 任意のリポジトリ名
}
variable "gitlab_project_path" {
type = string
description = "CI/CDを許可するGitLabリポジトリのパス"
}
variable "gitlab_group_path" {
type = string
description = "CI/CDを許可するGitLabグループのパス"
}
variable "workload_pool_id" {
type = string
description = "Workload Identity プールのID"
default = "gitlab-pool"
}
variable "workload_provider_id" {
type = string
description = "Workload Identity プロバイダのID"
default = "gitlab"
}
variable "ci_cd_service_account_name" {
type = string
description = "CI/CD用のサービスアカウント名"
default = "gitlab-ci-sa"
}
terraform.tfvars
# Google Cloud Project URL: https://console.cloud.google.com/welcome?project=awesome-project-12345
# GitLab Repository URL: https://gitlab.com/xxxxx/aaaaa
project_id = "awesome-project-12345"
gitlab_project_path = "xxxxx/aaaaa"
gitlab_group_path = "xxxxx"
コマンド
main.tfがあるディレクトリで以下のコマンドが実行できます。
# Dry Run
terraform plan
# 実行
terraform apply
Terraformのシンタックス
resource "google_project_service" "apis" {}
- resource: 定義と宣言。何を作成するのかを宣言します。上記についてはgoogle_project_serviceについてです。
- google_project_service: Terraform側が提供しているリソースを指定しています。
- apis: 識別名。同じリソースをしてする場合も多々あるので、任意の名前をつけて識別します。
depends_on = [google_project_service.apis]みたいな形で、apisの処理が終わったら実行するなどを定義できます。
その他のコード
cloudbuild.yamlや.gitlab-ci.ymlは今までとほとんど変わりませんが、自分がコピペすると時に便利なのでこちらにも記述しておきます。
cloudbuild.yaml
steps:
# 1. Dockerイメージをビルドする
- name: 'gcr.io/cloud-builders/docker'
id: 'Build'
args: [
'build',
'-t', '${_REGION}-docker.pkg.dev/$PROJECT_ID/${_ARTIFACT_REPO_NAME}/${_SERVICE_NAME}:${_IMAGE_TAG}',
'.'
]
# 2. ビルドしたイメージをArtifact Registryにプッシュする
- name: 'gcr.io/cloud-builders/docker'
id: 'Push'
args: [
'push',
'${_REGION}-docker.pkg.dev/$PROJECT_ID/${_ARTIFACT_REPO_NAME}/${_SERVICE_NAME}:${_IMAGE_TAG}'
]
wait_for: ['Build']
# 3. 新しいイメージをCloud Runにデプロイする
- name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
id: 'Deploy'
entrypoint: gcloud
args: [
'run', 'deploy', '${_SERVICE_NAME}',
'--image', '${_REGION}-docker.pkg.dev/$PROJECT_ID/${_ARTIFACT_REPO_NAME}/${_SERVICE_NAME}:${_IMAGE_TAG}',
'--region', '${_REGION}',
'--platform', 'managed'
]
wait_for: ['Push']
# ビルドしたイメージを記録
images:
- '${_REGION}-docker.pkg.dev/$PROJECT_ID/${_ARTIFACT_REPO_NAME}/${_SERVICE_NAME}:${_IMAGE_TAG}'
.gitlab-ci.yml
variables:
GCP_PROJECT_ID: "awesome-project-12345"
GCP_REGION: "asia-northeast1"
SERVICE_NAME: "■■■■■"
ARTIFACT_REPO_NAME: "■■■■■-repo"
# GCPのプロジェクト番号を取得するコマンドを実行して、その結果を設定してください。
# gcloud projects describe {project_id} --format='value(projectNumber)'
GCP_PROJECT_NUMBER: "xxxxxxxxxx"
GCP_WORKLOAD_IDENTITY_PROVIDER: "projects/${GCP_PROJECT_NUMBER}/locations/global/workloadIdentityPools/gitlab-pool/providers/gitlab"
GCP_SERVICE_ACCOUNT: "gitlab-ci-sa@${GCP_PROJECT_ID}.iam.gserviceaccount.com"
default:
image: google/cloud-sdk:latest
stages:
- build-and-deploy
deploy-to-cloud-run:
stage: build-and-deploy
id_tokens:
GITLAB_OIDC_TOKEN:
# ステップ1で更新したプロバイダ設定と完全に一致させる
aud: "$GCP_WORKLOAD_IDENTITY_PROVIDER"
script:
- echo "$GITLAB_OIDC_TOKEN" > /tmp/oidc_token.json
- gcloud iam workload-identity-pools create-cred-config "$GCP_WORKLOAD_IDENTITY_PROVIDER" --service-account="$GCP_SERVICE_ACCOUNT" --output-file=/tmp/gcp_cred.json --credential-source-file=/tmp/oidc_token.json
- gcloud auth login --cred-file=/tmp/gcp_cred.json --update-adc
- gcloud config set project $GCP_PROJECT_ID
- echo "Submitting build to Cloud Build..."
- >
gcloud builds submit . --config cloudbuild.yaml
--substitutions=_SERVICE_NAME=$SERVICE_NAME,_ARTIFACT_REPO_NAME=$ARTIFACT_REPO_NAME,_REGION=$GCP_REGION,_IMAGE_TAG=$CI_COMMIT_SHORT_SHA
rules:
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH


