Good news everyone: We finally managed to make deploying serverless containers as simple as gcloud run deploy --image=[IMAGE]. This command deploys an application to Cloud Run with the given docker image, but what does really happen behind the scenes to make this happen?

Recently I reimplemented this gcloud run deploy command in a program, using the API calls through client libraries, so I’ll explain a bit how this command works under the covers.

If you’re trying to deploy to Cloud Run programmatically, this guide might help, however you’re far better off using the gcloud run services replace command (which works like kubectl apply, with YAML manifests).

Cloud Run API endpoints

Normally, Cloud Run API is at run.googleapis.com. However, each region of Cloud Run actually has a different control plane. So your requests go to regional endpoint, which looks like:

https://us-central1-run.googleapis.com/apis/serving.knative.dev/v1/[...]

As you may notice, the URL follows the Kubernetes API style, which is:

https://{SERVER}/apis/{apiGroup}/{apiVersion}/[...]

This is not a coincidence: Cloud Run implements the Knative API (which is modeled on top of Kubernetes API). Therefore, the inner workings of this API is designed to work with Knative tooling (such as kn CLI or kubectl), more on this below.

Knative API under the hood

After collecting deployment options from the command-line options (like name, image, CPU, environment variables…) the command builds an in-memory Knative Service object.

After that, the object is serialized into a JSON object (in case you did not know, kubectl apply also converts your YAMLs to JSON on the wire) and submitted to the Cloud Run API endpoint.

Kubernetes Namespaces are used to identify GCP Project IDs

Cloud Run uses the metadata.namespace field of Knative API objects to indicate which GCP project the resource belongs to. For example, listing Cloud Run apps look like:

GET /apis/serving.knative.dev/v1/namespaces/[GCP_PROJECT]/services

Creating a Service

Creating a Service follows the Kubernetes API convention. After all, a Service is a CRD under apiGroup=serving.knative.dev and apiVersion=v1, just like it would be on a Kubernetes cluster –despite we’re not doing Kubernetes here at all.

The request involves sending the JSON-serialized object to this endpoint in a POST request:

POST /apis/serving.knative.dev/v1/namespaces/[GCP_PROJECT]/services

Updating a Service

If you need to make an update to a Service, you need to GET the Service first, make the updates you want and PUT that back to the REST API object:

PUT /apis/serving.knative.dev/v1/namespaces/[GCP_PROJECT]/services/[NAME]

Doing this will generate a new Revision, with a random name like hello-jfhpd. If you want to generate nicer names gcloud provides with numbers like hello-00001-jfhpd, you can update spec.template.metadata.name (sets Revision name) as part of the updated object.

Wait… if this behaves like Kubernetes API Server, can’t I use kubectl?

Technically yes, but not yet. You could craft a special kubeconfig file or specify kubectl options like (--token, --server) and point it to the Cloud Run API endpoint, but it still wouldn’t work, since currently:

  • Cloud Run API does not support the API discovery endpoints that kubectl needs
  • Cloud Run API does not support strategic merge patch which is used in kubectl apply.

Waiting for deployment readiness

Differently than kubectl apply which just fires & forgets deployments, the gcloud command actually waits until the Service reaches a “ready” state.

This is done by polling the Service (GET) and look at the status.conditions field. A successful deployment will have a condition like:

status:
  conditions:
  - type: Ready
    status: "True"

A failed deployment will have a condition listing an error message like this:

status:
  conditions:
  - type: Ready
    status: "Unknown" # or "False" if deployed but failed to become ready
    message: "Cloud Run failed to deploy the application [...]"

Making the application publicly accessible

If you have specified --allow-unauthenticated to make your application publicly accessible, this involves setting IAM bindings on the Service you deployed.

To see how it is done under the covers, run this with --log-http:

gcloud run services add-iam-policy-binding NAME \
  --member=allUsers --role=roles/run.invoker

Conclusion

To see my reimplementation of gcloud run deploy using GCP Go client libraries, check out here. Hope you had fun! Let me know on Twitter if this helped you implement a tool or automation around the Cloud Run API.