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-appcd 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/[email protected]: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/[email protected]: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/[email protected]: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.