Knative: Configuration, Routes and Revisions

This is part 3 of a series on serverless applications with Knative. Part 1 is here, and Part 2 is here.

Introduction

In Part-1 of this series, you were introduced on how deploy your first serverless service using Knative Serving and in Part-2 you were able to understand how to build the serverless services using Knative Build. You may still have following questions, however:

  1. What is a Revision? Why is it needed?
  2. What is a route and what is its relation to a revision?
  3. Can I split traffic between revisions?

This post will answer these questions, but to get started I will answer the easiest of these questions: What is a Revision?

Revision is an immutable snapshot of the serverless workload at a given point in time. Sound confusing? Imagine a revision to be like a version control “tag.”  You can create any number of tags based on a commit but you can never edit or push a commit to a named tag.  

But why revision?

In Knative serving, things are driven via a Configuration to make a separation between code (container images) and config. One of the good practices in configuration management is that we should always be able to rollback the application state to any “last known good configuration”, to be able to allow this type of rollback Knative creates an unique revision for each and every configuration change.

Application Overview

For this post, let’s use OKD, the community distribution of Kubernetes that powers Red Hat OpenShift. To run OKD locally, we can use Minishift, a single node OKD cluster that can be used for development purposes.

These instructions will detail on how to install and configure Knative Serving on Minishift.The demo application we will be building is a greeter application using Spring Boot. As part of this demo, we will also be able to route the traffic to multiple revisions of the same greeter service.

 

First, clone the repo using the following command:

git clone https://github.com/workspace7/knative-serving-blogs

The sources for the examples shown in this post are available in the directory called “part-2” located in the root of the source repository. For convenience, we will refer to this directory (part-2) as $PROJECT_HOME in rest of this post.

Deploying the Serverless Service

In Part 1 of this series we utilized the all-in-one service based deployment workflow which created the needed objects such as a configuration, route and revisions automatically.   

In Part 2 we added build to the service to have a source-to-url workflow.  It was similar to the all-in-one service based workflow but instead of having to build the container images separately we embedded it as part of the service deployment.

In this post, we introduce you to another way of deploying your serverless workloads, where you will manually create the required objects such as configuration (with integrated build) and routes.  

Increase max user namespace

minishift ssh
sudo -i
sysctl user.max_user_namespaces=15000  && sysctl -p

Skip Tag to Digest

val=$(oc -n knative-serving get cm config-controller -oyaml \
  | yq r - data.registriesSkippingTagResolving \
  | awk '{print $1",docker-registry.default.svc:5000"}')
oc -n knative-serving get cm config-controller -oyaml \
  | yq w - data.registriesSkippingTagResolving $val \
  | oc apply -f - 

Check the Part-2 of this blog series for more information about the significance of these settings.

Before we can deploy the services, let’s have the build template deployed, this template will be used build section of the configuration to allow building of container images from sources:-

cd $PROJECT_HOME/build

oc apply -f  templates/java-buildah-template.yaml

By running oc apply -f config/java/configuration_rev1.yaml -n myproject from $PROJECT_HOME,  we trigger the deployment of the greeter service. You can watch the pod status using the command oc get pods -w -n myproject. A successful service deployment should create a Kubernetes Deployment like “greeter-00001-deployment”:

Though you have created a new service deployment via configuration, your service is not accessible yet as you have not yet defined a Knative route to it. Let’s create a route by running the command oc apply -f route/route_default.yaml -n myproject

Now you can run the script ${PROJECT_HOME}/bin/call.sh to invoke the service and see a response “Java Knative on OpenShift”.

TIP:

Time out is configurable as well, for this demo sake let us change the scale-to-zero-threshold from 5m to 30s and the scale-to-zero-grace-period from 2m to 1m.

oc -n knative-serving get cm config-autoscaler -o yaml \
  | yq w -  data.scale-to-zero-threshold 30s \
  | kubectl apply -f -
oc -n knative-serving get cm config-autoscaler -o yaml \
  | yq w -  data.scale-to-zero-grace-period 1m \
  | kubectl apply -f -

If there are no invocations to the application for 1 minute(default idle time), you will see the application getting automatically scaled down to zero. Running the command ${PROJECT_HOME}/bin/call.sh again, you will notice the pod comes up to serve the request again.

Let’s examine the  configuration and route objects that were created previously.

Configuration

Running this command will return the configuration object that was created for greeter service.

oc get configurations.serving.knative.dev greeter -o yaml

The yaml output of the above command is edited for brevity and shown below,

apiVersion: serving.knative.dev/v1alpha1
kind: Configuration
metadata:
  generation: 1
  labels:
    serving.knative.dev/route: greeter
  name: greeter
  namespace: myproject
spec:
  generation: 1
  revisionTemplate:
    metadata:
      creationTimestamp: null
      labels:
        app: greeter
    spec:
      container:
        image: docker-registry.default.svc:5000/myproject/greeter:0.0.1
        name: ""
        resources: {}
status:
  latestCreatedRevisionName: greeter-00001
  latestReadyRevisionName: greeter-00001
  observedGeneration: 1

Analyzing the highlighted lines above

  • The configuration object now has  a label serving.knative.dev/route with a value of route name that was created using this configuration, in this case greeter.
  • The status section has two attributes called latestCreatedRevisionName and latestReadyRevisionName both of them point to the one and only revision greeter-00001, which is the latest available revision.

Route

Running this command, will return the route object that was created for the greeter service.

oc get routes.serving.knative.dev greeter -o yaml

For a seasoned Kubernetes user the command above is unidomatic,  as you would be wondering why should I use the fully qualified name such as routes.serving.knative.dev ?

To know why let’s execute the command oc api-resources  | grep route, the command should return two API resources with name routes that are of kind Route.

OpenShift does already have a route object called route.openshift.io.  After deploying  Knative serving it has added one more Custom Resources Definitions(CRD) called routes.serving.knative.dev.

In this current context we are more concerned with using Knative serving route, hence we used oc get routes.serving.knative.dev with a fully qualified name to avoid any ambiguity.

TIP:

You can also use the short name rt  like oc get rt greeter -oyaml to get greeter route or oc get rt to get all the Knative routes

The YAML output of the above command is edited for brevity and shown below,

apiVersion: serving.knative.dev/v1alpha1
kind: Route
metadata:
  generation: 1
  name: greeter
  namespace: myproject
spec:
  generation: 1
  traffic:
  - configurationName: greeter
    percent: 100
status:
 domain: greeter.myproject.example.com
  domainInternal: greeter.myproject.svc.cluster.local
  targetable:
    domainInternal: greeter.myproject.svc.cluster.local
  traffic:
  - percent: 100
    revisionName: greeter-00001

Analyzing the highlighted lines above

  • spec. traffic  – the spec.traffic tells you
  • Which service identified configurationName to which the traffic will be routed.  The serving.knative.dev/route label of the configuration object identified by configurationName will be updated with the name of the route.
  • What will be the percentage of traffic that needs to be routed, in this case 100%
  • status.traffic

This gives all the possible traffic to revisionName distribution, in this case  as we have configured the route with configurationName “greeter” and the configuration has only one revision namely “greeter-00001”, all the traffic will be routed to greeter-00001.

  • status.domain DNS name of the route that can be used when calling the service from outside of the cluster. In this case “greeter.myproject.example.com”
  • status.domainInternal  – DNS name of the route that can to be used when calling the service from within the cluster. In this case “greeter.myproject.svc.cluster.local”

Rolling out a new Revision

Before rolling out a new revision, let’s make sure your previous revision is still responsive. Invoking the service via ${PROJECT_HOME}/bin/call.sh will send you a response like “Java Knative on OpenShift”.

As part of this new revision roll out, we will add the environment variable MESSAGE_PREFIX to the configuration which will  then be used by the application in the greeting response.

By running oc apply -f config/java/configuration_rev2.yaml -n myproject,  we trigger the deployment of the greeter service. You can watch the pod status using the command oc get pods -w -n myproject. A successful service deployment should create a Kubernetes Deployment like “greeter-00002-deployment”:

Now you can run the script ${PROJECT_HOME}/bin/call.sh to invoke the service and see a response “Hello Knative on OpenShift” instead of earlier “Java Knative on OpenShift”.

On reexamining our updated configuration via the command oc get routes.serving.knative.dev greeter -o yaml we see:

apiVersion: serving.knative.dev/v1alpha1
kind: Configuration
metadata:
  generation: 1
  labels:
    serving.knative.dev/route: greeter
  name: greeter
  namespace: myproject
spec:
  generation: 2
  revisionTemplate:
    metadata:
      creationTimestamp: null
      labels:
        app: greeter
    spec:
      container:
        env:
        - name: MESSAGE_PREFIX
          value: Hi
        image: docker-registry.default.svc:5000/myproject/greeter:0.0.1
        name: ""
        resources: {}
status:
  latestCreatedRevisionName: greeter-00002
  latestReadyRevisionName: greeter-00002
  observedGeneration: 2

Analyzing the highlighted lines above

  • The route label remains the same as you have not updated the route
  • The revisionTemplate has been updated to add the environment variable but you are still using the same container image as previous revision
  • The latestCreatedRevisionName and latestReadyRevisionName are updated to “greeter-00002” which is the new revision of the service that was created as a result of the configuration change ( adding environment variable)

You can get the list of available revisions via the command oc get revisions -n myproject or via OpenShift webconsole as shown below:

Distributing traffic between revisions

A common requirement in today’s distributed service world is to split traffic between revisions. This feature could be useful for testing  or it could be used to try some new or selected features with a smaller percentage of users (Canary Release).

The route you deployed earlier was tagged to the configurationName, but you did not specify what revision to use. In such cases, Knative-serving by default will route all  of the traffic to the latest revision.

To make your route distribute the traffic between revisions we need to define the route as follows,

apiVersion: serving.knative.dev/v1alpha1
kind: Route
metadata:
  name: greeter
spec:
  traffic:
    - revisionName: greeter-00001
      percent: 90
    - revisionName: greeter-00002
      percent: 10

In the traffic distribution above, you specify that you want the traffic to be split 90% to 10% between revision 1 (greeter-00001)  and revision 2 (greeter-00002) of the greeter service.
Run the command oc apply -f route/route_rev1-90_rev2-10.yaml -n myproject to deploy the updated route.

Now you can run the script ${PROJECT_HOME}/bin/call.sh to invoke the service and see various  responses of “Hi Knative on OpenShift” and “Java Knative on OpenShift”.  As you have defined revision 1 (greeter-00001) to have more percentage of traffic, you will see more “Java Knative on OpenShift” compared to “Hi Knative on OpenShift”.

How does Knative manages traffic distribution ?

The traffic distribution between the revision of Knative services are managed by Isito via its virutalservices.

Knative serving automatically create one virtualservice per configuration,  executingthe command oc get virtualservices you will see a virtualservice called “greeter” ( name of the virtualservice is same as that of the configuration) listed.

Examining the virtualservice,  you will notice that the virtualservice is configured to route the traffic between two revisions greeter-00001 and greeter-00002.

Running oc get virtualservices greeter -o yaml will show response as shown below: (The yaml response has been trimmed for brevity)

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: greeter
spec:
    route:
    - destination:
        host: greeter-00001-service.myproject.svc.cluster.local
        port:
          number: 80
      weight: 90
    - destination:
        host: greeter-00002-service.myproject.svc.cluster.local
        port:
          number: 80
      weight: 10

Point to Ponder

An interesting point to note the traffic distribution(route) when the deployment is dormant i.e. scaled down to zero.  You can use the command oc get virtualservices greeter -o yaml to get details about the greeter virutalservice:

apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: greeter
  ...
spec:
 ....
    route:
    - destination:
        host: activator-service.knative-serving.svc.cluster.local
        port:
          number: 80
      weight: 100
    timeout: 60s

As you see all the traffic is routed to “activator” service ( Knative serving component).  When the dormant service receives a request, the activator will activate i.e. scale up the service to its desired count ( replicas) and start to proxy the request to the activated service.

Try this!

The route folder in the $PROJECT_HOME has few more samples to demonstrate various traffic distribution amongst the service revisions like 50%-50%, 75%-25%.

After each deployment try to analyze the Istio virtualservice using the command oc get virtualservice greeter -oyaml and observe the traffic distribution between routes of the revisions.

Summary


At this point, you should have an understanding of what a Revision is in Knative and appreciate its significance. You learned how to execute a configuration and setup a route workflow along with distributing the traffic between revisions.  Lastly you also came to know how Knative serving activates a dormant service to start serving requests.

Categories
Kubernetes, Serverless
Tags
, , , , ,