Zenn Tech Blog

iTranslated by AI

The content below is an AI-generated translation. This is an experimental feature, and may contain errors. View original article
🧱

Switching Cloud Build Triggers from the Default to a Custom Service Account

に公開

Cloud Build on Google Cloud is a service for building applications. For teams deploying applications to Cloud Run or GKE, it is likely a frequently used service. However, there is a change coming to the default behavior of Cloud Build. For new Google Cloud projects created on or after April 29, 2024, the service account automatically associated when creating a Cloud Build trigger will be changed from the Cloud Build service account to the Compute Engine default service account.

https://cloud.google.com/build/docs/cloud-build-service-account-updates?hl=ja

Changes

When using Cloud Build, you create a resource called a trigger. A trigger holds information such as:

  • Trigger conditions for the build
  • The source location
  • The build configuration file name

Cloud Build executes builds based on this information. You can also specify a service account for a trigger to restrict the permissions applied during the build process. While you can explicitly specify a service account you created yourself, it is also possible to leave it "unspecified." In fact, this might be the most common state.

The change mentioned here refers to the behavior when this service account is left unspecified. Previously, if nothing was specified, the Cloud Build service account was automatically used. For projects created on or after April 29, the Compute Engine default service account will be selected instead. Both the Cloud Build service account and the Compute Engine default service account are automatically created when you create a Google Cloud project.

Why is this changing?

The documentation only states:

These changes improve your default security posture for future customers.

While this is just my speculation, I believe the intention is to phase out the Cloud Build service account, which holds overly broad permissions by default. Cloud Build has several implicit behaviors to focus on building applications, one of which is logging output to Cloud Storage buckets. The Cloud Build service account seems to be granted write permissions so that writing to Cloud Storage buckets can occur without the user needing to be explicitly aware of it. As a result, the default service account has more permissions than strictly necessary. It is possible that the Cloud Build service account might not even be created in projects started after April 29.

What about existing projects?

For projects that have already been created, you do not need to do anything, and there will be no issues. However, if you are managing Cloud Build configurations with IaC tools like Terraform, and the service account is unspecified (i.e., using the Cloud Build service account), applying the same Cloud Build settings to a new project created after April 29 will result in the Compute Engine default service account being set, which may lead to a mismatch in expected permissions.

Therefore, it is recommended to create a dedicated service account for Cloud Build when you have the opportunity and explicitly specify it when creating triggers. In this article, I will record the snippets for creating a service account with Terraform and re-configuring it for triggers.

What to do

To create a service account for Cloud Build and enable it to perform builds, the following steps were necessary:

  1. Create a service account for Cloud Build
  2. Add IAM bindings for the service account in Artifact Registry
  3. Specify the service account in the Cloud Build trigger
  4. Specify the logging option in cloudbuild.yml

1. Create a service account for Cloud Build

Since it can be difficult to re-investigate the necessary roles from scratch, it is easier to extract the roles currently set for the Cloud Build service account and assign those exact roles.

Extracting roles from the Cloud Build service account

First, identify the account name of the Cloud Build service account. You can find it on the IAM page by enabling the Include Google-provided role grants checkbox. It should be named "Cloud Build Service Account"; make a note of this principal name.

Next, identify the roles. You can check them using the following gcloud command. Try running it in Cloud Shell or a similar environment. Replace xxxxxxxxxxx@cloudbuild.gserviceaccount.com with the Cloud Build principal name you just identified.

cloudshell
gcloud projects get-iam-policy <project-name> \
--flatten="bindings[].members" --format='table(bindings.role)' \
--filter="bindings.members:xxxxxxxxxxx@cloudbuild.gserviceaccount.com"

ROLE: roles/appengine.deployer
ROLE: roles/appengine.serviceAdmin
ROLE: roles/cloudbuild.builds.builder
...

Create a new service account by copying these roles. I learned about this command on Stack Overflow.

https://stackoverflow.com/questions/47006062/how-do-i-list-the-roles-associated-with-a-gcp-service-account

Creating a new service account (Terraform)

Writing this in Terraform:

terraform
variable "project_id" {
  description = "The GCP project_id"
  type        = string
}
resource "google_service_account" "cloud_build" {
  project      = var.project_id
  account_id   = "cloud-build"
  display_name = "Cloud Build Service Account"
}
resource "google_project_iam_member" "cloud_build_appengine_deployer" {
  project = var.project_id
  role    = "roles/appengine.deployer"
  member  = "serviceAccount:${google_service_account.cloud_build.email}"
}

Defining a google_project_iam_member for every single role can be a lot of work... For reference, you can make this easier by running the following script in your browser. Feel free to modify it as needed.

Run in your browser console
// List all required roles here
const roles = ["roles/appengine.deployer",
"roles/appengine.serviceAdmin",
"roles/cloudbuild.builds.builder",
"roles/cloudfunctions.developer"]

// Terraform definitions will be outputted for copy-pasting
roles.forEach(role => {
    console.log(`
resource "google_project_iam_member" "cloud_build_${role.split('/')[1].replaceAll('.','_')}" {
  project = var.project_id
  role    = "${role}"
  member  = "serviceAccount:\${google_service_account.cloud_build.email}"
}
    `)
})

Once the service account definition is created, you can run terraform apply to create it.

2. Add IAM Binding for the service account in Artifact Registry

At Zenn, we deploy applications to Cloud Run and use Artifact Registry to store the Docker images that form the basis of our apps. Docker images are pushed from Cloud Build to Artifact Registry during the build. This means our newly created service account needs permission to write to Artifact Registry. Let's add the setting to the Artifact Registry side. We'll do this using Terraform as well.

terraform
variable "gcp_project_id" {
  type = string
}
+variable "cloud_build_service_account_email" {
+  type = string
+  description = "Email address of the Cloud Build service account"
+}
variable "artifact_registry_location" {
  type = string
  # https://cloud.google.com/storage/docs/locations
  description = "Location for Artifact Registry"
}

# Artifact Registry repository for applications
resource "google_artifact_registry_repository" "myapp" {
  project       = var.gcp_project_id
  location      = var.artifact_registry_location
  repository_id = "myapp"
  description   = "myapp application"
  format        = "DOCKER"
}

# Allow access from Cloud Build and Cloud Run in the build project.
resource "google_artifact_registry_repository_iam_binding" "binding_app_and_builder" {
  project    = var.gcp_project_id
  location   = var.artifact_registry_location
  repository = google_artifact_registry_repository.myapp.name
  role       = "roles/artifactregistry.repoAdmin"
  members = [
    # Allow pushing from Cloud Build
    "serviceAccount:xxxxxxxxxxxx@cloudbuild.gserviceaccount.com",
+    "serviceAccount:${var.cloud_build_service_account_email}",
  ]
}

Run terraform apply.

3. Specify the service account in the Cloud Build trigger

Add this in Terraform as well.

terraform
variable "project_id" {
  description = "Project ID"
  type        = string
}
+variable "cloud_build_service_account_id" {
+  description = "The ID of the service account to set for Cloud Build"
+  type        = string
+}
variable "cloud_run_service_account" {
  description = "The backend service account to set for Cloud Run"
  type        = string
}

resource "google_cloudbuild_trigger" "deploy-cloudrun" {
  name            = "deploy-cloudrun"
  description     = "Deploy application to Cloud Run"
+  service_account = var.cloud_build_service_account_id

  github {
    owner = "zenn-dev"
    name  = "application"
    push {
      branch = "main"
    }
  }
  included_files = ["*"]
  filename       = "cloudbuild.yml"
  substitutions = {
    _CLOUD_RUN_SERVICE_ACCOUNT      = var.cloud_run_service_account
    _SHORT_BUILD_ID                 = "$${BUILD_ID:0:8}"
  }
}

Running terraform apply will add the service account to your Cloud Build trigger.

4. Specify the logging option in cloudbuild.yml

Modifications are also required for cloudbuild.yml. You must explicitly specify the logging option. Roughly speaking, you can choose whether to output to Cloud Logging or Cloud Storage, but I have set it to record to Cloud Logging this time. Note that the default behavior is to output to both Cloud Logging and a Cloud Storage bucket, so be aware that the behavior will change if you follow the setting below.

https://cloud.google.com/build/docs/securing-builds/store-manage-build-logs?hl=ja

cloudbuild.yml
    steps:
    - name: 'gcr.io/cloud-builders/docker'
    args: ['build', '-t', 'us-east1-docker.pkg.dev/myproject/myimage', '.']
+   options:
+     logging: CLOUD_LOGGING_ONLY

Once you have prepared these materials and run the build, it should complete successfully using the service account you created.

Conclusion

I have documented the procedure for changing the service account for Cloud Build triggers in existing projects. Going through this process was a good opportunity to learn about some of the implicit behaviors and permissions involved. Since I will continue to heavily use Cloud Build, I am glad I could become a bit more familiar with it. I hope this article is helpful to someone.

References

Zenn Tech Blog
Zenn Tech Blog

Discussion