Unpack Cloud Native Buildpacks

Buildpacks Re-Imagined

Srinivasa Vasu
Geek Culture
10 min readApr 29, 2020

--

In this blog, let’s look at Cloud Native Buildpacks(CNB) specs and the recently announced open-source implementation of the CNB — paketo-buildpacks.

Need for a Spec

The art of creating a self-contained runnable artifact from the source code isn’t new. It started with Heroku and then the Cloud Foundry community adopted it and took it to the enterprises. The spec for the buildpack technology wasn’t thoroughly defined back then and there was no clear mention of the underlying platform. It led to divergent implementations that are tightly coupled to the underlying platforms. Fast forward to today — the emergence and maturity of the container technology space and the toolsets have enabled the folks from Heroku and Pivotal to bring the best of both worlds on to a new unified convergent spec — Cloud Native Buildpacks (CNB) v3.

As per the official documentation,

CNB defines interactions between a platform, a lifecycle, a number of buildpacks, and an application

A buildpack is software that partially or completely transforms application source code into runnable artifacts.

A lifecycle is software that orchestrates buildpacks and transforms the resulting artifacts into an OCI image.

A platform is software that orchestrates a lifecycle to make buildpack functionality available to end-users such as application developers.

CNB

A well-defined spec takes this proven technology forward with the open-source community. In addition to the spec, CNB does provide infrastructure command line toolings like pack. pack-cli allows selecting multiple reference implementations to test this technology locally and to simulate what would it look like on an actual platform.

paketo-buildpacks

paketo-buildpacks is a reference implementation leveraging the CNB specs to build OCI conformant images for different language runtimes. In this blog, let’s look at a few participating image building modular layers and how we go about customizing those layers.

Outcomes:

  • Build an experimental tiny run-image stack for java apps
  • Build a meta java buildpack by putting together the contributing modular buildpacks
  • Build a custom builder leveraging the meta buildpack and tiny stack
  • Build locally and on a platform, a standard OCI image from the source using the custom builder

Pre-reqs:

Components:

Buildpack — is software (OCI image or self-contained archive) that partially or completely transforms application source code into runnable artifacts.

Builder — is software(OCI image) that leverages lifecycle to orchestrate buildpacks and transforms the resulting artifacts into an OCI image.

Stack — is software(OCI image) that provides build and run time environments that are leveraged during build and distribution phases.

Creating a tiny Stack:

Let's build a tiny run (distroless like) image for java apps to reduce the distribution image size. paketo-buildpacks has multiple stacks like base, tiny and full depending on the language runtimes need.

build base image is used by the Builder during the source compilation. It can have more packages & utilities needed to compile the source. run base image is used for distribution. We don’t need to package the build-time dependencies in the run base image. This reduces the distribution footprint of the final image. We can take one of the existing base images from paketo-buildpacks and build on top of it. Or we can build our base, build, and run images based on ubuntu:bionicimage from scratch. Let's go with the latter. The complete source and script to build the stack images are available at,

https://github.com/srinivasa-vasu/cnb/tree/master/stacks

Let’s look at the run base image,

base_image=humourmind/cnb-base:${version}
run_image=humourmind/cnb-run:${version}
run_base_image=gcr.io/distroless/base
build_image=humourmind/cnb-build:${version}
stack_id="io.buildpacks.stacks.bionic"
cnb_uid=1000
cnb_gid=1000
docker build -t "${base_image}" "$dir/base"
docker build --build-arg "base_image=${base_image}" --build-arg "stack_id=${stack_id}" --build-arg "cnb_uid=${cnb_uid}" --build-arg "cnb_gid=${cnb_gid}" -t "${build_image}" "$dir/build"
docker build --build-arg "base_image=${run_base_image}" --build-arg "stack_id=${stack_id}" --build-arg "cnb_uid=${cnb_uid}" --build-arg "cnb_gid=${cnb_gid}" -t "${run_image}" "$dir/run"

It’s been built with the distroless base image from gcr.io/distroless/base. Let’s look at the docker build-file,

FROM ${base_image} AS tiny-sourceFROM ubuntu:bionic AS builder
COPY --from=tiny-source / /tiny/
RUN cp /bin/bash /tiny/bin/
RUN cp /lib/x86_64-linux-gnu/libtinfo.so.5.9 /tiny/lib/x86_64-linux-gnu/
RUN cp /lib/x86_64-linux-gnu/libtinfo.so.5 /tiny/lib/x86_64-linux-gnu/
RUN cp /lib/x86_64-linux-gnu/libz.so.1 /tiny/lib/x86_64-linux-gnu/
RUN cp /lib/x86_64-linux-gnu/libz.so.1.2.11 /tiny/lib/x86_64-linux-gnu/
RUN cp /lib/x86_64-linux-gnu/libgcc_s.so.1 /tiny/lib/x86_64-linux-gnu/
RUN cp /bin/cat /tiny/bin/
RUN cp /usr/bin/nproc /tiny/usr/bin/
RUN cp /usr/bin/tr /tiny/usr/bin/
RUN echo "cnb:x:${cnb_uid}:${cnb_gid}:cnb:/home/cnb:/bin/bash" >> /tiny/etc/passwd \
&& echo "cnb:x:${cnb_gid}" >> /tiny/etc/group \
&& mkdir -p /tiny/home/cnb
FROM ${base_image}
COPY --from=builder /tiny/ /
ARG stack_id
USER ${cnb_uid}:${cnb_gid}
LABEL io.buildpacks.stack.id="${stack_id}"

The final distribution base image is crafted with just enough utilities/packages needed by the contributing buildpack layers. This reduces the overall size of the final distribution image. This is still *highly experimental*.

Creating a Meta Buildpack:

The complete source of the java meta-buildpack is available at,

https://github.com/srinivasa-vasu/paketo-buildpacks-java

api = "0.2"[buildpack]
id = "paketo-buildpacks/java"
name = "Paketo Java Buildpack"
version = "1.1.0"
homepage = "https://github.com/srinivasa-vasu/paketo-buildpacks-java"
# 1
[[order]]
[[order.group]]
id = 'paketo-buildpacks/bellsoft-liberica'
optional = true
version = '2.3.1'
[[order.group]]
id = 'paketo-buildpacks/gradle'
optional = true
version = '1.1.1'
[[order.group]]
id = 'paketo-buildpacks/maven'
optional = true
version = '1.1.1'

The custom meta-buildpack for java is defined following the CNB spec. paketo-buildpacks/bellsoft-liberica contributes to the JRE runtime layer. In a similar way, other layers contribute accordingly to the final application image.

To package out this buildpack for distribution, package.toml file is required. Following the spec,

[buildpack]
uri = "./buildpacks/"
dependencies = [
{ image = "gcr.io/paketo-buildpacks/bellsoft-liberica:2.3.1" },
{ image = "gcr.io/paketo-buildpacks/gradle:1.1.1" },
{ image = "gcr.io/paketo-buildpacks/maven:1.1.1" },
{ image = "gcr.io/paketo-buildpacks/sbt:1.1.1" },
{ image = "gcr.io/paketo-buildpacks/executable-jar:1.2.1" },
{ image = "gcr.io/paketo-buildpacks/apache-tomcat:1.1.1" },
{ image = "gcr.io/paketo-buildpacks/dist-zip:1.2.1" },
{ image = "gcr.io/paketo-buildpacks/procfile:1.3.1" },
{ image = "gcr.io/paketo-buildpacks/azure-application-insights:1.1.1" },
{ image = "gcr.io/paketo-buildpacks/debug:1.2.1" },
{ image = "gcr.io/paketo-buildpacks/google-stackdriver:1.1.1" },
{ image = "gcr.io/paketo-buildpacks/jmx:1.1.1" },
{ image = "gcr.io/paketo-buildpacks/spring-boot:1.5.1" },
{ image = "gcr.io/paketo-buildpacks/encrypt-at-rest:1.2.1" },
{ image = "gcr.io/paketo-buildpacks/image-labels:1.0.1" }
]

This summarizes the meta buildpack dependencies. Each one is a buildpack distribution image on its own. Use pack-cli to create the standard OCI distribution image,

pack package-buildpack humourmind/paketo-buildpacks-java:1.1.0 --package-config package.toml --publish

This creates the buildpack distribution image humourmind/paketo-buildpacks-java:1.1.0 for the meta-buildpack and publishes it to the registry.

Creating a Builder:

Let’s leverage the tiny stack and buildpack created earlier to build our own custom standard OCI builder image. The complete source is available at,

https://github.com/srinivasa-vasu/cnb/tree/master/paketo-buildpacks

Following the spec,

description = "Ubuntu bionic base image with paketo-buildpacks for Java"[lifecycle]
version = "0.7.3"
[[buildpacks]]
id = "paketo-buildpacks/java"
image = "humourmind/paketo-buildpacks-java@sha256:d1ed1afa05b2bffba43b3befb02a85496019032ba8694f3ad7f78e24079a7180"
# 1
[[order]]
group = [
{ id = "paketo-buildpacks/bellsoft-liberica", version="2.3.1", optional = true },
{ id = "paketo-buildpacks/gradle", version="1.1.1", optional = true },
{ id = "paketo-buildpacks/executable-jar", version="1.2.1", optional = true },
{ id = "paketo-buildpacks/apache-tomcat", version="1.1.1", optional = true },
{ id = "paketo-buildpacks/dist-zip", version="1.2.1", optional = true },
{ id = "paketo-buildpacks/spring-boot", version="1.5.1", optional = true }
]
# 2
[[order]]
group = [
{ id = "paketo-buildpacks/bellsoft-liberica", version="2.3.1", optional = true },
{ id = "paketo-buildpacks/maven", version="1.1.1", optional = true },
{ id = "paketo-buildpacks/executable-jar", version="1.2.1", optional = true },
{ id = "paketo-buildpacks/apache-tomcat", version="1.1.1", optional = true },
{ id = "paketo-buildpacks/dist-zip", version="1.2.1", optional = true },
{ id = "paketo-buildpacks/spring-boot", version="1.5.1", optional = true }
]
[stack]
id = "io.buildpacks.stacks.bionic"
build-image = "humourmind/cnb-build:tiny"
run-image = "humourmind/cnb-run:tiny"

This builder spec encompasses the stack images, lifecycle to orchestrate the buildpack resulting artifacts to an OCI image. Multiple order groups defined participate in the decision cycle and the first matched order will be orchestrated by the lifecycle layer. The entire build lifecycle runs on the earlier created stack build base image.

To build an OCI image,

pack create-builder humourmind/paketo-java-builder-tiny:0.7.3 --builder-config builder.toml

This builder image is sufficient to use with pack-cli to build OCI app images in the local machine. To use it on a platform, a buildpackage distribution layer needs to be defined and created. Use the Dockerfile packaged in the same repo to create the needed distribution layer and push it to the registry.

To create a standard OCI distribution builder image,

docker build . -t humourmind/paketo-java-builder-tiny:0.7.3-cnb -f Dockerfile

and publish it to the registry. Inspect this builder to reason over the BOM and provenance,

pack inspect-builder humourmind/paketo-java-builder-tiny:0.7.3-cnb

Creating an OCI app image from source:

As with any java projects the best place to start with is start.spring.io. Let’s use one of the sample apps created with Spring. The source of the sample app is available at,

https://github.com/srinivasa-vasu/spring-boot-k8s

To build a tiny OCI image from the source code leveraging the custom builder,

pack build <registry>/<repo>/<app_name>:<tag> -B humourmind/paketo-java-builder-tiny@sha256:78d04ac5f05ee21a4bfd84d36573483fd04a2ac97c036db1ffd6065ef5e5e229

This creates the final image with the tiny run stack with layers contributed by the appropriate participating buildpacks. Use docker run to test this app locally.

Relocating the custom builder to a platform:

To test this on a Kubernetes platform, either the open-source K8s native kpack distribution or Tanzu build service — commercial distribution of it can be leveraged. Let’s go with kpack.

Let’s create the necessary k8s objects for stack, store (buildpacks), and builder. The complete source is available at,

https://github.com/srinivasa-vasu/cnb/tree/master/kpack/paketo

Creating a store:

apiVersion: experimental.kpack.pivotal.io/v1alpha1
kind: Store
metadata:
name: paketocnb-java-store-tiny
spec:
sources:
- image: humourmind/paketo-java-builder-tiny@sha256:78d04ac5f05ee21a4bfd84d36573483fd04a2ac97c036db1ffd6065ef5e5e229

This creates the contributing buildpack layers store. Source pulls the meta-buildpack created earlier.

kubectl get storeNAME                        READY
build-service-store True
paketocnb-java-store-tiny True

Creating a stack:

apiVersion: experimental.kpack.pivotal.io/v1alpha1
kind: Stack
metadata:
name: paketocnb-java-stack-tiny
spec:
id: "io.buildpacks.stacks.bionic"
buildImage:
image: "humourmind/cnb-build:tiny@sha256:17ed33b3cc4e926cd941fff3d375d01daef5887266c8e7eea5086a69b6d7d3d4"
runImage:
image: "humourmind/cnb-run:tiny@sha256:9e069f5c33818c2ddec99d16c7501a9775f005e5cb69072f8dc618d6120f5d46"

This creates the stack that contributes to build and run base images using the custom stack created earlier.

kubectl get stackNAME                        READY
build-service-stack True
paketocnb-java-stack-tiny True

Creating a builder:

apiVersion: experimental.kpack.pivotal.io/v1alpha1
kind: CustomClusterBuilder
metadata:
name: paketocnb-java-builder-tiny
spec:
# to relocate the builder image
tag: <registry>/<repo>/<builder>:<tag>
stack: paketocnb-java-stack-tiny
store: paketocnb-java-store-tiny
order:
- group:
- id: paketo-buildpacks/bellsoft-liberica
- id: paketo-buildpacks/gradle
- id: paketo-buildpacks/executable-jar
- id: paketo-buildpacks/apache-tomcat
- id: paketo-buildpacks/dist-zip
- id: paketo-buildpacks/spring-boot
- group:
- id: paketo-buildpacks/bellsoft-liberica
- id: paketo-buildpacks/maven
- id: paketo-buildpacks/executable-jar
- id: paketo-buildpacks/apache-tomcat
- id: paketo-buildpacks/dist-zip
- id: paketo-buildpacks/spring-boot

This creates the custom builder leveraging the store and stack.

kubectl get CustomClusterBuilderNAME                          LATESTIMAGE                                                                                                                                             READY
default <registry>/<repo>/build-service/default-builder@sha256:9e97c76d205b3f78cecb3753f5039a2ac5af7a31ba681ae9d48be0cc68d35fe6 True
paketocnb-java-builder-tiny <registry>/<repo>/<builder>:<tag> True

Follow the kpack documentation to create the appropriate secret and service-account objects for the container registry.

Creating an image spec:

apiVersion: build.pivotal.io/v1alpha1
kind: Image
metadata:
name: k8s-sb-image
spec:
tag: <registry>/<repo>/<app>:<tag>
serviceAccount: <sa>
builder:
name: paketocnb-java-builder-tiny
kind: CustomClusterBuilder
cacheSize: "8Gi"
source:
git:
url: https://github.com/srinivasa-vasu/spring-boot-k8s.git
revision: master

This triggers the final standard OCI image creation from the source leveraging the paketo custom builder.

default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[prepare]: prepare:fetch.go:88: Successfully cloned "https://github.com/srinivasa-vasu/spring-boot-k8s.git" @ "8f63422d11859fa11a3c832ba5a7c07685b96f57" in path "/workspace"
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[analyze]: Restoring metadata for "paketo-buildpacks/bellsoft-liberica:jvmkill" from app image
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[analyze]: Restoring metadata for "paketo-buildpacks/bellsoft-liberica:link-local-dns" from app image
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[analyze]: Restoring metadata for "paketo-buildpacks/bellsoft-liberica:memory-calculator" from app image
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[analyze]: Restoring metadata for "paketo-buildpacks/bellsoft-liberica:security-providers-configurer" from app image
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[analyze]: Restoring metadata for "paketo-buildpacks/bellsoft-liberica:class-counter" from app image
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[analyze]: Restoring metadata for "paketo-buildpacks/bellsoft-liberica:java-security-properties" from app image
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[analyze]: Restoring metadata for "paketo-buildpacks/bellsoft-liberica:jre" from app image
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[analyze]: Restoring metadata for "paketo-buildpacks/bellsoft-liberica:jdk" from cache
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[analyze]: Restoring metadata for "paketo-buildpacks/maven:application" from cache
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[analyze]: Restoring metadata for "paketo-buildpacks/maven:cache" from cache
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[analyze]: Restoring metadata for "paketo-buildpacks/maven:maven" from cache
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[analyze]: Restoring metadata for "paketo-buildpacks/executable-jar:class-path" from app image
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[build]:
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[build]: Paketo BellSoft Liberica Buildpack 2.3.1
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[build]: Set $BP_JAVA_VERSION to configure the Java version. Default 11.*.
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[build]: Set $BPL_HEAD_ROOM to configure the headroom in memory calculation. Default 0.
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[build]: Set $BPL_LOADED_CLASS_COUNT to configure the number of loaded classes in memory calculation. Default 35% of classes.
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[build]: Set $BPL_THREAD_COUNT to configure the number of threads in memory calculation. Default 250.
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[build]: BellSoft Liberica JDK 11.0.7: Reusing cached layer
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[build]: BellSoft Liberica JRE 11.0.7: Reusing cached layer
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[build]: JVMKill Agent 1.16.0: Reusing cached layer
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[build]: Link-Local DNS: Reusing cached layer
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[build]: Memory Calculator 4.0.0: Reusing cached layer
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[build]: Class Counter: Reusing cached layer
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[build]: Java Security Properties: Reusing cached layer
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[build]: Security Providers Configurer: Reusing cached layer
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[build]:
default/i-oud-native-v1-0-cfebbe85ecce1517a1e4dab2a1b8f537075-build-pod[build]: Paketo Maven Buildpack 1.1.1

Finally, it pushes the standard OCI image to the registry on successful completion.

export Reusing layer 'paketo-buildpacks/bellsoft-liberica:security-providers-configurer'                                                                                                                                                 │
│ export Adding layer 'paketo-buildpacks/executable-jar:class-path' │
│ export Adding 1/1 app layer(s) │
│ export Adding layer 'config' │
│ export *** Images (sha256:00e39d0a1cbda2a10f68a84b2f6f49ae68a5ba23733908bf614c5ea1a09338c1): │
│ export <registry>/<repo>/spring-kloud-native:v1.0 │
│ export <registry>/<repo>/spring-kloud-native:v1.0-b4.20200428.110405

References:
CNB
https://buildpacks.io/
paketo-buildpacks
https://paketo.io/
kpack
https://github.com/pivotal/kpack
pack-cli
https://github.com/buildpacks/pack
Source
https://github.com/srinivasa-vasu/cnb
https://github.com/srinivasa-vasu/paketo-buildpacks-java
https://github.com/srinivasa-vasu/cnb/tree/master/stacks
https://github.com/srinivasa-vasu/cnb/tree/master/paketo-buildpacks

--

--

Srinivasa Vasu
Geek Culture

Aspiring Software Artist | views expressed on this blog are solely mine |