LAB32_GCP_DevOps: CI/CD Pipeline with Cloud Build

DevOps: CI/CD Pipeline with Cloud Build

☁ GCP 🔥 Intermediate Cloud Build Cloud Run CI/CD GitHub

Scenario

You want code pushes to automatically build a Docker container and deploy it to serverless infrastructure. Instead of using GitHub Actions or Jenkins, you will use Google's native Cloud Build. You need to create a Build Trigger that listens to a GitHub repository branch, executes a `cloudbuild.yaml` file to build the Docker image, pushes it to Artifact Registry, and deploys it to Cloud Run.

Your Objectives
  • Provision a google_artifact_registry_repository to host the built images.
  • Provision a dedicated google_service_account for Cloud Build to use.
  • Grant this service account roles/run.admin, roles/iam.serviceAccountUser, and Artifact Registry Writer roles.
  • Create a google_cloudbuild_trigger that points to a specific GitHub repository branch (assumes the GCP-GitHub app integration is pre-configured).

Additional Context

GitHub Integration: For `google_cloudbuild_trigger` to natively attach to a GitHub repository, a GCP Administrator must manually install the "Google Cloud Build" GitHub App into their GitHub organization out-of-band via the Cloud Console once. Terraform cannot do the initial OAuth handshake automatically.

Service Accounts: By default, Cloud Build uses the legacy Cloud Build Service Account `[Project_Number]@cloudbuild.gserviceaccount.com`. Security best practice mandates that you deploy a dedicated custom service account and attach it to the trigger via `service_account = ...` to enforce Least Privilege.

variables.tf

variable "project_id" { type = string }
variable "github_owner" { type = string; default = "my-github-username" }
variable "github_repo" { type = string; default = "my-app-repo" }

main.tf

provider "google" {
  project = var.project_id
  region  = "us-central1"
}

# 1. Image Registry
resource "google_artifact_registry_repository" "docker_repo" {
  location      = "us-central1"
  repository_id = "cicd-app-repo"
  format        = "DOCKER"
}

# 2. Custom Service Account for the Pipeline
resource "google_service_account" "builder_sa" {
  account_id   = "cloud-build-runner"
  display_name = "Cloud Build Execution Account"
}

# 3. IAM Bindings for the Builder SA
# Build needs to push images
resource "google_project_iam_member" "artifact_writer" {
  project = var.project_id
  role    = "roles/artifactregistry.writer"
  member  = "serviceAccount:${google_service_account.builder_sa.email}"
}

# Build needs to deploy to Cloud Run
resource "google_project_iam_member" "run_admin" {
  project = var.project_id
  role    = "roles/run.admin"
  member  = "serviceAccount:${google_service_account.builder_sa.email}"
}

# Build needs permission to act as the Cloud Run runtime Service Account
resource "google_project_iam_member" "sa_user" {
  project = var.project_id
  role    = "roles/iam.serviceAccountUser"
  member  = "serviceAccount:${google_service_account.builder_sa.email}"
}

# 4. The Cloud Build Trigger
resource "google_cloudbuild_trigger" "push_to_main" {
  name        = "deploy-main-branch"
  description = "Builds and deploys container to Cloud Run on push to main"

  # Uses the connected GitHub repository (requires prior manual connection in Console)
  github {
    owner = var.github_owner
    name  = var.github_repo
    push {
      branch = "^main$"
    }
  }

  # Use the custom Least-Privilege IAM identity for the build
  service_account = google_service_account.builder_sa.id

  # Look for this file in the root of the GitHub repository
  filename = "cloudbuild.yaml"

  # Define substitution variables that cloudbuild.yaml will ingest
  substitutions = {
    _REGION    = "us-central1"
    _REPO_NAME = google_artifact_registry_repository.docker_repo.name
    _APP_NAME  = "my-serverless-app"
  }
}

Sample cloudbuild.yaml (stored in GitHub)

steps:
  # 1. Build the container image
  - name: 'gcr.io/cloud-builders/docker'
    args: ['build', '-t', '${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_REPO_NAME}/${_APP_NAME}:${SHORT_SHA}', '.']

  # 2. Push the image to Artifact Registry
  - name: 'gcr.io/cloud-builders/docker'
    args: ['push', '${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_REPO_NAME}/${_APP_NAME}:${SHORT_SHA}']

  # 3. Deploy the container to Cloud Run
  - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
    entrypoint: gcloud
    args:
      - 'run'
      - 'deploy'
      - '${_APP_NAME}'
      - '--image'
      - '${_REGION}-docker.pkg.dev/${PROJECT_ID}/${_REPO_NAME}/${_APP_NAME}:${SHORT_SHA}'
      - '--region'
      - '${_REGION}'