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.