Stark & Wayne

Investigating kpack - continuously updating Docker images with cloud native buildpacks

Docker images don't grow on trees, but you shouldn't buy them from Etsy either.

What I mean is, you don't want your company running on bespoke artisan Docker images based on source code and upstream dependencies that you can't reproduce 50 times a day, and can't keep continually updated and secure for the next 10 years.

Future You does not want artisan Etsy docker images. Future You wants a you to use a build system that will still exist in 10 years.

Cloud Native Buildpacks are part of the answer - their heritage from Heroku and Clouid Foundry means they are already almost a decade old, and almost guaranteed to still be being maintained and secure a decade from now. This is critical to the hopes, dreams, and happiness of Future You.

kpack is a Kubernetes native system to automatically, continuously convert your applications or build artifacts into runnable Docker images.

As a tribute to kpack being Kubernetes native, in lieu of me knowing what that means, I will use the word native a lot in this article. English is my native language.

Starting with pack

Before we get to kpack, let's visit the pack CLI from which kpack derives its name.

Future You will be celebrating that you built all your Docker images to combine your Git repositories and the latest Cloud Native Buildpacks.

Current You will do this using the pack CLI.

An example walk-thru of this simple process is at https://buildpacks.io/docs/app-journey/.

git clone https://github.com/buildpack/sample-java-app
cd sample-java-app
pack build myapp
docker run --rm -p 8080:8080 myapp

The pack CLI will start a process upon your Docker daemon that automatically discovers the dependencies required for your application to build it (Java & Maven), and to run it (Java). You didn't need any of these dependencies on your local machine, only pack. Fabulous.

But where will you run pack build  for your own production applications, and what will trigger pack build to run when new Git commits are pushed?

Good questions. You could setup a bespoke CI system to watch your Git repos, watch for updates to buildpacks, and run pack build automatically.

Or you could run kpack, configure it for each application Git repo, and walk away forever.

Getting Started with kpack on Kubernetes

kpack uses the same CNB lifecycle system as the pack CLI, combined with the ability to watch for changes in both the source Git repository, and the upstream buildpacks. If anything changes then your application is re-built and a new Docker image is created.

Excellent, let's get started.

Install v0.0.3 of kpack into your Kube cluster:

kubectl apply -f <(curl -L https://github.com/pivotal/kpack/releases/download/v0.0.3/release.yaml)

It installs many CRDs, so you know it's good:

$ kubectl api-resources --api-group build.pivotal.io
NAME              SHORTNAMES                    APIGROUP           NAMESPACED   KIND
builders          cnbbuilder,cnbbuilders,bldr   build.pivotal.io   true         Builder
builds            cnbbuild,cnbbuilds,bld        build.pivotal.io   true         Build
images            cnbimage,cnbimages            build.pivotal.io   true         Image
sourceresolvers                                 build.pivotal.io   true         SourceResolver

Download kpack project for the sample YAML files and the logs CLI (currently kpack does not use go modules, so am installing into $GOPATH):

git clone https://github.com/pivotal/kpack \
   $GOPATH/src/github.com/pivotal/kpack
cd $GOPATH/src/github.com/pivotal/kpack
dep ensure
go install ./cmd/logs

Ok, we have kpack running "natively" (we don't know what that word means) in Kubernetes, and have a logs command ready to stream some build logs later on.

Building the first application

A pack Builder is a collection of Cloud Native Buildpacks. One of these Builders already exists and is constantly updated with latest buildpacks, which in turn maintain the latest secure versions of all dependencies. All these wonder buildpacks are included in the Docker image cloudfoundry/cnb.

We need to tell kpack which upstream Builder image we want to use.

To be fair, you personally don't know what Builder image you want to use, and kpack 0.0.3 does not create a default Builder, so you need to create it, even though you don't know what it is. But perhaps this Builder should have just been created for you when you installed kpack? Anyway, today you need to create a Builder resource to point to the upstream Docker image that contains all the magical buildpacks.

Let's apply the sample Builder which will work with our sample Java applications just nicely:

$ kubectl apply -f samples/builder.yaml

$ kubectl get builds,images,builders,sourceresolvers
NAME                                      AGE
builder.build.pivotal.io/sample-builder   3s

Create a service account for your docker registry and git host. The various samples files assume the ServiceAccount is called service-account, and references secrets for a Git host and a Docker image registry. In the example below I describe my GitHub and Docker Hub registry basic auth secrets.

---
apiVersion: v1
kind: Secret
metadata:
  name: basic-docker-user-pass
  annotations:
    build.pivotal.io/docker: index.docker.io
type: kubernetes.io/basic-auth
stringData:
  username: drnic
  password: ...
---
apiVersion: v1
kind: Secret
metadata:
  name: basic-git-user-pass
  annotations:
    build.pivotal.io/git: https://github.com
type: kubernetes.io/basic-auth
stringData:
  username: drnic
  password: ....
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: service-account
secrets:
  - name: basic-docker-user-pass
  - name: basic-git-user-pass

Apply these secrets the Kubnetes native way with kubectl apply -f my-service-account.yml.

Java/Spring applications can be built from either source code or from a pre-build JAR. Let's do it with a JAR file first, hosted natively on the Internet, with the pre-drafted samples/image_from_blob_url.yaml YAML file.

If you are using a public Docker Hub account then you and I do not have permissions to create sample/image-from-jar, as specified in the sample file. You need to update the YAML file to edit the image name from sample/image-from-jar to <you>/kpack-image-from-jar.

...
spec:
  tag: drnic/kpack-image-from-jar

Apply the new Image and kpack will automatically commence creating the new Docker image, using a Java buildpack.

$ kubectl apply -f samples/image_from_blob_url.yaml
image.build.pivotal.io/sample created

$ kubectl get builds,images,builders,sourceresolvers
NAME                                          IMAGE   SUCCEEDED
build.build.pivotal.io/sample-build-1-xnkq6           Unknown

NAME                            LATESTIMAGE   READY
image.build.pivotal.io/sample                 Unknown

NAME                                      AGE
builder.build.pivotal.io/sample-builder   4m

NAME                                            AGE
sourceresolver.build.pivotal.io/sample-source   4s

Watching the image being built with buildpacks

To tail the logs, use the ./cmd/logs helper app previously installed above as logs:

logs -image sample

The output is similar to our pack build myapp command earlier. This time it is natively running on Kubernetes.

{"level":"info","ts":1566860410.5345762,"logger":"fallback-logger","caller":"creds-init/main.go:40","msg":"Credentials initialized.","commit":"002a41a"}
source-init:main.go:261: Successfully downloaded storage.googleapis.com/build-service/sample-apps/spring-petclinic-2.1.0.BUILD-SNAPSHOT.jar in path "/workspace"
2019/08/26 23:00:35 Unable to read "/root/.docker/config.json": open /root/.docker/config.json: no such file or directory
Trying group 1 out of 6 with 14 buildpacks...
======== Results ========
skip: Cloud Foundry Archive Expanding Buildpack
pass: Cloud Foundry OpenJDK Buildpack
skip: Cloud Foundry Build System Buildpack
pass: Cloud Foundry JVM Application Buildpack
pass: Cloud Foundry Apache Tomcat Buildpack
pass: Cloud Foundry Spring Boot Buildpack
pass: Cloud Foundry DistZip Buildpack
skip: Cloud Foundry Procfile Buildpack
skip: Cloud Foundry Azure Application Insights Buildpack
skip: Cloud Foundry Debug Buildpack
skip: Cloud Foundry Google Stackdriver Buildpack
skip: Cloud Foundry JDBC Buildpack
skip: Cloud Foundry JMX Buildpack
pass: Cloud Foundry Spring Auto-reconfiguration Buildpack
Cache '/cache': metadata not found, nothing to restore
Analyzing image 'index.docker.io/drnic/kpack-image-from-jar@sha256:4ede3a534f5de34372edf4eb026ef784aaf1c7a45e63a6e597083326a37be699'
Writing metadata for uncached layer 'org.cloudfoundry.openjdk:openjdk-jre'
Writing metadata for uncached layer 'org.cloudfoundry.springautoreconfiguration:auto-reconfiguration'

Cloud Foundry OpenJDK Buildpack 1.0.0-M9
  OpenJDK JRE 11.0.3: Reusing cached layer

Cloud Foundry JVM Application Buildpack 1.0.0-M9
  Executable JAR: Contributing to layer
    Writing CLASSPATH to shared
  Process types:
    executable-jar: java -cp $CLASSPATH $JAVA_OPTS org.springframework.boot.loader.JarLauncher
    task:           java -cp $CLASSPATH $JAVA_OPTS org.springframework.boot.loader.JarLauncher
    web:            java -cp $CLASSPATH $JAVA_OPTS org.springframework.boot.loader.JarLauncher

Cloud Foundry Spring Boot Buildpack 1.0.0-M9
  Spring Boot 2.1.6.RELEASE: Contributing to layer
    Writing CLASSPATH to shared
  Process types:
    spring-boot: java -cp $CLASSPATH $JAVA_OPTS org.springframework.samples.petclinic.PetClinicApplicatio
    task:        java -cp $CLASSPATH $JAVA_OPTS org.springframework.samples.petclinic.PetClinicApplicatio
    web:         java -cp $CLASSPATH $JAVA_OPTS org.springframework.samples.petclinic.PetClinicApplicatio

Cloud Foundry Spring Auto-reconfiguration Buildpack 1.0.0-M9
  Spring Auto-reconfiguration 2.7.0: Reusing cached layer
Reusing layers from image 'index.docker.io/drnic/kpack-image-from-jar@sha256:4ede3a534f5de34372edf4eb026ef784aaf1c7a45e63a6e597083326a37be699'
Reusing layer 'app' with SHA sha256:f640054e9917dc79f4d1c60d8c649032d4156a91b7a3b047e03cbbe3bb21f596
Reusing layer 'config' with SHA sha256:d4a0ae6271b134dd22f162c48b456abdae0c853c90adfe0d43734be09fa0c728
Reusing layer 'launcher' with SHA sha256:2187c4179a3ddaae0e4ad2612c576b3b594927ba15dd610bbf720197209ceaa6
Reusing layer 'org.cloudfoundry.openjdk:openjdk-jre' with SHA sha256:b4c9e176f3e59c28939bcbdf3cd8d8bcbd25dd396cffc831c50400bda14c8498
Reusing layer 'org.cloudfoundry.jvmapplication:executable-jar' with SHA sha256:4504416ffcfe48c04b303f209a71360ef054d759b7d5b7deae53d34542c066a2
Reusing layer 'org.cloudfoundry.springboot:spring-boot' with SHA sha256:84f04b234d761615aa79ea77b691fe6d2cee0f7921cc28d1d52eadd84774fab7
Reusing layer 'org.cloudfoundry.springautoreconfiguration:auto-reconfiguration' with SHA sha256:41658755805c0452025f24e92ea9c26f736c0661c478e8cd69f5d4b6bf9280b9
*** Images:
      drnic/kpack-image-from-jar - succeeded
      index.docker.io/drnic/kpack-image-from-jar:b1.20190826.225838 - succeeded

*** Digest: sha256:d936cb02755bc835018ba9283b763a1095856b4ef533ed1bac90ddb450dc82ca
Caching layer 'org.cloudfoundry.jvmapplication:executable-jar' with SHA sha256:4504416ffcfe48c04b303f209a71360ef054d759b7d5b7deae53d34542c066a2
Caching layer 'org.cloudfoundry.springboot:spring-boot' with SHA sha256:84f04b234d761615aa79ea77b691fe6d2cee0f7921cc28d1d52eadd84774fab7

Watching kpack-controller logs

If the logs command does nothing, perhaps there is an error in the kpack controller which is attempting to orchestrate your image build.

To watch the kpack controller logs try out this:

kubectl logs -n kpack \
   $(kubectl get pod -n kpack | grep Running | head -n1 | awk '{print $1}') \
   -f

Maybe you'll see the following error:

... {"error": "serviceaccounts \"service-account\" not found"}

You have forgotten create your secrets and the wrapper service-account ServiceAccount, from above. Once these are created, the kpack-controller will automatically resume the buildpack sequence.

Docker image created

Once the Image has been built successfully, the LATESTIMAGE attribute is updated to reflect its status in the Docker Registry:

$ kubectl get images sample
NAME     LATESTIMAGE                                                        READY
sample   index.docker.io/drnic/kpack-image-from-jar@sha256:d936cb02755bc...   True

You can see the resulting image at https://hub.docker.com/r/drnic/kpack-image-from-jar/tags

Building an application from its Git repository

Let's create a new Image that will target a public Git repository containing a simple Spring application. This example is the same as our pack run myapp example – building the application image from source code – though the source code is fetched from a Git repository rather than from the local machine.

Create samples/kpack-image-from-git.yml, and remember to change spec.tag to a Docker image you can push to your Registry.

apiVersion: build.pivotal.io/v1alpha1
kind: Image
metadata:
  name: kpack-image-from-git
spec:
  tag: drnic/kpack-image-from-git
  builderRef: sample-builder
  serviceAccount: service-account
  source:
    git:
      url: https://github.com/buildpack/sample-java-app.git
      revision: master

Our Image will use the Git credentials (not required for this public Git repo) from service-account to fetch the repo, and the Docker Registry credentials to push the resulting Docker image.

To create the Image and watch the buildpack process in action:

kubectl apply -f samples/kpack-image-from-git.yml
logs -kubeconfig ~/.kube/config -image kpack-image-from-git

This time we see half of Maven being downloaded as the buildpack sequence first creates the JAR, and then creates the Docker image with everything necessary for our application to run in any Docker, Kubernetes, or Cloud Foundry environment that supports Docker images.

The resulting Image is again visible in the target Docker Registry. I created mine in the public Docker Hub at https://hub.docker.com/r/drnic/kpack-image-from-git/tags

Running the Docker image

Whilst we used kpack-on-kubernetes to create the Docker image, we can now use our Docker image anywhere that makes us happy.

For example, in Docker itself. Like the old days.

$ docker run -p 8080:8080 -e PORT=8080 drnic/kpack-image-from-git
Unable to find image 'drnic/kpack-image-from-git:latest' locally
latest: Pulling from drnic/kpack-image-from-git
...
Status: Downloaded newer image for drnic/kpack-image-from-git:latest
    |'-_ _-'|       ____          _  _      _                      _             _
    |   |   |      |  _ \        (_)| |    | |                    | |           (_)
     '-_|_-'       | |_) | _   _  _ | |  __| | _ __    __ _   ___ | | __ ___     _   ___
|'-_ _-'|'-_ _-'|  |  _ < | | | || || | / _` || '_ \  / _` | / __|| |/ // __|   | | / _ \
|   |   |   |   |  | |_) || |_| || || || (_| || |_) || (_| || (__ |   < \__ \ _ | || (_) |
 '-_|_-' '-_|_-'   |____/  \__,_||_||_| \__,_|| .__/  \__,_| \___||_|\_\|___/(_)|_| \___/
                                              | |
                                              |_|

:: Built with Spring Boot :: 2.1.3.RELEASE
...
2019-08-26 23:24:15.605  INFO 1 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 29 ms

We exposed the application on port 8080, so visit http://localhost:8080/ to see the running sample app!

What happens now?

You've got what you always wanted – an always-up-to-date Docker image that contains your latest source code, combined with the latest, most secure dependencies.

If you're running your application on Docker, then ensure your application now uses the new image.

If you're running your application on Kubernetes, then ensure your pods now uses the new image.

If you're running your application on Cloud Foundry, then cf push again.

cf push kpack-app -o drnic/kpack-image-from-git --random-route

Good times, the native way.

Thanks

Thanks to Stephen Levine and Matthew McNew for repairing some factual inaccuracies and recent fixes.