👋

Terraformを使ってCloud BuildからCloud Runにデプロイする仕組みを作る

2024/02/19に公開1

以前こちらの記事でNext.jsのアプリをCloud Runにデプロイしましたが、今回はCloud Buildからデプロイするように変更したので、ハマりポイントなどを共有します。

https://zenn.dev/tokku5552/articles/run-and-sql-nextjs

GitHubとの接続

まずGoogle CloudのプロジェクトとGitHubリポジトリを接続します。
接続するにはアクセストークンを使ったりする方法もありますが、今回はGoogle Cloud BuildのGitHub Appsを使って接続する方法を紹介します。

  • まずCloud Buildのコンソールでホスト接続を作成をクリックします。
    image

  • GitHubを選択した状態で、リージョンを選択肢、名前をつけて接続をクリックします。
    image

  • 以下のようなポップアップが出てくるのでContinueを押してしばらく待ちます。
    image

  • インストールを選択する画面に切り替わりますので、すでにGitHub Appsをインストールしているorgはそれを使い、そうでない場合は新しいアカウントでインストールをクリックします。
    image

  • GitHubの画面にリダイレクトするので、orgを選択して必要なリポジトリに対して権限を与えます。
    image

  • そうすると以下のように接続されます。
    image

今回は上記の接続は使わずにTerraformで管理します。

terraformでCloud Build周りを構成する

Cloud Buildのトリガーを作成する前に、リポジトリとの接続とCloud Buildのサービスアカウントへの権限付与を行います。

resource "google_cloudbuildv2_connection" "github_connection" {
  location = var.region
  name = "github-connection"

  github_config {
    app_installation_id = var.github_app_installation_id
    authorizer_credential {
      oauth_token_secret_version = var.github_oauth_token_secret_version
    }
  }
}

resource "google_cloudbuildv2_repository" "github_repository" {
  name = "github-repository"
  parent_connection = google_cloudbuildv2_connection.github_connection.id
  remote_uri = var.github_repository_remote_uri
}

resource "google_project_iam_member" "cloudbuild_iam" {
  for_each = toset([
    "roles/run.admin",
    "roles/iam.serviceAccountUser",
    "roles/secretmanager.secretAccessor",
  ])
  role    = each.key
  member  = "serviceAccount:${var.project_number}@cloudbuild.gserviceaccount.com"
  project = var.project_id
}

変数で指定している箇所がいくつかありますが、以下を指定してください。

  • var.github_app_installation_id
    • GitHub Apps の Cloud Build アプリケーションのインストール ID
  • var.github_oauth_token_secret_version
    • GitHub AppsのOAUTH TOKENのシークレット。Google Cloud BuildのGitHub Appsを接続した手順でxxxx-github-oauthtoken-xxxxのような名前でSecret Managerに登録されているはずなので確認してください。例えばprojects/123456789/secrets/xxxx-github-oauthtoken-xxxx/versions/1のような形式になります。
  • var.github_repository_remote_uri
    • リポジトリのuri(https)。今回のサンプルアプリの場合https://github.com/tokku5552/nextjs-gcp-sample.git

そしてCloud Build Triggerのリソースを追加します。

resource "google_cloudbuild_trigger" "my-app_trigger" {
  location = var.region
  project = var.project_id
  repository_event_config {
    repository = google_cloudbuildv2_repository.github_repository.id
    push {
      branch = "^main$"
    }
  }
  filename = "my-app/cloudbuild.yaml"

  substitutions = {
    _REGION                         = var.region
    _ARTIFACT_REPOSITORY_IMAGE_NAME = "${var.region}-docker.pkg.dev/${var.project_id}/artifact-registry-nextjs-gcp-sample-app/console"
  }
}

substitutionscloudbuild.yaml内で使える環境変数を定義することができます。

repository_event_configでmainブランチにpushしたときとしていますが、他にも色々設定できます。

https://registry.terraform.io/providers/hashicorp/google/latest/docs/resources/cloudbuild_trigger#repository_event_config

cloudbuild.yamlを追加してビルドとデプロイを自動化する

次にcloudbuild.yamlを追加します。

以下の記事を参考にさせてもらいました。

https://zenn.dev/waddy/books/graphql-nestjs-nextjs-bootcamp/viewer/deploy_nextjs

https://zenn.dev/waddy/articles/nextjs-terraform-cloud-run

my-app/cloudbuild.yaml
steps:
  - name: "gcr.io/cloud-builders/docker"
    entrypoint: "bash"
    args:
      - -c
      - >-
        docker buildx build
        --platform linux/amd64
        --build-arg DATABASE_URL=$$DATABASE_URL
        --tag $_ARTIFACT_REPOSITORY_IMAGE_NAME:$SHORT_SHA
        --tag $_ARTIFACT_REPOSITORY_IMAGE_NAME:latest
        --file Dockerfile
        --cache-from $_ARTIFACT_REPOSITORY_IMAGE_NAME:latest
        --push
        .
    dir: "my-app"
    automapSubstitutions: true
    secretEnv: ["DATABASE_URL"]
  - name: "gcr.io/google.com/cloudsdktool/cloud-sdk"
    entrypoint: gcloud
    args:
      - "run"
      - "deploy"
      - "app"
      - "--image"
      - "$_ARTIFACT_REPOSITORY_IMAGE_NAME:$SHORT_SHA"
      - "--region"
      - "$_REGION"
substitutions:
  _REGION: by-terraform
  _ARTIFACT_REPOSITORY_IMAGE_NAME: by-terraform
availableSecrets:
  secretManager:
    - versionName: projects/$PROJECT_ID/secrets/DATABASE_URL/versions/latest
      env: DATABASE_URL
images:
  - "$_ARTIFACT_REPOSITORY_IMAGE_NAME:$SHORT_SHA"

上記の記事の割とそのままなので、ここで特に解説することは無いですが、モノレポを意識してmy-app/cloudbuild.yamlにファイルを配置したため、dir: "my-app"を指定しています。
また、Secret ManagerからDATABASE_URLを取得していますが、build-argで渡したい場合はgcr.io/cloud-builders/dockerのentrypointをbashにする必要があります。

最初から使える変数や、自分で定義する場合、Secret Managerから取得する場合などについては、以下の公式ページに情報がまとまっていますので参照ください。

https://cloud.google.com/build/docs/configuring-builds/substitute-variable-values?hl=ja

Cloud Buildからデプロイされても良いようにCloud Runリソースを修正する

もともとのCloud Runのリソースに以下を追加して、Cloud Buildからデプロイされてimage urlなどが変更されたとしても、terraformからは検知しないようにします。

lifecycle {
    ignore_changes = [
      client,
      client_version,
      template[0].containers[0].image,
    ]
  }

これでterraform applyすれば、以後mainブランチにマージされるたびにビルドとデプロイを行ってくれます。

まとめ

振り返って整理してみるとたいしたことないように見えますが、自分は結構時間を使ってしまいました。
特にIAM周りの設定がうまくいかずに失敗すると、Cloud Build上で404エラーとだけ出て原因がわからないのでかなり試行錯誤しました。
ただ、今回Terraformで接続部分もコード化したため、アプリケーションを環境毎に別のプロジェクトにデプロイしたい場合なんかでも使い回せるようになったと思います。

Discussion

ハトすけハトすけ

記事ありがとうございます。

ハマったので共有です。

google_cloudbuild_triggerの部分ですが、400エラーがでて失敗してしまいます。

Error: Error creating Trigger: googleapi: Error 400: Request contains an invalid argument.

おそらく、GoogleCloudの仕様変更で CloudBuildのAPIを有効化しても、デフォルトのCloudBuildアカウントが作成されなくなったのが原因かもしれません(間違ってたらすみません)。

stackoverflowを参考にその場でservice_accountを作成してgoogle_cloudbuild_triggerに渡せば、エラーなく通りました。

resource "google_service_account" "cloudbuild_service_account" {
  account_id   = "cloudbuild-sa"
  display_name = "cloudbuild-sa"
  description  = "Cloud build service account"
}

resource "google_project_iam_member" "act_as" {
  project = var.project_id
  role    = "roles/iam.serviceAccountUser"
  member  = "serviceAccount:${google_service_account.cloudbuild_service_account.email}"
}

resource "google_project_iam_member" "logs_writer" {
  project = var.project_id
  role    = "roles/logging.logWriter"
  member  = "serviceAccount:${google_service_account.cloudbuild_service_account.email}"
}

resource "google_cloudbuild_trigger" "github" {
  location = var.region

  repository_event_config {
    repository = google_cloudbuildv2_repository.github_repository.id
    push {
      branch = var.branch_pattern
    }
  }

  filename = var.cloudbuild_file_name

  # これがないと400エラーになる
  # 仕様変更: https://blog.g-gen.co.jp/entry/cloud-build-service-account-changes
  service_account = google_service_account.cloudbuild_service_account.id

  # cloudbuild.yamlで使用できる変数
  substitutions = {
    _REGION                         = var.region
    _ARTIFACT_REPOSITORY_IMAGE_PATH = var.registory_path
  }
}

参考:
https://blog.g-gen.co.jp/entry/cloud-build-service-account-changes
https://stackoverflow.com/questions/76352037/error-400-request-contains-an-invalid-argument-while-creating-google-cloudbuild