This article explains an approach to continuously deploy revisions to Cloud Run service(s) based on a new container image.

Continuous Deployment

Continuous deployment is a strategy for software releases wherein any  code commit that passes the automated testing phase is automatically  released into the production environment, making changes that are  visible to the software's users.

Cloud Run

Cloud Run is a managed compute platform that enables you to run  stateless containers, on its serverless environment and abstracts away  all infrastructure management, so you can focus on what matters  most — building great applications.

If you are new to Cloud Run, feel free to jump into its documentation here.

Continuous deployment on Cloud Run across multiple unknown services based on a container image could be challenging.

Google Cloud Run does not automatically deploy a revision when you  push a new image to a tag reference. There are many good reasons it  doesn’t.
When a Cloud Run revision is deployed, it computes the sha256 hash of the image reference.
Therefore when you specify a container image with :latest tag, Cloud  Run uses its sha256 reference to deploy and scale out that revision of  your service. When you update :latest tag to point to the new image,  Cloud Run will still use the previous image. It would be a dangerous and  slippery slope otherwise.  - Ahmet on SO

Continuous Deployment for a Single Cloud Run Service

Google Cloud Build triggers can be used to build and push new  container images when code is updated, you can as well add another step  to your build configuration for continuous deployment.

Here's an example of a Cloud Build configuration that builds and  pushes new images to Google Container Registry (GCR), it also deploys a  new revision to the Cloud Run service dashboard.


# cloudbuild.yaml
# Build and Push New Image to Google Container Registry
- name: "gcr.io/kaniko-project/executor:latest"
  args: ["--cache=true", "--cache-ttl=48h", "--destination=gcr.io/project/dashboard:latest"]

# Extra step to Deploy New Revision to Cloud Run
- name: "gcr.io/cloud-builders/gcloud"
  args: ['beta', 'run', 'deploy', 'dashboard', '--image', 'gcr.io/project/dashboard:latest', '--region', 'us-central1', '--allow-unauthenticated', '--platform', 'managed']

Continuous Deployment for Multiple Cloud Run Services

If you need to update multiple Cloud Run services, you can simply  have extra steps for deploying to Cloud Run services within your Cloud  Build configuration, provided you know the service name of each Cloud  Run service.

At  Mercurie, we use Cloud Run for Multitenancy - this involves automatically creating new services for every new client (or tenant).

To achieve continuous deployment across stores, you could write a  Cloud Function that deploys new revisions to your multiple Cloud Run  services with the updated container image. This works by subscribing to  Cloud Build's notifications through Pub/Sub and then triggering the  Cloud Function.

Cloud Function to Update Cloud Run Services

Cloud Functions allows users to write single-use, programmatic  functions which listen for cloud events, such as builds. When a cloud  event occurs, a trigger responds by executing an action, such as sending  a Cloud Pub/Sub message.

Cloud Build publishes messages on a Google Cloud Pub/Sub topic when  your build's state changes, such as when your build is created, when  your build transitions to a working state, and when your build  completes.

The Pub/Sub topic to which Cloud Build publishes these build update messages is called cloud-builds,  and it is automatically created for you when you enable the Cloud Build  API. Each message contains a JSON representation of your Build  resource, and the message's attributes field contain the build's unique  ID and the build's status.

Visit Cloud Functions and Create Function

  • Enter your function's name : updateCloudRunServices
  • Set Memory Allocated : 256MB
  • Set Trigger : Cloud Pub/Sub
  • Set Topic : cloud-builds
  • Source code : Select - Inline editor
  • Runtime : Node.js 8
  • Function to execute : updateCloudRunServices

Paste the following code snippet into the Inline editor and create your function.

// index.js

const request = require("request");

// Main function called by Cloud Functions trigger.
module.exports.updateCloudRunServices = (event, callback) => {
  const build = eventToBuild(event.data);

  // Check if push is to Dashboard source Code repo and if Cloud Build is successful
  if (build.hasOwnProperty("source")) {
    if (
      build.source.repoSource.repoName === "dashboard" &&
      build.status == "SUCCESS"
    ) {
      updateDashboardRevisions();
    }
  }
};

const updateDashboardRevisions = () => {
  let servicesListOption = {
    method: "GET",
    url:
      "https://run.googleapis.com/v1alpha1/projects/mercuriemart/locations/us-central1/services",
    headers: {
      Authorization: `Bearer ${token}`, // @TODO: Ensure you pass your GCP API *token*
      "Content-Type": "application/json"
    }
  };

  // Get Cloud Run Services List
  request(servicesListOption, function(error, response, body) {
    if (error) throw new Error(error);

    const services = JSON.parse(body);

    let deploySteps = [];

    // Iterate Through Services
    services.items.forEach(element => {
      let serviceName = element.metadata.name;
      if (
        element.metadata.annotations.hasOwnProperty(
          "client.knative.dev/user-image"
        )
      ) {
        // Filter service based on Image and create new Build
        if (
          element["metadata"]["annotations"]["client.knative.dev/user-image"] ==
          "gcr.io/project/dashboard:latest"
        ) {
          deploySteps.push({
            // Deploy New Revision to Service
            name: "gcr.io/cloud-builders/gcloud",
            args: [
              "beta",
              "run",
              "deploy",
              `${serviceName}`,
              "--image",
              "gcr.io/project/dashboard:latest",
              "--region",
              "us-central1",
              "--allow-unauthenticated",
              "--platform",
              "managed"
            ]
          });
        }
      }
    });

    // Update Cloud Run Services
    let options = {
      method: "POST",
      url: "https://cloudbuild.googleapis.com/v1/projects/mercuriemart/builds",
      headers: {
        "cache-control": "no-cache",
        Authorization: `Bearer ${token}`, // @TODO: Ensure you pass your GCP API *token*
        "Content-Type": "application/json"
      },
      body: {
        steps: deploySteps,
        timeout: "1200s"
      },
      json: true
    };

    // Cloud Build Request to Deploy Revisions across Cloud Run Services
    request(options, function(error, response, body) {
      if (error) throw new Error(error);

      console.log(body);
    });
  });
};
{
    "name": "updateRevisions",
    "version": "0.0.1",
    "description": "Update Cloud Run Services",
    "main": "index.js",
    "dependencies": {
      "request": "^2.88.0"
    }
}
 

You  now have a Cloud Function that listens to Pub/Sub notifications from  Cloud Build when source code gets updated, which also builds a new  container image and pushes to GCR using Cloud Build Triggers.

It checks if the updates are to the dashboard source code repo and if the build status is SUCCESS, then it calls the updateDashboardRevisions method which uses a token to make API request to Cloud Run, fetches all services, filters them to get only the services using the container image gcr.io/project/dashboard:latest.

Then creates a new build configuration with steps which is submitted  to Cloud Build via its API. This successfully updates all Cloud Run  services that are based on our container image gcr.io/project/dashboard:latest.

Additional Resources

Thanks for reading through! Let me know if I missed any step, if  something didn’t work out quite right for you or if this guide was  helpful.