🐈

Terraform で Google Cloud の CI/CD パイプラインを構築する入門ガイド

に公開

はじめに

こんにちは、クラウドエース株式会社 第三開発部の小薗です。

先日、実務で初めて Google Cloud の CI/CD パイプライン構築に携わる機会がありました。
一般的に CI/CD パイプラインの構築はコンソールから手動で行うことも多いですが、今回は環境の再現性を高めることと、インフラ構成をすべてコードで一元管理するメリットを重視し、Terraform で構築することにしました。

構築にあたってリサーチを行いましたが、多くの記事ではコンソールからの手動構築手順が紹介されており、Terraform での構築手順を網羅した情報は意外と少ないことに気づきました。
そこで本記事では、自身の備忘録も兼ねて、Terraform で CI/CD パイプラインを構築する手順について詳しく解説します。
「CI/CD パイプラインもコードで管理したいけれど、具体的な手順がわからない」という、同じような課題に取り組んでいる方の参考になれば幸いです。

本記事の対象読者と前提知識

対象読者

本記事は以下の方を対象としています。

  • Google Cloud の CI/CD パイプラインを Terraform で構築したい方
  • ローカル実行から Cloud Build トリガーによる自動デプロイへ移行したい方
  • Terraform の tfstate ファイルの適切な保管方法を知りたい方

前提知識

本記事では以下の知識を前提として解説を進めます。

  • Google Cloud の基本的なプロダクト知識(Cloud Build, Google Cloud Storage (GCS), Service Account など)
  • Terraform の基本知識(コードの読み書き、init/plan/apply コマンド、backend の概念)
  • GitHub の基本操作(リポジトリ管理、プルリクエストの作成・マージ)
  • CI/CD の基本概念(プルリクエストベースの開発フロー)

構築の全体像

このセクションでは、構築に入る前に全体像を把握していただくため、解説のポイント、構成要素、ディレクトリ構造について説明します。

解説のポイント

本記事では、手順だけでなく背景や理由を丁寧に解説しています。
特に 「構築の順番」「権限の移り変わり」 の 2 点に注目してください。

1. 構築の順番

リソース作成には明確な依存関係があります。
「なぜこのリソースを先に作るのか」「なぜこの時点でこの設定が必要なのか」といった背景を理解することで、ご自身の環境に応じた応用がしやすくなり、トラブル発生時にも原因を特定しやすくなります。

2. 権限の移り変わり

実行者と実行権限の変化を意識しながら読み進めてください。
以下の図で、構築時と完成後の違いを確認できます。

権限の移譲

  • 構築時:作業者個人の Google アカウントの権限でリソースを作成
    • ローカルから terraform apply を実行して GCS バケット、サービスアカウント、トリガーなどを作成
  • 完成後:CI/CD 専用サービスアカウントの権限でインフラを操作
    • GitHub でのプルリクエスト作成・マージ操作だけでインフラを更新
    • 作業者個人の強い権限は不要になる

この「権限の移譲」により、以下が実現されます。

  • 個人に強い権限を付与し続ける必要がない
  • 手動操作による誤操作を防止できる
  • 作業者の権限を削除しても CI/CD パイプラインは稼働し続ける
  • GitHub の操作ログが「いつ、誰が、何を変えたか」の証跡になる

完成後の具体的な運用フローについては、「完成後の運用」セクションで詳しく説明します。

構成要素

CI/CD パイプラインは、以下のリソースで構成されます。

  1. Google Cloud Storage (以下、GCS) バケット

    • tfstate 保存用バケット:tfstate ファイルを保存し、チーム開発を支援します
    • Cloud Build ログ保存用バケット:Cloud Build の実行ログを保存します
  2. Service Account(サービスアカウント)
    CI/CD 専用の実行権限を持つサービスアカウントです。CI/CD パイプライン完成後のインフラ操作は、このアカウントで実行されます。

  3. GitHub 接続

    • GitHub ホスト接続:GitHub と Google Cloud を連携させる基盤
    • リポジトリ接続:特定のリポジトリを Google Cloud に接続
  4. Cloud Build Trigger(自動化トリガー)

    • Plan トリガー:プルリクエスト作成時に terraform plan を実行
    • Apply トリガー:マージ時に terraform apply を実行

ディレクトリ構造

最終的なディレクトリ構造は以下のようになります。

.
├── src/
│   ├── cloudstorage/            # STEP 1: GCS バケット(tfstate 用・ログ用)
│   │   ├── backend.tf
│   │   ├── provider.tf
│   │   └── main.tf
│   ├── iam/                     # STEP 2: CI/CD 用サービスアカウント
│   │   ├── backend.tf
│   │   ├── provider.tf
│   │   └── main.tf
│   └── cicd/                    # STEP 4: リポジトリ接続とトリガー
│       ├── backend.tf
│       ├── provider.tf
│       └── main.tf
└── cloudbuild/                  # STEP 5: ビルド構成ファイル
    ├── plan.yaml                # PR 作成時の実行内容
    └── apply.yaml               # マージ時の実行内容

共通ファイル

以降の構築手順で各ディレクトリに配置する共通ファイルを紹介します。

各ディレクトリで使用する provider.tf の内容は共通です。

provider.tf(全ディレクトリ共通)

terraform {
  required_version = "1.13.3"
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "7.6.0"
    }
  }
}

provider "google" {
  project = "your-project-id"  # ご自身のプロジェクト ID に置き換えてください
  region  = "asia-northeast1"
}

backend.tf は各ディレクトリで prefix のみが異なります。

backend.tf の例

terraform {
  backend "gcs" {
    bucket = "your-project-tfstate"  # tfstate 保存用バケット名に置き換えてください
    prefix = "ディレクトリ名/state"  # cloudstorage/state, iam/state, cicd/state
  }
}

ビルド構成ファイル

Cloud Build で実行する内容を定義する YAML ファイルです。
詳細は「STEP 5:ビルド構成ファイルの作成」で説明します。

  • cloudbuild/plan.yaml: プルリクエスト作成時に実行(各ディレクトリで terraform plan を実行)
  • cloudbuild/apply.yaml: マージ時に実行(各ディレクトリで terraform apply を実行)

構築手順

ここからは、実際に CI/CD パイプラインを構築していきます。
Terraform のコードを記述し、ローカルからコマンドを実行して、必要なリソースを順番に作成していきます。

以下の 5 ステップで構築を進めます。

  • STEP 1:GCS バケットの作成と tfstate の移行
  • STEP 2:CI/CD 専用サービスアカウントの作成
  • STEP 3:GitHub ホスト接続の作成
  • STEP 4:リポジトリの接続設定とトリガーの作成
  • STEP 5:ビルド構成ファイルの作成

なお、STEP 3 の GitHub ホスト接続は、PAT(個人アクセストークン)の発行が必要となるため、Terraform ではなく Google Cloud コンソールから手動で作成します。
詳細な手順は STEP 3 で説明します。

STEP 1:GCS バケットの作成と tfstate の移行

STEP 1 では 2 段階で作業を行います。

Phase 1:GCS バケットの作成

  • backend.tf なしでローカルから terraform apply を実行して GCS バケットを作成

Phase 2:tfstate の移行

  • backend.tf を追加して terraform init でローカルの tfstate を GCS バケットへ移行

2 段階に分ける理由は、tfstate の保存先となる GCS バケットがまだ存在しないためです。
Terraform では通常 backend.tf で tfstate の保存先を指定しますが、保存先バケット自体を Terraform で作成する場合、先にバケットを作成してから backend.tf を追加する必要があります。

Phase 1:GCS バケットの作成

Phase 1 では、2種類の GCS バケットを作成します。

  • tfstate 保存用バケット:tfstate ファイルを保存
  • Cloud Build ログ保存用バケット:Cloud Build の実行ログを保存

以降のすべてのステップで作成するリソース(サービスアカウント、トリガーなど)の tfstate ファイルを tfstate 保存用バケットに保存するため、他のリソースよりも先に GCS バケットを作成する必要があります。

src/cloudstorage ディレクトリを作成し、以下のファイルを配置します。

src/cloudstorage/main.tf

# tfstate 保存用バケット
resource "google_storage_bucket" "tfstate_bucket" {
  name                     = "your-project-tfstate"
  location                 = "asia-northeast1"
  storage_class            = "STANDARD"
  public_access_prevention = "enforced"

  versioning {
    enabled = true
  }

  # 古いバージョンを自動削除してストレージコストを削減
  lifecycle_rule {
    action {
      type = "Delete"
    }
    condition {
      num_newer_versions = 10
    }
  }
}

# Cloud Build ログ保存用バケット
resource "google_storage_bucket" "cloudbuild_logs" {
  name                     = "your-project-cloudbuild-logs"
  location                 = "asia-northeast1"
  storage_class            = "STANDARD"
  public_access_prevention = "enforced"

  # 90 日経過したログを自動削除
  lifecycle_rule {
    action {
      type = "Delete"
    }
    condition {
      age = 90
    }
  }
}

src/cloudstorage ディレクトリ内で以下のコマンドを実行し、GCS バケットを作成します。

terraform init
terraform plan
terraform apply

この時点で、src/cloudstorage ディレクトリ内に terraform.tfstate ファイルが生成されます。
これはローカルに保存された tfstate ファイルです。

Phase 2:tfstate の移行

tfstate ファイルは、Terraform が管理するリソースの現在の状態を記録する重要なファイルです。
ローカルに保存したままでは、以下の問題があります。

  • チーム開発での状態共有が困難
  • 作業者個人の PC に依存してしまう
  • バックアップや履歴管理が難しい

GCS バケットに tfstate を保存することで、これらの問題を解決できます。
また、複数人が同時に操作する際の状態ロック機能や、バージョニングによる履歴管理も可能になります。

先ほど GCS バケットを作成した際、src/cloudstorage ディレクトリにローカルの tfstate ファイル(terraform.tfstate)が生成されました。
次に、このローカルの tfstate ファイルを、先ほど作成した GCS バケットへ移行する作業を行います。

まず、src/cloudstorage ディレクトリに backend.tf を追加します。

src/cloudstorage/backend.tf

terraform {
  backend "gcs" {
    bucket = "your-project-tfstate"
    prefix = "cloudstorage/state"
  }
}

backend.tf を配置した後、src/cloudstorage ディレクトリ内で以下のコマンドを実行し、ローカルの tfstate を GCS バケットへ移行します。

terraform init

すると、Terraform が以下のようなメッセージを表示します。

Initializing the backend...
Do you want to copy existing state to the new backend?
  Pre-existing state was found while migrating the previous "local" backend to the
  newly configured "gcs" backend. No existing state was found in the newly
  configured "gcs" backend. Do you want to copy this state to the new "gcs"
  backend? Enter "yes" to copy and "no" to start with an empty state.

  Enter a value:

これは、ローカルに保存されている既存の tfstate を新しいバックエンド(GCS)にコピーするかどうかを確認するメッセージです。
ここで yes と入力すると、ローカルの terraform.tfstate ファイルの内容が GCS バケットへコピーされ、以降は GCS バケットの tfstate が利用されるようになります。
移行が完了すると、ローカルの terraform.tfstate ファイルは不要になります。誤操作を防ぐため、削除することを推奨します。

STEP 2:CI/CD 専用サービスアカウントの作成

CI/CD パイプラインで使用するサービスアカウントを作成します。

STEP 1 で tfstate 保存用の GCS バケットを作成したため、このステップから backend.tf を最初から配置できます。
また、STEP 4 で作成する Cloud Build トリガーは、このサービスアカウントを実行権限として参照するため、トリガーを作成する前にサービスアカウントを作成する必要があります。

CI/CD パイプライン完成後、Cloud Build がインフラ操作を自動実行する際に、このサービスアカウントの権限を使用します。

src/iam ディレクトリを作成し、以下のファイルを配置します。

src/iam/main.tf

resource "google_service_account" "cloudbuild_sa" {
  account_id   = "cloudbuild-terraform-sa"
  display_name = "Cloud Build Terraform Service Account"
}

resource "google_project_iam_member" "cloudbuild_sa_admin" {
  project = "your-project-id"
  role    = "roles/admin"
  member  = "serviceAccount:${google_service_account.cloudbuild_sa.email}"
}

provider.tfbackend.tf も配置します。

src/iam/backend.tf

terraform {
  backend "gcs" {
    bucket = "your-project-tfstate"
    prefix = "iam/state"
  }
}

src/iam ディレクトリ内で以下のコマンドを実行し、CI/CD 専用サービスアカウントを作成します。

terraform init
terraform plan
terraform apply

STEP 3:GitHub ホスト接続の作成

Cloud Build と GitHub のホスト接続を作成します。

STEP 4 で作成する GitHub リポジトリ接続と Cloud Build トリガーは、このホスト接続を基盤として動作します。
そのため、リポジトリ接続やトリガーを作成する前に、まずホスト接続を設定する必要があります。

なお、Cloud Build のリポジトリには第 1 世代と第 2 世代があり、本記事では第 2 世代を使用します。第 2 世代では gcloud コマンドや Terraform での接続が可能になり、リポジトリ接続もコードで管理できます。
世代の違いについては、公式ドキュメントを参照してください。

手動作成の手順

  1. Cloud Build コンソールを開く
  2. 左メニューの [リポジトリ][第 2 世代] タブを選択
  3. [ホスト接続を作成] をクリック
  4. プロバイダで [GitHub] を選択
  5. リージョン名前(例: github-connection)を入力して [接続] をクリック
  6. 画面の指示に従って GitHub の認証を完了

次の STEP で使用するため、以下の情報を控えてください。

  • 接続名(例: github-connection
  • リージョン(例: asia-northeast1

STEP 4:リポジトリの接続設定とトリガーの作成

GitHub リポジトリの接続設定と Cloud Build トリガーを作成します。

ここでは、STEP 3 で作成したホスト接続を使ってリポジトリを接続し、STEP 2 で作成したサービスアカウントを実行権限として設定したトリガーを作成します。
以下の 2 つのトリガーで CI/CD を自動化します。

  • プルリクエスト作成時: terraform plan を実行
  • メインブランチへのマージ時: terraform apply を実行

src/cicd ディレクトリを作成し、以下のファイルを配置します。

src/cicd/main.tf

# サービスアカウントの参照
data "google_service_account" "cloudbuild_sa" {
  account_id = "cloudbuild-terraform-sa"
}

# リポジトリのリンク
resource "google_cloudbuildv2_repository" "terraform_repo" {
  location          = "asia-northeast1"
  name              = "terraform-repo"
  parent_connection = "github-connection"  # STEP 3 で作成した接続名
  remote_uri        = "https://github.com/your-org/your-repo.git"  # ご自身の GitHub 組織名・リポジトリ名に置き換えてください
}

# Plan トリガー(PR 作成時)
resource "google_cloudbuild_trigger" "terraform_plan" {
  location    = "asia-northeast1"
  name        = "terraform-plan"
  description = "Terraform Plan on Pull Request"

  repository_event_config {
    repository = google_cloudbuildv2_repository.terraform_repo.id
    pull_request {
      branch = "^main$"
    }
  }

  service_account = data.google_service_account.cloudbuild_sa.id

  filename = "cloudbuild/plan.yaml"
}

# Apply トリガー(マージ時)
resource "google_cloudbuild_trigger" "terraform_apply" {
  location    = "asia-northeast1"
  name        = "terraform-apply"
  description = "Terraform Apply on Merge to Main"

  repository_event_config {
    repository = google_cloudbuildv2_repository.terraform_repo.id
    push {
      branch = "^main$"
    }
  }

  service_account = data.google_service_account.cloudbuild_sa.id

  filename = "cloudbuild/apply.yaml"
}

provider.tfbackend.tf も配置します。

src/cicd/backend.tf

terraform {
  backend "gcs" {
    bucket = "your-project-tfstate"
    prefix = "cicd/state"
  }
}

src/cicd ディレクトリ内で以下のコマンドを実行し、リポジトリ接続とトリガーを作成します。

terraform init
terraform plan
terraform apply

STEP 5:ビルド構成ファイルの作成

CI/CD パイプライン構築の最後のステップとして、Cloud Build で実行する内容を定義する YAML ファイルを作成します。
このファイルで、プルリクエスト作成時とマージ時にどのような処理を自動実行するかを定義します。

plan.yaml の作成

プルリクエスト作成時に実行される内容を定義します。
各ディレクトリで terraform fmtterraform initterraform plan を実行します。

cloudbuild/plan.yaml を作成し、以下の内容を記述します。

cloudbuild/plan.yaml

timeout: "3600s"
logsBucket: "$_CLOUDBUILD_LOGBUCKET"

substitutions:
  _TERRAFORM_VERSION: "1.13.3"
  _CLOUDBUILD_LOGBUCKET: "gs://your-project-cloudbuild-logs"

steps:
  # ---------------------------------------------------------------------------
  # 1. GCS のplan
  # ---------------------------------------------------------------------------
  - id: "tf fmt for cloudstorage"
    name: "hashicorp/terraform:${_TERRAFORM_VERSION}"
    dir: "src/cloudstorage"
    args: ["fmt", "-check", "-diff"]

  - id: "tf init for cloudstorage"
    name: "hashicorp/terraform:${_TERRAFORM_VERSION}"
    dir: "src/cloudstorage"
    args: ["init"]

  - id: "tf plan for cloudstorage"
    name: "hashicorp/terraform:${_TERRAFORM_VERSION}"
    dir: "src/cloudstorage"
    args: ["plan"]

  # ---------------------------------------------------------------------------
  # 2. IAM のplan
  # ---------------------------------------------------------------------------
  - id: "tf fmt for iam"
    name: "hashicorp/terraform:${_TERRAFORM_VERSION}"
    dir: "src/iam"
    args: ["fmt", "-check", "-diff"]

  - id: "tf init for iam"
    name: "hashicorp/terraform:${_TERRAFORM_VERSION}"
    dir: "src/iam"
    args: ["init"]

  - id: "tf plan for iam"
    name: "hashicorp/terraform:${_TERRAFORM_VERSION}"
    dir: "src/iam"
    args: ["plan"]

  # ---------------------------------------------------------------------------
  # 3. CICD のplan
  # ---------------------------------------------------------------------------
  - id: "tf fmt for cicd"
    name: "hashicorp/terraform:${_TERRAFORM_VERSION}"
    dir: "src/cicd"
    args: ["fmt", "-check", "-diff"]

  - id: "tf init for cicd"
    name: "hashicorp/terraform:${_TERRAFORM_VERSION}"
    dir: "src/cicd"
    args: ["init"]

  - id: "tf plan for cicd"
    name: "hashicorp/terraform:${_TERRAFORM_VERSION}"
    dir: "src/cicd"
    args: ["plan"]

apply.yaml の作成

マージ時に実行される内容を定義します。
各ディレクトリで terraform fmtterraform initterraform planterraform apply を実行します。

cloudbuild/apply.yaml を作成し、以下の内容を記述します。

cloudbuild/apply.yaml

timeout: "3600s"
logsBucket: "$_CLOUDBUILD_LOGBUCKET"

substitutions:
  _TERRAFORM_VERSION: "1.13.3"
  _CLOUDBUILD_LOGBUCKET: "gs://your-project-cloudbuild-logs"

steps:
  # ---------------------------------------------------------------------------
  # 1. GCS のapply
  # ---------------------------------------------------------------------------
  - id: "tf fmt for cloudstorage"
    name: "hashicorp/terraform:${_TERRAFORM_VERSION}"
    dir: "src/cloudstorage"
    args: ["fmt", "-check", "-diff"]

  - id: "tf init for cloudstorage"
    name: "hashicorp/terraform:${_TERRAFORM_VERSION}"
    dir: "src/cloudstorage"
    args: ["init"]

  - id: "tf plan for cloudstorage"
    name: "hashicorp/terraform:${_TERRAFORM_VERSION}"
    dir: "src/cloudstorage"
    args: ["plan"]

  - id: "tf apply for cloudstorage"
    name: "hashicorp/terraform:${_TERRAFORM_VERSION}"
    dir: "src/cloudstorage"
    args: ["apply", "-auto-approve"]

  # ---------------------------------------------------------------------------
  # 2. IAM のapply
  # ---------------------------------------------------------------------------
  - id: "tf fmt for iam"
    name: "hashicorp/terraform:${_TERRAFORM_VERSION}"
    dir: "src/iam"
    args: ["fmt", "-check", "-diff"]

  - id: "tf init for iam"
    name: "hashicorp/terraform:${_TERRAFORM_VERSION}"
    dir: "src/iam"
    args: ["init"]

  - id: "tf plan for iam"
    name: "hashicorp/terraform:${_TERRAFORM_VERSION}"
    dir: "src/iam"
    args: ["plan"]

  - id: "tf apply for iam"
    name: "hashicorp/terraform:${_TERRAFORM_VERSION}"
    dir: "src/iam"
    args: ["apply", "-auto-approve"]

  # ---------------------------------------------------------------------------
  # 3. CICD のapply
  # ---------------------------------------------------------------------------
  - id: "tf fmt for cicd"
    name: "hashicorp/terraform:${_TERRAFORM_VERSION}"
    dir: "src/cicd"
    args: ["fmt", "-check", "-diff"]

  - id: "tf init for cicd"
    name: "hashicorp/terraform:${_TERRAFORM_VERSION}"
    dir: "src/cicd"
    args: ["init"]

  - id: "tf plan for cicd"
    name: "hashicorp/terraform:${_TERRAFORM_VERSION}"
    dir: "src/cicd"
    args: ["plan"]

  - id: "tf apply for cicd"
    name: "hashicorp/terraform:${_TERRAFORM_VERSION}"
    dir: "src/cicd"
    args: ["apply", "-auto-approve"]

完成後の運用

CI/CD パイプライン完成後は、作業者個人の Google アカウントから CI/CD 専用サービスアカウントへ実行権限が移譲され、すべてのインフラ変更を GitHub 経由で行います。
作業者は GitHub でのプルリクエスト作成・マージ操作のみを行い、実際のインフラ変更は CI/CD 専用サービスアカウントが自動実行します。

基本的な運用フロー

  1. ブランチを作成して Terraform コードを変更
  2. GitHub でプルリクエストを作成
  3. 自動で terraform plan が実行され、変更内容を確認
  4. レビュー後、メインブランチへマージ
  5. 自動で terraform apply が実行され、インフラへ反映

この流れで、以下のようなインフラ変更を安全に実施できます。

  • 既存リソースの変更:GCS バケットの設定変更、サービスアカウントの権限変更など
  • 新しいリソースの追加:Compute Engine、Cloud SQL など
  • トリガーの変更:Cloud Build トリガーの設定変更

新しいリソースを追加する場合

新しい種類のリソース(例:Compute Engine)を追加する場合は、以下の作業が必要です。

  1. Terraform ファイルの追加

    • src/ ディレクトリ配下に新しいディレクトリを作成(例: src/compute
    • main.tfprovider.tfbackend.tf を配置
  2. ビルド構成ファイルへのステップ追加

    • cloudbuild/plan.yamlcloudbuild/apply.yaml に新しいディレクトリ用のステップを追加
    • 依存関係を考慮して適切な順序に配置

これらの変更も、上記の基本的な運用フロー(プルリクエスト作成→レビュー→マージ)に従って実施します。

おわりに

本記事では、Terraform による Google Cloud の CI/CD パイプラインの構築手順を、「なぜこの順番で構築するのか」「なぜこの時点でこの設定が必要なのか」という背景を重視して解説しました。
同じような課題に取り組んでいる方の参考になれば幸いです。

最後までご覧いただき、ありがとうございました。

Discussion