Oops!!

プログラミングとかのラフなブログ

Terrafromを使ってGitLab CI/CD + Cloud Runへのデプロイ環境を構築

Terrafromを使ってGitLab CI/CD + Cloud Runへのデプロイ環境を構築

前回の 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にすると以下のような警告みたいなものが出るので、ここは適時「公開アクセスを許可する」にしておけばいいと思います。

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
プロフィール画像

すずき ゆうた

愛知県でフリーランスのフロントエンド・エンジニアをしています。Reactを用いた開発が得意です。 他にもプロジェクトマネジメントや組織マネジメントも行ってきました。エビデンスのない事でも自分の経験から書いていくので話半分くらいでお願いします。