Skip to content

Latest commit

 

History

History
421 lines (337 loc) · 26.4 KB

README.md

File metadata and controls

421 lines (337 loc) · 26.4 KB

Heroku Java Cloud Native Buildpack (CNB) Tutorial

Build a Java (Maven) application image in 5 minutes, no Dockerfile required.

At the end of this tutorial, you'll have a working OCI image of a Java (Maven) application that can run locally. You will learn about the Cloud Native Buildpack (CNB) ecosystem, and how to utilize the pack CLI to build images without the need to write or maintain a Dockerfile.

You can now also use the heroku/java CNB on Heroku Fir generation via the pilot program. See the Getting started on Heroku Fir Dev Center tutorial.

Install the pack CLI

We assume you have docker installed and a working copy of git. Next, you will need to install the CLI tool for building CNBs, pack CLI. If you're on a Mac you can install it via Homebrew:

$ brew install buildpacks/tap/pack

Ensure that pack is installed correctly:

$ pack --version
0.35.1+git-3a22a7f.build-6099

Configure the default pack builder

Once pack is installed, the only configuration you'll need for this tutorial is to set a default builder:

$ pack config default-builder heroku/builder:24
Builder 'heroku/builder:24' is now the default builder

You can view your default builder at any time:

$ pack config default-builder
The current default builder is 'heroku/builder:24'

Note: The heroku/builder:24 supports both amd64 (also known as x86) and arm64 (such as aarch64 used with newer Mac machines) architectures. If needed, you can configure the architecture for docker and pack CLIs using the --platform argument if needed. For example --platform linux/amd64.

What is a builder?

Note

Skip ahead if you want to build the application first and get into the details later. You won't need to know about builders for the rest of this tutorial.

In short, a builder is a delivery mechanism for buildpacks. A builder contains references to base images and individual buildpacks. A base image contains the operating system and system dependencies. Buildpacks are the components that will configure an image to run your application, that’s where the bulk of the logic lives and why the project is called “Cloud Native Buildpacks” and not “Cloud Native Builders.”

You can view the contents of a builder via the command pack builder inspect. For example:

$ pack builder inspect heroku/builder:24 | grep Buildpacks: -m1 -A10
Buildpacks:
  ID                                NAME                               VERSION        HOMEPAGE
  heroku/deb-packages               Heroku .deb Packages               0.0.3          https://github.com/heroku/buildpacks-deb-packages
  heroku/dotnet                     Heroku .NET                        0.1.9          https://github.com/heroku/buildpacks-dotnet
  heroku/go                         Heroku Go                          0.5.0          https://github.com/heroku/buildpacks-go
  heroku/gradle                     Heroku Gradle                      6.0.4          https://github.com/heroku/buildpacks-jvm
  heroku/java                       Heroku Java                        6.0.4          https://github.com/heroku/buildpacks-jvm
  heroku/jvm                        Heroku OpenJDK                     6.0.4          https://github.com/heroku/buildpacks-jvm
  heroku/maven                      Heroku Maven                       6.0.4          https://github.com/heroku/buildpacks-jvm
  heroku/nodejs                     Heroku Node.js                     3.4.0          https://github.com/heroku/buildpacks-nodejs
  heroku/nodejs-corepack            Heroku Node.js Corepack            3.4.0          https://github.com/heroku/buildpacks-nodejs

Note

Your output version numbers may differ.

This output shows the various buildpacks that represent the different languages that are supported by this builder such as heroku/go and heroku/nodejs-engine.

Download an example Java (Maven) application

How do you configure a CNB? Give them an application. While Dockerfile is procedural, buildpacks, are declarative. A buildpack will determine what your application needs to function by inspecting the code on disk.

For this example, we're using a pre-built Java (Maven) application. Download it now:

$ git clone https://github.com/heroku/java-getting-started
$ cd java-getting-started

Verify you're in the correct directory:

$ ls -A
.env
.git
.github
.gitignore
.mvn
LICENSE
Procfile
README.md
app.json
mvnw
mvnw.cmd
pom.xml
src
system.properties

This tutorial was built using the following commit SHA:

$ git log --oneline | head -n1
a583168 Update docs for Fir (#219)

Build the application image with the pack CLI

Now build an image named my-image-name by executing the heroku builder against the application by running the pack build command:

$ pack build my-image-name --path .
===> ANALYZING
Image with name "my-image-name" not found
===> DETECTING
3 of 4 buildpacks participating
heroku/jvm      6.0.4
heroku/maven    6.0.4
heroku/procfile 3.1.2
===> RESTORING
Skipping buildpack layer analysis
===> BUILDING

[Installing OpenJDK 17.0.13]

[Installing Maven]
Maven wrapper detected, skipping installation.

[Executing Maven]
$ ./mvnw -DskipTests clean install
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
[INFO] Scanning for projects...
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-starter-parent/3.3.5/spring-boot-starter-parent-3.3.5.pom
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-starter-parent/3.3.5/spring-boot-starter-parent-3.3.5.pom (13 kB at 185 kB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-dependencies/3.3.5/spring-boot-dependencies-3.3.5.pom
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/springframework/boot/spring-boot-dependencies/3.3.5/spring-boot-dependencies-3.3.5.pom (100 kB at 5.0 MB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/apache/activemq/activemq-bom/6.1.3/activemq-bom-6.1.3.pom
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/activemq/activemq-bom/6.1.3/activemq-bom-6.1.3.pom (7.9 kB at 788 kB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/apache/activemq/artemis-bom/2.33.0/artemis-bom-2.33.0.pom
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/activemq/artemis-bom/2.33.0/artemis-bom-2.33.0.pom (9.6 kB at 1.4 MB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/apache/activemq/artemis-project/2.33.0/artemis-project-2.33.0.pom
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/activemq/artemis-project/2.33.0/artemis-project-2.33.0.pom (64 kB at 5.8 MB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/apache/apache/31/apache-31.pom
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/apache/31/apache-31.pom (24 kB at 2.9 MB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/assertj/assertj-bom/3.25.3/assertj-bom-3.25.3.pom
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/assertj/assertj-bom/3.25.3/assertj-bom-3.25.3.pom (3.7 kB at 461 kB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/io/zipkin/reporter2/zipkin-reporter-bom/3.4.2/zipkin-reporter-bom-3.4.2.pom
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/io/zipkin/reporter2/zipkin-reporter-bom/3.4.2/zipkin-reporter-bom-3.4.2.pom (6.4 kB at 800 kB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/io/zipkin/brave/brave-bom/6.0.3/brave-bom-6.0.3.pom
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/io/zipkin/brave/brave-bom/6.0.3/brave-bom-6.0.3.pom (11 kB at 1.8 MB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/apache/cassandra/java-driver-bom/4.18.1/java-driver-bom-4.18.1.pom
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/cassandra/java-driver-bom/4.18.1/java-driver-bom-4.18.1.pom (5.5 kB at 683 kB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/glassfish/jaxb/jaxb-bom/4.0.5/jaxb-bom-4.0.5.pom
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/glassfish/jaxb/jaxb-bom/4.0.5/jaxb-bom-4.0.5.pom (12 kB at 1.9 MB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/eclipse/ee4j/project/1.0.9/project-1.0.9.pom
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/eclipse/ee4j/project/1.0.9/project-1.0.9.pom (16 kB at 2.3 MB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/apache/groovy/groovy-bom/4.0.23/groovy-bom-4.0.23.pom
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/groovy/groovy-bom/4.0.23/groovy-bom-4.0.23.pom (27 kB at 3.4 MB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/infinispan/infinispan-bom/15.0.10.Final/infinispan-bom-15.0.10.Final.pom
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/infinispan/infinispan-bom/15.0.10.Final/infinispan-bom-15.0.10.Final.pom (18 kB at 1.7 MB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/infinispan/infinispan-build-configuration-parent/15.0.10.Final/infinispan-build-configuration-parent-15.0.10.Final.pom
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/infinispan/infinispan-build-configuration-parent/15.0.10.Final/infinispan-build-configuration-parent-15.0.10.Final.pom (19 kB at 1.2 MB/s)
...
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/velocity/velocity/1.7/velocity-1.7.jar (450 kB at 3.4 MB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/codehaus/plexus/plexus-archiver/4.8.0/plexus-archiver-4.8.0.jar
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/commons-chain/commons-chain/1.1/commons-chain-1.1.jar (90 kB at 652 kB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/apache/commons/commons-compress/1.23.0/commons-compress-1.23.0.jar
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/dom4j/dom4j/1.1/dom4j-1.1.jar (457 kB at 3.3 MB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/com/github/luben/zstd-jni/1.5.5-5/zstd-jni-1.5.5-5.jar
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/oro/oro/2.0.8/oro-2.0.8.jar (65 kB at 447 kB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/codehaus/plexus/plexus-io/3.4.1/plexus-io-3.4.1.jar
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/codehaus/plexus/plexus-archiver/4.8.0/plexus-archiver-4.8.0.jar (224 kB at 1.5 MB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/codehaus/plexus/plexus-i18n/1.0-beta-10/plexus-i18n-1.0-beta-10.jar
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/codehaus/plexus/plexus-i18n/1.0-beta-10/plexus-i18n-1.0-beta-10.jar (12 kB at 74 kB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/apache/maven/shared/maven-dependency-analyzer/1.13.2/maven-dependency-analyzer-1.13.2.jar
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/codehaus/plexus/plexus-io/3.4.1/plexus-io-3.4.1.jar (79 kB at 487 kB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/apache/maven/maven-model/3.2.5/maven-model-3.2.5.jar
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/commons-collections/commons-collections/3.2.2/commons-collections-3.2.2.jar (588 kB at 3.6 MB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/apache/maven/shared/maven-artifact-transfer/0.13.1/maven-artifact-transfer-0.13.1.jar
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/maven/shared/maven-dependency-analyzer/1.13.2/maven-dependency-analyzer-1.13.2.jar (39 kB at 221 kB/s)
[INFO] Downloading from central: https://repo.maven.apache.org/maven2/org/codehaus/plexus/plexus-component-annotations/2.0.0/plexus-component-annotations-2.0.0.jar
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/maven/maven-model/3.2.5/maven-model-3.2.5.jar (161 kB at 898 kB/s)
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/commons/commons-compress/1.23.0/commons-compress-1.23.0.jar (1.1 MB at 5.9 MB/s)
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/apache/maven/shared/maven-artifact-transfer/0.13.1/maven-artifact-transfer-0.13.1.jar (159 kB at 870 kB/s)
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/org/codehaus/plexus/plexus-component-annotations/2.0.0/plexus-component-annotations-2.0.0.jar (4.2 kB at 23 kB/s)
[INFO] Downloaded from central: https://repo.maven.apache.org/maven2/com/github/luben/zstd-jni/1.5.5-5/zstd-jni-1.5.5-5.jar (5.9 MB at 25 MB/s)
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  3.922 s
[INFO] Finished at: 2024-12-18T16:36:34Z
[INFO] ------------------------------------------------------------------------

[Discovering process types]
Procfile declares types -> web
===> EXPORTING
Adding layer 'heroku/jvm:openjdk'
Adding layer 'heroku/jvm:runtime'
Adding layer 'buildpacksio/lifecycle:launch.sbom'
Added 1/1 app layer(s)
Adding layer 'buildpacksio/lifecycle:launcher'
Adding layer 'buildpacksio/lifecycle:config'
Adding layer 'buildpacksio/lifecycle:process-types'
Adding label 'io.buildpacks.lifecycle.metadata'
Adding label 'io.buildpacks.build.metadata'
Adding label 'io.buildpacks.project.metadata'
Setting default process type 'web'
Saving my-image-name...
*** Images (7741bdbc205f):
      my-image-name
Adding cache layer 'heroku/jvm:openjdk'
Adding cache layer 'heroku/maven:repository'
Successfully built image 'my-image-name'

Note

Your output may differ.

Verify that you see “Successfully built image my-image-name” at the end of the output. And verify that the image is present locally:

$ docker image ls --format "table {{.ID}}\t{{.Repository}}\t{{.Tag}}" | grep my-image-name
7741bdbc205f   my-image-name                             latest

What does pack build do?

Note

Skip ahead if you want to run the application first and get into the details later.

When you run pack build with a builder, each buildpack runs a detection script to determine if it should be eligible to build the application. In our case the heroku/java buildpack found a pom.xml file. As a result, the buildpack has enough information to install Java dependencies. You can view a list of the buildpacks used in the build output:

===> DETECTING
3 of 4 buildpacks participating
heroku/jvm      6.0.4
heroku/maven    6.0.4
heroku/procfile 3.1.2
===> RESTORING

After the detect phase, each buildpack will execute. Buildpacks can inspect your project, install files to disk, run commands, write environment variables, and more. You can see some examples of that in the output above. For example, the Java buildpack installs dependencies from the pom.xml automatically:

$ ./mvnw -DskipTests clean install

If you’re familiar with Dockerfile you might know that many commands in a Dockerfile will create a layer. Buildpacks also use layers, but the CNB buildpack API provides for fine grained control over what exactly is in these layers and how they’re composed. Unlike Dockerfile, all images produced by CNBs can be rebased. The CNB api also improves on many of the pitfalls outlined in the satirical article Write a Good Dockerfile in 19 'Easy' Steps.

Use the image

Even though we used pack and CNBs to build our image, it can be run with your favorite tools like any other OCI image. We will be using the docker command line to run our image.

By default, images will be booted into a web server configuration. You can launch the app we just built by running:

$ docker run -it --rm --env PORT=5006 -p 5006:5006 my-image-name
Picked up JAVA_TOOL_OPTIONS: -XX:MaxRAMPercentage=80.0 -Dfile.encoding=UTF-8
  _    _                _
 | |  | |              | |
 | |__| | ___ _ __ ___ | | ___   _
 |  __  |/ _ \ '__/ _ \| |/ / | | |
 | |  | |  __/ | | (_) |   <| |_| |
 |_|  |_|\___|_|  \___/|_|\_\\__,_|

:: Built with Spring Boot :: 3.3.5

2024-12-18T16:36:43.733Z  INFO 1 --- [           main] c.heroku.java.GettingStartedApplication  : Starting GettingStartedApplication v1.0.0-SNAPSHOT using Java 17.0.13 with PID 1 (/workspace/target/java-getting-started-1.0.0-SNAPSHOT.jar started by heroku in /workspace)
2024-12-18T16:36:43.737Z  INFO 1 --- [           main] c.heroku.java.GettingStartedApplication  : No active profile set, falling back to 1 default profile: "default"
2024-12-18T16:36:44.827Z  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 5006 (http)
2024-12-18T16:36:44.840Z  INFO 1 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2024-12-18T16:36:44.841Z  INFO 1 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.31]
2024-12-18T16:36:44.880Z  INFO 1 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2024-12-18T16:36:44.882Z  INFO 1 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1071 ms
2024-12-18T16:36:45.014Z  INFO 1 --- [           main] o.s.b.a.w.s.WelcomePageHandlerMapping    : Adding welcome page template: index
2024-12-18T16:36:45.369Z  INFO 1 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port 5006 (http) with context path '/'
2024-12-18T16:36:45.388Z  INFO 1 --- [           main] c.heroku.java.GettingStartedApplication  : Started GettingStartedApplication in 2.059 seconds (process running for 2.515)

Now when you visit http://localhost:5006 you should see a working web application:

Screenshot of http://localhost:5006/

Don't forget to stop the docker container when you're done.

Here's a quick breakdown of that command we just ran:

  • docker run Create and run a new container from an image.
  • -it Makes the container interactive and allocates a TTY.
  • --rm Automatically remove the container when it exits.
  • --env PORT=5006 Creates an environment variable named PORT and sets it to 5006 this is needed so the application inside the container knows what port to bind the web server.
  • -p 5006:5006 Publishes a container's port(s) to the host. This is what allows requests from your machine to be received by the container.
  • my-image-name The name of the image you want to use for the application.

So far, we've downloaded an application via git and run a single command pack build to generate an image, and then we can use that image as if it was generated via a Dockerfile via the docker run command.

In addition to running the image as a web server, you can access the container's terminal interactively. In a new terminal window try running this command:

$ docker run -it --rm my-image-name bash

Now you can inspect the container interactively. For example, you can see the files on disk with ls:

$ ls -A
.env
.git
.github
.gitignore
.mvn
LICENSE
Procfile
README.md
app.json
build_output.txt
mvnw
mvnw.cmd
pom.xml
src
system.properties
target

And anything else you would typically do via an interactive container session.

Image structure under the hood

Note

Skip this section if you want to try building your application with CNBs and learn about container structure later.

If you’re an advanced Dockerfile user you might be interested in learning more about the internal structure of the image on disk. You can access the image disk interactively by using the bash docker command above.

If you view the root directory / you’ll see there is a layers folder. Every buildpack that executes gets a unique folder:

$ docker run --rm my-image-name "ls /layers"
config
heroku_jvm
sbom

Individual buildpacks can compose multiple layers from their buildpack directory. For example you can see that java binary is present within that buildpack layer directory:

$ docker run --rm my-image-name "which java"
/layers/heroku_jvm/openjdk/bin/java

OCI images are represented as sequential modifications to disk. By scoping buildpack disk modifications to their own directory, the CNB API guarantees that changes to a layer in one buildpack will not affect the contents of disk to another layer. This means that OCI images produced by CNBs are rebaseable by default, while those produced by Dockerfile are not.

We saw before how the image booted a web server by default. This is accomplished using an entrypoint. In another terminal outside of the running container you can view that entrypoint:

$ docker inspect my-image-name | grep '"Entrypoint": \[' -A2
            "Entrypoint": [
                "/cnb/process/web"
            ],

From within the image, you can see that file on disk:

$ docker run --rm my-image-name "ls /cnb/process/"
web

While you might not need this level of detail to build and run an application with Cloud Native Buildpacks, it is useful to understand how they’re structured if you ever want to write your own buildpack.

Try CNBs out on your application

So far we've learned that CNBs are a declarative interface for producing OCI images (like docker). They aim to be no to low configuration and once built, you can interact with them like any other image.

For the next step, we encourage you to try running pack with the Heroku builder against your application and let us know how it went. We encourage you to share your experience by opening a discussion and walking us through what happened:

  • What went well?
  • What could be better?
  • Do you have any questions?

We are actively working on our Cloud Native Buildpacks and want to hear about your experience. The documentation below covers some intermediate-level topics that you might find helpful.

Configuring your web process with the Procfile

Most buildpacks rely on existing community standards to allow you to configure your application declaratively. They can also implement custom logic based on file contents on disk or environment variables present at build time.

The Procfile is a configuration file format that was introduced by Heroku in 2011, you can now use this behavior on your CNB-powered application via the heroku/procfile, which like the rest of the buildpacks in our builder is open source. The heroku/procfile buildpack allows you to configure your web startup process.

This is the web entry in the getting started guide's Procfile:

web: java -jar target/java-getting-started-1.0.0-SNAPSHOT.jar

By including this file and using heroku/procfile buildpack, your application will receive a default web process. You can configure this behavior by changing the contents of that file.