Cloud Native Buildpacks to unite PaaS and CaaS

Roman Bachmann
9 min readJun 16, 2020

--

Photo by chuttersnap on Unsplash

While Kubernetes developers are still writing Dockerfiles to pack their piece of software into a container, Cloud Foundry developers have already shipped their code into production and let the platform run their code.

How is that possible? And how can I leverage this for my Kubernetes setup? This blog post reflects my personal opinion working as a cloud solution architect at Swisscom, Switzerland’s leading telecoms company and one of its leading IT companies. Swisscom relies heavily on cloud offerings such as Cloud Foundry (PaaS) and Kubernetes (CaaS) for its own workload, and also offers these, among other solutions, as cloud products and services to enterprise customers.

To set the context: I will give you an overview of what buildpacks are, where they come from and how they work. Then I will introduce you to Cloud Native Buildpacks. With that in mind, I will show you how to create a Docker image from your application with the help of Cloud Native Buildpacks and how to deploy that Docker image to Cloud Foundry and Kubernetes.

The shown code-snippets are just excerpts from a demo application. You can access the complete project in this dedicated GitHub repo: github.com/swisscom/blogpost-cnb

What are buildpacks in Cloud Foundry?

Cloud Foundry is a Platform-as-a-Service (PaaS) product and facilitates buildpacks to provide a standardized and robust way to deploy and run software. The concept of buildpacks dates back to 2011 and was originally conceived by Heroku. Over the years, the buildpack’s advantages in terms of security, higher-level abstraction, and automation became more and more apparent to the developer community, resulting in the Cloud Native Buildpacks project initiated by Pivotal and Heroku in 2018.

A buildpack is a sophisticated piece of software that accepts an arbitrary software artifact and tries to run it. There are a lot of buildpacks available, arguably one for every possible language or technology including Java, Go, Node.js, PHP, .NET Core, and static content just to name a few.

Upon pushing an application’s artifact(s) to Cloud Foundry — and without specifying a buildpack — the platform will try every single available buildpack and pick the one which succeeds in running the provided application.

Buildpacks are the glue between software artifacts and runnable containers, as the following figure shows and as described afterwards:

Example of a cf push process in Cloud Foundry

Application developers push compiled artifacts (e.g. for Java apps) or the app’s source-code (e.g. for Node.js apps) via CLI or API.
The resulting package enters the staging phase in which all specified buildpacks are applied. For that, a preconfigured filesystem is created (a so-called stack, e.g. based on Ubuntu 18.04) and populated with the middleware defined by the buildpack(s).
The created droplet, as they are called in Cloud Foundry, gets stored in the platform and is ready to be run. Upon starting, environment variables get injected and the runtime configuration (e.g. memory) is applied to the requested count of instances.

As Cloud Foundry supports running Docker containers too, the application developer can also specify a Docker image to be run. In this case, staging is essentially reduced to downloading the image from a given Docker repository.

What are the advantages of buildpacks?

Buildpacks greatly enhance productivity as the platform takes care of many aspects of a DevOps engineer’s work. For Java, for example, the buildpack will be kept up to date to use the latest versions of the JRE, and you can even choose which vendor’s JRE to use. Also, a memory-calculator ensures all possible memory settings are set properly and according to your configured memory limit. Furthermore, if the Java buildpack detects a Spring application, it will adjust some additional settings there.

But it’s not just the middleware that is taken care of. The DevOps engineer won’t have to patch the OS either, since this is also part of the buildpack. All in all, buildpacks simply abstract everything beneath the application itself, thus boosting the efficiency.

Ok, cool. But are there any disadvantages of buildpacks?

It is important to know that buildpacks are built to run in the platform, i.e. in Cloud Foundry. This makes it harder for local development and to ensure your code behaves exactly the same whether you run it locally or in the cloud. Another slight downside is the fact that it’s hard to ensure you run the exact same version in test and in production, as your artifact is effectively staged twice. If you wait too long to promote your application from test to prod, the odds are that a newer buildpack version uses a different patch-level of the underlying layers. And finally, it’s hard to scan your droplets for vulnerabilities, as market hardly offers any scanners for Cloud Foundry droplets.

Docker solved these limitations and therefore, Docker containers have prevailed as the de facto standard for shipping applications.

What are Cloud Native Buildpacks?

Cloud Native Buildpacks, CNB in short, is a project initiated by Heroku and Pivotal back in 2018. To quote their website:

“Cloud Native Buildpacks embrace modern container standards such as the OCI image format. They take advantage of the latest capabilities of these standards, such as cross-repository blob mounting and image layer “rebasing” on Docker API v2 registries.”

Put simply, it’s the combination of both worlds: OS and runtime are provided by a curated, layered Docker image and enriched with the application by the developer. The resulting Docker image can be run locally as well as in any Docker compatible (cloud) platform.

Simplified pack process

I emphasize layered Docker image because it goes further than simply putting the whole application artifact in one layer: Using the Spring framework, for example, a compiled fat jar would contain all dependencies and the business logic itself as well. With the layered Cloud Native Buildpacks, these are split into different layers since dependencies usually change less often than the application code between two builds resulting in faster builds due to the cached underlying layers. A nice write-up from VMware Tanzu mentions these five advantages of CNBs over traditional buildpacks: Portability, modularity, speed, troubleshooting, and reproducibility.

How can I build my application with the help of Cloud Native Buildpacks?

To create a CNB from your application, you need to have pack installed. Pack uses builders which scan your provided code and apply all suitable buildpacks.

Luckily, there are a couple of builders ready to use which already chain different buildpacks to have them contribute to the resulting image. Based on your need and preferences, you can pick one of the following builders:

$ pack suggest-buildersSuggested builders:Cloud Foundry:     cloudfoundry/cnb:bionic         Ubuntu bionic base image with buildpacks for Java, NodeJS and GolangCloud Foundry:     cloudfoundry/cnb:cflinuxfs3     cflinuxfs3 base image with buildpacks for Java, .NET, NodeJS, Golang, PHP, HTTPD and NGINXCloud Foundry:     cloudfoundry/cnb:tiny           Tiny base image (bionic build image, distroless run image) with buildpacks for GolangHeroku:            heroku/buildpacks:18            heroku-18 base image with buildpacks for Ruby, Java, Node.js, Python, Golang, & PHP
Tip: Learn more about a specific builder with:
pack inspect-builder [builder image]

In this case, I use the cloudfoundry/cnb:cflinuxfs3 builder because it comes closest to what I know from Cloud Foundry.

You can choose whether you want pack to compile your source code or use your provided, compiled artifact. I decided to let pack compile my application so the command is as simple as follows:

Congratulations, you’ve just created a runnable Docker image from your sources using a Cloud Native Buildpack!

You can quickly validate that the image is working as expected by running it locally:

$ docker run — rm -d -p 8080:8080 robachmann/webflux-rest-cnb:0.0.1 
8b28c7c7f026ea73059bc779cb0f38bc826265f4f10cbb196523a71359f8e727
$ http :8080/actuator/health/readiness
HTTP/1.1 200 OK
Content-Length: 15
Content-Type: application/vnd.spring-boot.actuator.v3+json
{
“status”: “UP”
}

To deploy it, you will certainly need to push it to a Docker registry first.

So that was very easy. But can it be done even easier? Yes! Spring developers benefit from tooling-integration introduced in Spring Boot 2.3.0. Using Gradle (or Maven, respectively), creating a CNB from a Spring application is as simple as running ./gradlew bootBuildImage :

As you can see, the Spring developers have already picked a suitable builder knowing which image serves the purpose of running a Spring application the best. It results in a smaller image by removing all the (presumably) unnecessary extra tools:

$ docker images robachmann/webflux-rest-cnb:0.0.1
REPOSITORY TAG IMAGE ID SIZE
robachmann/webflux-rest-cnb 0.0.1 128a146254aa 1.25GB
$ docker images robachmann/webflux-rest-cnb:1.0.0-200615094959
REPOSITORY TAG IMAGE ID SIZE
robachmann/webflux-rest-cnb 1.0.0-200615094959 41cb49057144 268MB

That’s nice because every unnecessary file removed from the resulting image reduces the likelihood of deploying software with (security) vulnerabilities.

Furthermore, as expected from Spring devs, we can overwrite certain defaults. Rather than using latest tag, I want to include the app’s version and a unique timestamp. This is possible by simply providing the custom config in my build.gradle:

bootBuildImage {
imageName = "robachmann/${project.name}:${project.version}-${new Date().format("YYMMddHHmmss")}"
}

I could also overwrite the default builder or instruct it to use a different Java runtime version:

bootBuildImage {
builder = "cloudfoundry/cnb:cflinuxfs3"
environment = ["BP_JVM_VERSION" : "13.0.1"]
}

How can I deploy a CNB to Cloud Foundry?

As mentioned, Cloud Foundry not only runs buildpack based droplets but also Docker containers.

Assuming you have successfully built your image and pushed it to a Docker registry of your choice, you can simply deploy it to your Cloud Foundry space from that registry. If you’re looking for a hosted Cloud Foundry offering, consider Swisscom’s Application Cloud.

An example manifest could look like this:

Note that the services and env sections are specific to this particular application and do not necessarily apply to other apps, e.g. yours ;-)

Now you can push the app using cf push:

How can I deploy a CNB to Kubernetes?

You deploy the Docker image like any other application to a Kubernetes cluster of your choice — I recommend Swisscom’s Kubernetes offering. Simply use this deployment.yml:

As a side note, the values of CPU and memory requests/limits follow the best-practices for Spring applications deployed to Kubernetes, as explained by Neven Cvetkovic in his webcast on Effective Spring on Kubernetes. He recommends setting CPU requests based on a warmed up JVM, not setting CPU limits to burst up to the full worker node’s CPU and to set memory requests equally to memory limits.

Run kubectl apply and check the pod:

$ kubectl apply -f k8s/deployment.yml
deployment.apps/weather-data-client created
$ kubectl get pods
NAME READY STATUS RESTARTS AGE
weather-data-client-77c99dfb84-xs2f9 1/1 Running 0 34s
$ kubectl top pod
NAME CPU(cores) MEMORY(bytes)
weather-data-client-77c99dfb84-xs2f9 5m 176Mi

Voilà, you’ve just deployed the exact same application to Cloud Foundry and Kubernetes with the same OS and and JRE stack!

Conclusion and Outlook

In conclusion, CNBs offer great support for developing and running applications in any possible cloud platform by using state-of-the-art tools and concepts. I see huge potential for the project and it will be tough to argue why to not use CNBs to build and ship your software. You might argue it’s a step back for Cloud Foundry developers as they seemingly need to create CNBs now and can’t rely on the platform handling that anymore. However, for Cloud Foundry, the famous cf push experience is preserved, as the underlying process of staging (i.e. creating buildpacks from source code) will be adjusted to support CNBs transparently for the developer. This is done by the kpack project, which in turn will be used in cf-for-k8s, the evolution of Cloud Foundry based on Kubernetes. Therefore, you can use CNBs and their advantages (run them locally or on other platforms) or simply stick to using the built-in mechanism of Cloud Foundry and let the platform figure out how to run your code.

It’s exciting to see that yet another Cloud Foundry project has just been launched recently: Paketo implements the CNB specification and offers modular and transparent buildpacks while maintaining a vendor-neutral governance process. Given the background of the Cloud Foundry Foundation and the experience of hundreds of thousands of developers using Cloud Foundry buildpacks, this is a project worth following and keeping on your radar. For this demo, I have used Paketo’s Java buildpack. At the time of writing, other popular buildpacks such as the Staticfile or Python buildpack were not yet available from Paketo but have been confirmed to be on their or the community’s roadmap.

Happy packing!

PS: As this blog post is partially about Cloud Foundry, please note the upcoming Cloud Foundry summit EU taking place on October 21-22, 2020. This year’s #CFSummit is dedicated to simplifying the developer experience with many sessions dedicated to using Cloud Foundry on Kubernetes. Check out the schedule now: cloudfoundry.org/events/summit/europe-2020/schedule

PPS: If you prefer to watch me explain this topic, check out the recording of our Cloud Native Computing meetup:

Cloud Native Computing Switzerland Meetup, Cloud Native Buildpacks presentation starts at 1:35:10

--

--