The concept of Buildpacks was first conceived by Heroku in 2011. PaaS platforms like Heroku needed to support applications in multiple languages, which were often built with very similar logic. In January 2018, Pivotal and Heroku co-launched the Cloud Native Buildpacks (CNB) project, which joined the CNCF in October of the same year.
In this blog, I will give an overview of the CNB project and its core components, and then use an example to demonstrate how to use it to build images for Function Mesh.
What does CNB mean for developers and operators?
We know that the container runtime ecosystem today has long been more than a Docker monopoly. The advent of the Open Container Initiative (OCI) has set the standard for the industry, meaning that given an OCI image, any container runtime that implements the OCI standard can use that image properly.
Buildpacks is one such image builder that is able to produce OCI-compliant images. It satisfies the needs of both developers and operators and solves the conflict between the two groups.
The CNB project shields developers from the details of the application building and deployment process. They don’t need to understand and write the code for the runtime environment, or worry about details, such as which operating system to use for the image, the differences in scripts under such operating system, and image size optimization. When using CNB, developers only need to select the appropriate builder image and then provide their source code directory to build the application image.
For the Ops team, they can assemble the application image builder with several Buildpacks (the minimal build unit in CNB) in a lego-like manner to meet various needs. Based on the mechanism between the base runtime environment and the application artifacts (i.e. ABI) in the CNB image, operators can replace the base runtime environment in the application image with a single command when there is a CVE in the image's base runtime environment. They don’t need to rebuild a new image and make any adaptive changes for the new base runtime environment.
Why does Function Mesh need CNB?
Function Mesh is a serverless framework purpose-built for stream processing applications. It brings powerful event-streaming capabilities to your applications by orchestrating multiple Pulsar Functions and Pulsar IO connectors for complex stream processing jobs.
A serverless framework like Function Mesh inevitably needs to provide a way for users to submit their functions when it is working. There are currently two common ways to do this.
Upload the function to the package management service of the Pulsar cluster
Customize the function Docker image
Both approaches involve plenty of repetitive manual operations, including compiling, packaging, and uploading the function code to package management systems, and writing Dockerfiles.
CNB is well suited for scenarios where the build process is constant and has proven to be working on serverless frameworks such as Google Cloud Functions and OpenFunction. Thus, we have reason to believe that CNB will help improve the image building experience of Function Mesh.
Cloud Native Buildpacks consist of the following main components.
Buildpack: The minimal build unit.
Stack: Provides the base runtime environment for the build phase and the application runtime phase.
Lifecycle: A lifecycle management interface abstracted from CNB to guide the entire build process.
Builder: A builder that integrates a Stack and several Buildpacks with a specific build purpose.
Platform: The executor of the interfaces in the lifecycle to meet the user's build requirements.
First, let’s look at these components in detail and how they can work together. Later, I will use an example to demonstrate how to create them.
A Stack entity is composed of two OCI images, namely the build image and the run image. For example, we can use Ubuntu as the base runtime environment for the build, and then run different phases for the application and install the required software.
To build a Java application, typically, the build logic is comprised of the following steps.
Check if there is a Java code file in the target directory (i.e., files with the .java suffix).
Check if there is a pom.xml file in the target directory.
Make sure the necessary compilation tools such as maven are in the PATH.
Run mvn clean install -B -DskipTests to compile and package the application.
Set the entry point for the image to start the application.
A good principle for making a simple Buildpack is to determine the contents of each Buildpack based on the build steps, so now we need to make 5 Buildpacks.
Lifecycle is the most important component of CNB. It is essentially an abstraction and orchestration of the build steps from the source code to the image, and its main phases are listed as follows.
Detect: Checks which Buildpack is to be executed
Build: Executes the build logic in the Buildpack
Analyze: Handles the cached content of the build process
Export: Exports the OCI image
Rebase: Replaces the base runtime environment of the application image
A Builder entity is an OCI image. By aggregating a Stack, several Buildpacks, and a Lifecycle (which does not need to be prepared by the user), and specifying the execution order of these Buildpacks, a builder with a specific build purpose is produced.
After you have the Builder ready, you can use the Platform to apply the Builder to the given source code, complete the execution in the Lifecycle, execute Buildpacks in a given order, and finally build the source code into an image and export it.
Common Platforms include Tekton and CNB's pack-cli.
Building a Java function image with Function Mesh Buildpacks
As I mentioned above, the Stack provides basic building and running environments for an application (in this case, a Java function). It is composed of a build image to construct the build environment and a run image to build application images.
Create the build image
The build image provides the OS environment for the application during the building phase. Note that the Stack ID is io.functionmesh.stack in this example.
In this example, we need a Buildpack to check whether the Java files (with the suffix “.java”) and the required items (e.g. “pom.xml”) exist. If they do exist, we can build the target artifact (usually a “.jar” file) with Maven and move it to /pulsar.
Use the following command to create the Buildpack. Note that the Buildpack ID is functionmesh/java-maven in this example.
buildpack.toml is the configuration file for the Buildpack, which contains the buildpack id, the stack id, and other information.
api = "0.7"
id = "functionmesh/java-maven"
version = "0.0.1"
id = "io.functionmesh.stack"
bin/detect & bin/build
Create two scripts of bin/detect and bin/build. You can find them on this page.
The contents of bin/detect check if the Buildpack can be applied to the source code. In this example, bin/detect will check if the source directory includes .java files, and if so, the script will return true and let the Buildpack be applied to this source.
The contents of bin/build compiles the source code. The script is used to:
Download mvn and jdk tools
Build the package
Clear the source code
A Builder is an image that contains all the necessary components to execute a build.
# Buildpacks to include in builder
uri = "../../buildpacks/java-maven"
# Order used for detection
# This buildpack will display build-time information (as a dependency)
id = "functionmesh/java-maven"
version = "0.0.1"
# Stack that will be used by the builder
id = "io.functionmesh.stack"
# This image is used at runtime
run-image = "fm-stack-java-runner-run:v1"
# This image is used at build-time
build-image = "fm-stack-build:v1"
Another amazing thing about CNB is that when the runtime-runner image needs a patch update (for example, fixing a critical CVE that requires the version number of the runtime-runner image to be changed, like streamnative/pulsar-functions-java-runner:18.104.22.168-patch), you just need to prepare a new runtime image fm-stack-java-runner-run:v1-patch as follows.
Then, use the CNB rebase interface to replace the run image in the function image java-exclamation-function:v1 with the following.
pack rebase java-exclamation-function:v1 --run-image fm-stack-java-runner-run:v1-patch --pull-policy if-not-present
This way, you don't even need to change the function configuration. You just need to restart its workload to apply the function to the place where the function-runner has been replaced.
In the future development of Function Mesh, we plan to integrate CNB in a way that does not add complexity to the project itself, such as providing dedicated CLI tools combined with configurable builders.
More on Apache Pulsar
Pulsar has become one of the most active Apache projects over the past few years, with a vibrant community driving innovation and improvements to the project. Check out the following resources to learn more about Pulsar.
Spin up a Pulsar cluster in minutes with StreamNative Cloud. StreamNative Cloud provides a simple, fast, and cost-effective way to run Pulsar in the public cloud.
Register now for free for Pulsar Summit Asia 2022! Held on November 19th and 20th, this two-day virtual event will feature 36 sessions by developers, engineers, architects, and technologists from ByteDance, Huawei, Tencent, Nippon Telegraph and Telephone Corporation (NTT) Software Innovation Center, Yum China, Netease, vivo, WeChat, Nutanix, StreamNative, and many more.
Tian Fang is a Platform Engineer at StreamNative and a maintainer of Function Mesh and OpenFunction, focusing on cloud-native serverless technologies.