Knative: Building your Serverless Service

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

Introduction

In the Part-1 of Knative Serving blog series, you were introduced on how to build and deploy your first serverless service using Knative Serving. In this blog you will be introduced to another Knative component called Knative Build.

What is Knative Build ?

Knative Build is a way to do an on-cluster(Kubernetes) build of container images from your source code i.e. source-to-image, the built container image can then be used in other Kubernetes resources like Knative serving service.

Why Knative Build ?

Kubernetes native (using Custom Resources Definitions) way to define a set of  reusable steps that can run on-cluster builds from application sources to produce container images. These container images are designed to be optimised in size and have better runtime performance. Knative Build is not a CI/CD solution but is designed to provide building blocks to integrate with your existing CI/CD solutions.

Components Overview

Knative Build has two main components, Build and BuildTemplate, both defined via Custom Resources Definitions(CRD):

  • Build(builds.build.serving.knative.dev)

A build is a sequential set of instructions called “step”,  that defines the process of how to build your sources into a container image. Each build step will be run using another arbitrary container image called Builder.

The source of the build is usually an external data source mounted as a Kubernetes volume. Currently, the following sources are supported:

  • Git
  • Google Cloud Storage
  • An arbitrary container image

As the steps of the build are sequential, the output from the previous steps are available for the subsequent steps. In a build you can also define environment variables that can be used inside the build step, as well as pass arguments to the Builder image; e.g. if you are running a npm based builder and the npm command needs parameters to run,  in such cases you can pass arguments builder run command using the arguments. The later part of this post covers these in much more detail.

  • BuildTemplate(buildtemplate.build.knative.dev)

The build steps are usually defined inline within the build spec, or they can be defined via reusable build steps called BuildTemplate.

BuildTemplates provides an option for the Build authors to define the parameters that can then be used in build steps. The parameters help Build to override or define values to the parameters in the respective Build definition.

The Build and BuildTemplate can also make use of a ServiceAccount object to define the secrets to pass the credentials—GitHub tokens, user credentials, SSH-keys—to the build steps to help make use of them where they might be needed.

Setup

In order to be able to run the demo code in this post, you will need a Minishift instance running with Knative Serving and Knative Build.

For this blog, I will be using 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 explain how to install and configure Knative Serving on Minishift. Once you have Knative Serving up, add the Knative Build components by following the instructions from here.

Application Overview

The demo application we will be building is a greeter application built using Spring Boot. You can find the sources of the application in the GitHub repository.

Git clone the resources using the command:

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

All the sources for the examples shown in this blog are available in the directory “part-1” located in the root of the source repository, for convenience we will refer to this directory as $PROJECT_HOME in rest of this post.

The application will use Buildah to build the java application container images. Buildah allows you to build container images without the need of a Docker daemon. The images built by Buildah are OCI compliant.

Increase max user namespace

The user namespace values on RHEL 7 / Centos 7 are set to 0 by default. The value needs to be updated to enable buildah container builds to pass. For this demo we will be setting max_user_namespaces to a value of 15000.

minishift ssh

sudo -i

echo 15000 > /proc/sys/user/max_user_namespaces

Skip Tag to Digest (optional)

In the following section we will see what is a image digest,  how it is used by Kubernetes deployment and how you can skip the resolution of image to digest in Knative-serving:-

Docker images wit v2 or later format has content addressable identifier called digest. The digest remains unchanged as long the underlying image content remains unchanged.

Source: https://docs.docker.com/engine/reference/commandline/images/#list-image-digests

Let say you have a deployment like the following (note that resource definition have been trimmed for brevity):

apiVersion: apps/v1
kind: Deployment
metadata:
   name: helloworld 
spec:
  template:
    spec:
       containers:
         - name: my-container
           image: gcr.io/knative-samples/helloworld-go

. . .

When you deploy this application in Kubernetes, the deployment will look like:

apiVersion: apps/v1
kind: Deployment
metadata:
   name: helloworld 
spec:
  template:
    spec:
       containers:
          - name: my-container
            image: >-
                   gcr.io/knative-samples/helloworld-go@sha256:98af362ceca8191277206b3b3854220ce125924b28a1166126296982e33882d0
. . .

 

In the above example the container image name of the deployment  i.e. gcr.io/knative-samples/helloworld-go was resolved to its digest gcr.io/knative-samples/helloworld-go@sha256:98af362ceca8191277206b3b3854220ce125924b28a1166126296982e33882d0. Kubernetes extracts the digest from the image manifest.  This process of resolving  image tag to its digest is informally called as “Tag to Digest”.

Knative Serving deployments by default resolve the container images to digest during the deployment process. Knative Serving has been configured to skip the resolution of   image name/tag to image digest for registries ko.local and dev.local, which can be used for local development builds, as the underlying image content are subject to changes during the  development process.

As part of the demo in this post you will be building and pushing the container image(s) to the OpenShift internal docker registry (docker-registry.default.svc:5000). As there are chances that you will keep mutating the container image during local testing, I suggest that you add the OpenShift internal docker registry to the existing list of registries  to be skipped for image tag to digest resolution, like so

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 - 

NOTE: Please don’t do this in production. For production it’s safer and more secure for the tag to be resolved to a digest.

Deploy BuildTemplate

With your environment set now, you are ready to deploy your first build template. Let’s take a quick look at the various sections of a BuildTemplate.  

The snippets shown in the section below are extracted from https://github.com/workspace7/knative-build-blogs/blob/master/part-1/build/templates/java-buildah-template.yaml

The BuildTemplate has three main sections:

parameters

The parameters that are available for a build using this template to pass. A parameter has a name, description and an optional default value .

The following section details the possible ways to define a parameter.

Parameter with Default value

The parameter definition has three main parts: name, description and an optional default value,

   name: JAVA_APP_BUILDER_IMAGE
   description: The builder image used to build the java application
   default: gcr.io/cloud-builders/mvn

Parameter without Default value

If the default value is not provided to the parameter definition, then it means that it is a required parameter and the respective build must pass this value during build time.

  name: JAVA_APP_NAME
  description: The jar file name of the build artifact

steps

Each step has a name and image attribute (https://github.com/workspace7/knative-build-blogs/blob/master/part-1/build/templates/java-buildah-template.yaml#L29-L38) , the name will act as name of the the init container that runs the builder image (image) as part of the the step and the image is the Builder image that will be used by the step.

The step also has the following sub sections:

  • args

The args defines the array of arguments that will be passed to the builder image of the step. An example the builder would defined have entrypoint and expects few arguments.

Let’s take the first build step(some parts of the source is trimmed for brevity, and variables expanded),

   name: build-app
   image: gcr.io/cloud-builders/mvn
   args:
    - clean
    - package
    - -Duser.home=/builder/home

This build step uses the gcr.io/cloud-builders/mvn builder image which has the entry point to the mvn ( the Apache Maven build tool), as maven expects goals to be run as part of its program arguments, you can pass them using the args section of the build step as shown above.

  • workingDir

The directory which will be set as working directory inside for the builder image. This directory will be mounted automatically as /workspace onto the builder image by Knative Build.

Let’s say you are using git as the source in your build, this attribute could be set to the sub directory within the source root from where the build need to be run.

(e.g.)

https://github.com/workspace7/knative-build-blogs/blob/master/part-1/build/templates/java-buildah-template.yaml#L35

  • env

This section defines the environment variables that need to be available to the builder image. These variables can then be used internally by the build scripts or programs of the builder image.

  • volumeMounts

The mount points that are mounted using via build template volumes. In this example, the build step makes the maven .m2 repository directory to be available for the build to attach volumes.  

For this blog demo example you can attach a PersistenceVolumeClaim to mount the .m2 cache to make the subsequent builds faster. An example pvc is available here.

volumes

The optional extra volumes that need to be available during the build. These volumes are available to all build steps. The blog example defines an empty-dir-volume as default volume to hold the .m2 cache.

With enough details about BuildTemplate, we can now deploy the BuildTemplate and the PersistenceVolumeClaim

cd $PROJECT_HOME/build
oc apply -f templates/java-buildah-template.yaml 
oc apply -f m2-pvc.yaml

You can check the deployed build template using the command oc get buildtemplate.build.knative.dev, that should return the name of the template that was created earlier.

Build and Deploy Service

To deploy the greeter service, we will follow the same service based approach as we did in the Part-1 of the serving blog series, but this time the service definition will add some extra information to add build details.

Let’s examine the service-java.yaml, the service configuration section now has two blocks,

  • build
  • revisionTemplate

The revisionTemplate is the same as in Part-1 of the Knative serving blog series with the one important change of adding an annotation: alpha.image.policy.openshift.io/resolve-names: “*” . This annotation allows the Kubernetes Deployments (created via Knative serving) to be able resolve the images from OpenShift ImageStreams. You can refer to documentation to learn more details on how to manage images within OpenShift.

Let’s analyze more on the build section. The build section has the following attributes:

serviceAccount

The ServiceAccount that will be used the run the builder images of steps, this ServiceAccount should have permissions to push the images OpenShift internal docker registry. For this demo we will be using the in built “builder” ServiceAccount which has the required permissions to push images in to default internal Docker Registry. For more details on OpenShift Service Accounts check the documentation.

source

The Source provides the build data e.g. source code that will act as an input to get started with the build process.  As a blog example source we will be using Git as the source, from where the build pulls the code during build process.

template

The template allows you to reference an existing BuildTemplate and use its steps as part of the build process. The blog example template will use the BuildTemplate java-buildah that you deployed earlier. As you observe in the example template, you pass the template parameters using the arguments block.

volumes

The volumes , the blog example volumes we define a PersistenceVolumeClaim that allows you store the .m2 cache in persistence storage and use the same in subsequent builds.

As the build is defined as part of the service configuration, you don’t need a separate step to build the container image from the application sources. Running the following command will build and deploy the service:

cd $PROJECT_HOME
oc apply -f service-java.yaml

A successful service deployment should create a Kubernetes Deployment like “greeter-00001-deployment”,

Now invoking the service via ${PROJECT_HOME}/bin/call.sh will send you a response like “Java Knative on OpenShift”.

NOTE:

If your build fails for any reason, it’s not possible to restart the build. You might need to delete and recreate the build object. As the build is configured as part of the service, you need to execute the following set of commands,

# delete configuration
oc delete configurations.serving.knative.dev greeter
# delete revisions
oc delete revisions.serving.knative.dev greeter
# delete route
oc delete routes.serving.knative.dev greeter
# delete build
oc delete  builds.build.knative.dev greeter
# create the service again 
cd $PROJECT_HOME
oc apply -f service-java.yaml
Categories
News, OpenShift Ecosystem
Tags
, ,