Ecosyste.ms: Awesome

An open API service indexing awesome lists of open source software.

Awesome Lists | Featured Topics | Projects

https://github.com/maeddes/java-and-container

Sample code and instructions for steps through different container image build options.
https://github.com/maeddes/java-and-container

buildpacks docker dockerfile jib mav paketo-buildpack

Last synced: about 15 hours ago
JSON representation

Sample code and instructions for steps through different container image build options.

Awesome Lists containing this project

README

        

= Lab/Walkthrough instructions - Container Builds
:sectnums:
:toc:

image::pics/001-overview.png[Container Build Options,640]

== About

This lab will walk you through steps to build container images with various technologies.

* Slides: https://speakerdeck.com/maeddes/options-galore-from-source-code-to-container-image
* Recording (English): https://www.youtube.com/watch?v=mFamovTQ6Po
* Recording (German): https://www.youtube.com/watch?v=ga8iqQ25lUY
* Carlos Repo: https://github.com/carlosbarragan/java-containers-demo

== Prereqs

Mandatory:

* A Docker environment and Docker CLI https://docs.docker.com/get-docker/
* Pack CLI for Cloud-Native Buildpacks https://buildpacks.io/docs/tools/pack/
* Clone/Download this repo: https://github.com/maeddes/options-galore-container-build

Recommended:

* A Java (21 or later) Development Kit for Java examples, e.g https://adoptopenjdk.net/

Optional:

* Dive tool https://github.com/wagoodman/dive

Links:

* https://home.robusta.dev/blog/stop-using-cpu-limits

=== Validation

Validate docker installation.

[source]
----
docker version
----

Should display output like (version might differ):

----
Client: Docker Engine - Community
Version: 25.0.3
API version: 1.44
...

Server: Docker Engine - Community
Engine:
Version: 25.0.3
API version: 1.44 (minimum version 1.24)
----

Validate Java.

[source]
----
java --version
----

Should display output like (version might differ):

----
openjdk 21.0.7 2023-04-18
OpenJDK Runtime Environment (build 21.0.7+7-Ubuntu-0ubuntu120.04)
OpenJDK 64-Bit Server VM (build 21.0.7+7-Ubuntu-0ubuntu120.04, mixed mode, sharing)
----

== Dockerfile Exercises

=== Set environment and build code

Download/clone the repo and change to the root folder. If you are running in gitpod,codespaces or using devcontainer, you can skip this step.
[source, bash]
----
git clone https://github.com/maeddes/options-galore-container-build
----

Note: Without git CLI you can download the repo as zip file here: https://github.com/maeddes/options-galore-container-build/archive/refs/heads/main.zip
Extract it and change your command line shell to the root folder.

[source, bash]
----
cd options-galore-container-build
----

Build the code:

Change to the Java sample app
[source, bash]
----
cd java
----

Option 1 (with local JDK installed)
[source]
----
./mvnw clean package
----

Validate build artefact (timestamps will of course be different)
[source]
----
ls -ltr ./target/simplecode-0.0.1-SNAPSHOT.jar
----
----
-rw-r--r-- 1 root root 20951064 May 5 11:47 ./target/simplecode-0.0.1-SNAPSHOT.jar
----

=== Classic Dockerfile

image::pics/050-Dockerfile.png[Classic Dockerfile]

Observe contents of Dockerfile-simple-ubuntu

[source]
----
cat Dockerfile-simple-ubuntu
----

----
FROM ubuntu:22.04
RUN apt update && apt install openjdk-21-jre-headless -y
COPY target/simplecode-0.0.1-SNAPSHOT.jar /opt/app.jar
CMD ["java","-jar","/opt/app.jar"]
----

Build first image with this Dockerfile:

[source]
----
docker build -f Dockerfile-simple-ubuntu -t java-app:v-simple-ubuntu .
----

Build images with other predefined base images:

[source]
----
docker build -f Dockerfile-simple-temurin -t java-app:v-simple-temurin .
----

[source]
----
docker build -f Dockerfile-simple-ibm-semeru -t java-app:v-simple-ibm-semeru .
----

Validate images in local repo

[source]
----
docker images
----

----
REPOSITORY TAG IMAGE ID CREATED SIZE
java-app v-simple-ibm-semeru 3a7c058097d9 8 seconds ago 300MB
java-app v-simple-temurin 62c5ca75dad1 32 seconds ago 292MB
java-app v-simple-ubuntu a491383f3f53 2 minutes ago 400MB----
----

Observe build history and differences of the 3 images

[source]
----
docker history java-app:v-simple-ubuntu
docker history java-app:v-simple-temurin
docker history java-app:v-simple-ibm-semeru
----

You will observe different base layers and structure, but always the same top layer:
----
IMAGE CREATED CREATED BY SIZE COMMENT
7209f28736c8 3 minutes ago /bin/sh -c #(nop) CMD ["java" "-jar" "/opt/… 0B
e5385e2e3146 3 minutes ago /bin/sh -c #(nop) COPY file:90a1db2252f31169… 19MB
----

Optional: Use tool "dive" to show detailed history of image:

[source]
----
dive java-app:v-simple-ubuntu
----
[source]
----
dive java-app:v-simple-temurin
----
[source]
----
dive java-app:v-simple-ibm-semeru
----

Usage: ctrl+l (ensure layer changes) ctrl+u (uncheck unmodified) for layer switch

=== Multi-Stage

image::pics/055-Dockerfile-Buildkit-parallel.png[Multi-Stage Dockerfiles]

Build image with Multistage Dockerfile:

[source]
----
docker build -f Dockerfile-multistage-builder -t java-app:v-multistage-builder .
----

This will take a while as all the maven dependencies need to be downloaded.

Validate history:

[source]
----
docker history java-app:v-multistage-builder
----

Explore docker images:

[source]
----
docker images
----

----
REPOSITORY TAG IMAGE ID CREATED SIZE
java-app v-multistage-builder 816512fee0cd 21 seconds ago 291MB
----

Perform a slight modification in the source code which does not affect the behaviour of the application.
You can use the editor 'nano' to execute this:

[source]
----
nano src/main/java/de/maeddes/simplecode/SimplecodeApplication.java
----

Locate the method hello()

[java]
----
@GetMapping("/")
String hello(){

logger.info("Call to hello method on instance: " + getInstanceId());
return getInstanceId()+" Hello, Container people ! ";

}
----

and just add some characters to the method name, e.g.

[java]
----
String helloABC(){
----

And save it using Ctrl+X and confirm with 'Y'.

Now you can repeat the docker build call.

[source]
----
docker build -f Dockerfile-multistage-builder -t java-app:v-multistage-builder .
----

You can observe that all the dependencies will need to get downloaded again. This method does not cache anything.

=== BuildKit

Build with multistage cache option:

image::pics/056-Dockerfile-MountCache.png[Dockerfile with Cache]

[source]
----
docker build -f Dockerfile-multistage-cache -t java-app:v-multistage-cache .
----

Change the code and rebuild:

You can use an editor to change a method name in
----
src/main/java/de/maeddes/simplecode/SimplecodeApplication.java
----
or simply execute

[source]
----
sed -i 's/hello/helloABC/g' src/main/java/de/maeddes/simplecode/SimplecodeApplication.java
----
(Linux)

or

[source]
----
sed -i '' 's/hello/helloABC/g' src/main/java/de/maeddes/simplecode/SimplecodeApplication.java
----
(Mac)

Rebuild and observe faster build through caching:

[source]
----
docker build -f Dockerfile-multistage-cache -t java-app:v-multistage-cache .
----

Observe the history to validate that top layer is still 'monolithic':

[source]
----
docker history java-app:v-multistage-cache
----

Build the code with a layered jar approach:

image::pics/061-considerations.png[Layer considerations for Java]

[source]
----
docker build -f Dockerfile-multistage-layered -t java-app:layered .
----

Display layered state

[source]
----
docker history java-app:layered
----

----
IMAGE CREATED CREATED BY SIZE COMMENT
de2cb7c4be82 8 seconds ago ENTRYPOINT ["java" "org.springframework.boot… 0B buildkit.dockerfile.v0
8 seconds ago COPY application/application/ ./ # buildkit 6.12kB buildkit.dockerfile.v0
8 seconds ago COPY application/snapshot-dependencies/ ./ #… 0B buildkit.dockerfile.v0
8 seconds ago COPY application/spring-boot-loader/ ./ # bu… 245kB buildkit.dockerfile.v0
8 seconds ago COPY application/dependencies/ ./ # buildkit 18.9MB buildkit.dockerfile.v0
----

Finally have a look at the Dockerfile with specific JVM flags:

[source]
----
cat Dockerfile-multistage-layered-jvm-flags
----

in the final line you can see how to apply alternative settings here.

----
ENTRYPOINT ["java","-XX:+UseParallelGC","-XX:MaxRAMPercentage=75","org.springframework.boot.loader.JarLauncher"]
----

== Jib

The following steps show how to build container images with the jib-maven plugin.

image::pics/090-jib.png[Jib from Google]

Again the use of the local maven wrapper (mvnw) will require a local JDK installation.
If it's not present use option 2.

Option 1:
[source]
----
./mvnw compile com.google.cloud.tools:jib-maven-plugin:3.4.4:dockerBuild -Dimage=java-app:jib -Djib.from.image=eclipse-temurin:21-jre
----

In this case the *:dockerBuild* part will instruct the plugin to build to the local docker daemon.
The *-Dimage* parameter will specify the image name tag.

If you have a docker account you can login and push directly to the docker hub using:
(Replace with your own username)

[source]
----
./mvnw compile com.google.cloud.tools:jib-maven-plugin:3.4.4:build -Dimage=/java-app:jib -Djib.from.image=eclipse-temurin:21-jre
----

Another option is to export the image directly to a tar. Use the following command.

[source]
----
./mvnw compile com.google.cloud.tools:jib-maven-plugin:3.4.4:buildTar -Dimage=java-app:jib -Djib.from.image=eclipse-temurin:21-jre
----

You will see an output saying

After that you can import the image into the local registry.

[source]
----
docker load -i target/jib-image.tar
----

----
not showing any more
15bbc04e2cf6: Loading layer [==================================================>] 41.71MB/41.71MB
7f270d883779: Loading layer [==================================================>] 16.82MB/16.82MB
496ff124a7de: Loading layer [==================================================>] 213B/213B
965a8d44c836: Loading layer [==================================================>] 1.345kB/1.345kB
5e91304a655b: Loading layer [==================================================>] 219B/219B
Loaded image: java-app:jib
----

Option 2:

Without local maven you can only perform the tar build and direct import via load.

[source]
----
docker run -it --rm --name my-maven-project -v "$(pwd)":/opt/app -w /opt/app maven:3.6.3-jdk-11 mvn compile com.google.cloud.tools:jib-maven-plugin:3.3.1:buildTar -Dimage=java-app:jib
----

Load the exported tar file as image into the local registry.

[source]
----
docker load -i target/jib-image.tar
----

----
15bbc04e2cf6: Loading layer [==================================================>] 41.71MB/41.71MB
7f270d883779: Loading layer [==================================================>] 16.82MB/16.82MB
496ff124a7de: Loading layer [==================================================>] 213B/213B
965a8d44c836: Loading layer [==================================================>] 1.345kB/1.345kB
5e91304a655b: Loading layer [==================================================>] 219B/219B
Loaded image: java-app:jib
----

Both options - final steps:

Now that you've built and loaded the image into the local registry using one of the options above, inspect the layered structure of the image.

[source]
----
docker history java-app:jib
----

----
IMAGE CREATED CREATED BY SIZE COMMENT
2275828677a8 N/A jib-maven-plugin:3.4.4 1.66kB jvm arg files
N/A jib-maven-plugin:3.4.4 2.46kB classes
N/A jib-maven-plugin:3.4.4 1B resources
N/A jib-maven-plugin:3.4.4 23.1MB dependencies
----

Optional: Perform some small modifications in the code similar to the ones during the Dockerfile exercise.
Re-run the build steps and observe the caching and improved performance.

Note: All of the previous examples referenced the jib plugin directly in the maven call. An alternative (and probably the clean way) to the steps above is to add the plugin to your pom.xml:

The tag in the following xml sets the target image path in the image registry. In our case we are using the local registry and thus just providing the image tag.

You can add the following plugin to your pom.xml
[source]
----

com.google.cloud.tools
jib-maven-plugin
3.4.4


eclipse-temurin:21-jre


java-app:jib-v2.0

----

In this case the invocation looks much simpler.

[source]
----
./mvnw compile jib:dockerBuild
----

The *:build* and *:buildTar* options work accordingly.

It is of course also possible to define custom JVM arguments with Jib. However this is not possible with a plain mvn call.
You also can of course apply these settings not during build time, but when starting the container:

[source]
----
docker run --env JAVA_TOOL_OPTIONS='-XX:+UseParallelGC -XX:MaxRAMPercentage=75' java-app:jib
----

== Cloud-native buildpacks

image::pics/104-buildpacks-flow.png[Cloud-Native Buildpacks]

Access the pack CLI and list the suggest builders. A builder includes the buildpacks and environment that will be used for building and running your app.

[source]
----
pack builder suggest
----

Set a default builder to avoid specifying a builder every time you build. For the examples in this tutorial use the base builder image from Paketo buildpacks.

[source]
----
pack config default-builder paketobuildpacks/builder-jammy-base
----

Now all is set to build the container image using the buildpack. Simply execute:

[source]
----
pack build java-app:pack
----

The first invocation will take a long time. The builder image is big as it contains all the logic plus buildpacks.

After it is downloaded can now observe the output - the so-called bill of materials.
This gives detailed information about the build.

Should display output like:
----
===> ANALYZING
...
===> DETECTING
...
===> RESTORING
===> BUILDING
...
===> EXPORTING
...

Successfully built image java-app:pack^
----

Optimize the build with:

----
pack build java-app:pack-compressed --env BP_JVM_JLINK_ENABLED=true
----

If you want to configure specific JVM settings with Paketo Buildpacks you can extend the call to use alternative configuration:

[source]
----
pack build -e BPE_APPEND_JAVA_TOOL_OPTIONS='-XX:+UseParallelGC -XX:MaxRAMPercentage=75' -e BPE_DELIM_JAVA_TOOL_OPTIONS=' ' java-app:pack
----

Paketo buildpacks can be configured using different for external configuration (Environment Variables, buildpack.yml, Bindings, Procfiles).

Use an environment variable to configure the JVM version installed by the Java Buildpack and build a new version of the container image

[source]
----
pack build java-app:pack-v2.0 --env BP_JVM_VERSION=11
----

Observe the usage of (JDK 11.0.19, JRE 11.0.19) in the BUILDING phase of the output.

Get an overview of the built Images

[source]
----
docker images
----

Using pack it is possible to swap out the underlying OS layers (run image) of an app image with another run image version, without re-building the application.

Rebase app image with a version pinned run image

[source]
----
pack rebase java-app:pack --run-image paketobuildpacks/run:1.3.48-full-cnb
----

Should display output like:

----
1.3.48-full-cnb: Pulling from paketobuildpacks/run
83525de54a98: Already exists
c1dbbbd2a415: Pull complete
283105c565ee: Pull complete
7ead7caf102c: Pull complete
Digest: sha256:005e54c4254bd49fa5b0b55fd7b7f16a2654bc6643963dece1cd03f7a0abce24
Status: Downloaded newer image for paketobuildpacks/run:1.3.48-full-cnb
Rebasing java-app:pack on run image paketobuildpacks/run:1.3.48-full-cnb
Saving java-app:pack...
*** Images (a938edc476a8):
java-app:pack
Rebased Image: a938edc476a85ab53d6aa52a5cc6288c1dffdafd9b3654236cf8b62bbce70a83
Successfully rebased image java-app:pack
----

== Paketo with Spring Boot and Maven

image::pics/108-paketo-springboot.png[Paketo, Spring Boot, Maven]

For a Spring Boot application you can also invoke Paketo Buildpacks directly via maven.

[source]
----
./mvnw spring-boot:build-image -Dspring-boot.build-image.imageName=java-app:paketo
----

After compiling and testing the code within a standard Maven build, the build-image phase appears in the build log, in which you should observe display output like:

----
===> DETECTING
...
===> ANALYZING
...
===> RESTORING
===> BUILDING
...
===> EXPORTING
...
Successfully built image 'docker.io/library/java-app:paketo'
----

Get an overview of the built Images

== Options

You have now completed the core exercise.
Feel free to do some modifications yourself.
Suggestions:
* Edit the pom.xml and alternate the Java version (8,11,21 have been tested).
* Do minor or major code modifications and observe changes
* Use dive to analyze the created images.

(C) Matthias Haeussler. Free for private purposes. (Re)distribution for commercial purposes not allowed without owner permissions.