[maven] build container image with Buildpacks

สำหรับวันนี้พอดีสงสัยได้ไปส่อง Repo อื่นๆ แล้วเวลาทำ Container Image มันต้องมี Dockerfile แต่มา Project นึง (เพิ่งได้เข้ามาดู เพราะคนเดิมออกไป) เลยอ๋อ นอกจากทำ Dockerfile แล้วมันสามารถกำหนด Config ในตัว Maven หรือ gradle ได้ด้วยนะ เลยมา Recap ลง Blog ไว้นิดหน่อยทำ Second Brain อิอิ

Cloud Native Buildpacks

เป็น Project ที่ทาง Pivotal กับ Heroku ทำขึ้นมา เพื่อให้การทำ Container Image สะดวกขึ้น และได้ส่งต่อ CNCF ดูแลต่อ โดยเจ้า Buildpacks มันเข้ามาช่วยลดภาระในการมาจัดการดูแลตัว Dockerfile ของแต่ละ EndPoint ถ้ามีเยอะก็ดูแลลำบาก และยังช่วย Optimize ให้ในตัวด้วย รวมถึงเป็น Container Image ที่เข้ากับมาตรฐาน OCI ด้วย

แล้วที่นี้ใน maven หรือ gradle มันพร้อมใช้อยู่แล้วด้วย โดยใน Blog นี้จะเน้นไปในส่วนของ maven ครับ

Set up the project

ลองสร้าง Project ด้วย Spring Initializr + Spring Web เพิ่มเข้าไปเป็น Dependency และทำ Controller เพื่ออ่าน version ของ app

import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/status")
public class ApplicationStatusController {
    @Value("${api.version}")
    private String apiVersion;

    @GetMapping
    public Status getStatus() {
        return new Status(this.apiVersion, "running");
    }

    class Status {
        private String apiVersion;
        private String status;

        public Status(String apiVersion, String status) {
            this.apiVersion = apiVersion;
            this.status = status;
        }

        public String getApiVersion() {
            return apiVersion;
        }

        public String getStatus() {
            return status;
        }
    }
}

จากนั้นกำหนด application.properties ใน folder resource เพิ่ม property api.version=1.0 เข้าไป

สร้าง Container Image โดยใช้ buildpacks

ใน maven หรือ gradle รองรับตัว buildpacks อยู่แล้ว สำหรับใน maven ให้ลองหา section build ได้เลยครับ มี Code ปรมาณนี้

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

ปกติแล้ว เราสามารถ Run mvn spring-boot:build-image หรือ mvnw.cmd spring-boot:build-image ได้เลย มันจะไปอ่านในส่วน artifactId ของ maven project

แต่ถ้าเจอ Error failed: Unable to parse name "dsInsuranceApi". Image name must be in the form

[ERROR] Failed to execute goal org.springframework.boot:spring-boot-maven-plugin:3.4.5:build-image (default-cli) on project dsInsuranceApi: Execution default-cli of goal org.springframework.boot:spring-boot-maven-plugin:3.4.5:build-image failed: Unable to parse name "dsInsuranceApi". Image name must be in the form '[domainHost:port/][path/]name', with 'path' and 'name' containing only [a-z0-9][.][_][-] -> [Help 1]
[ERROR]
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.

แสดงว่าชื่อ artifactId เอาไปใช้เป็น container image ไม่ได้ เงื่อนไขต้อง lowercase letters/ numbers / hyphens (-)/ underscores (_) / periods (.) ตรงนี้สามารถใช้ tag configuration > image > name แทนครับ

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<image>
						<name>ds_insurance/api</name>
					</image>
				</configuration>
			</plugin>
		</plugins>
	</build>

ถ้าลองสั่ง Build อีกรอบ ลองสังเกตุดูมันจะพยายามดึง Base Image มาให้ด้วย

[INFO] Building image 'docker.io/library/ds-insurance-api:0.0.1-SNAPSHOT'
[INFO]
[INFO]  > Pulling builder image 'docker.io/paketobuildpacks/builder-jammy-java-tiny:latest' 100%
[INFO]  > Pulled builder image 'paketobuildpacks/builder-jammy-java-tiny@sha256:cbd65c21bc53832bd81932f9f52e650b275a6aa82fd30702f523907385a4788e'        
[INFO]  > Pulling run image 'docker.io/paketobuildpacks/run-jammy-tiny:latest' for platform 'linux/amd64' 100%

จากนั้น เข้า Process การ Build และทำเก็บข้อมูล รวมถึงการ Inject Config ต่างๆ เข้าไป BP_xx รวมถึงแอบเก็บ Cache ด้วย

INFO]  > Running creator
[INFO]     [creator]     ===> ANALYZING
[INFO]     [creator]     Image with name "docker.io/library/ds-insurance-api:0.0.1-SNAPSHOT" not found
[INFO]     [creator]     ===> DETECTING
[INFO]     [creator]     target distro name/version labels not found, reading /etc/os-release file
[INFO]     [creator]     6 of 26 buildpacks participating
[INFO]     [creator]     paketo-buildpacks/ca-certificates   3.10.1
[INFO]     [creator]     paketo-buildpacks/bellsoft-liberica 11.2.1
[INFO]     [creator]     paketo-buildpacks/syft              2.12.0
[INFO]     [creator]     paketo-buildpacks/executable-jar    6.13.0
[INFO]     [creator]     paketo-buildpacks/dist-zip          5.10.0
[INFO]     [creator]     paketo-buildpacks/spring-boot       5.33.0
[INFO]     [creator]     ===> RESTORING
[INFO]     [creator]     ===> BUILDING
[INFO]     [creator]     target distro name/version labels not found, reading /etc/os-release file
[INFO]     [creator]
[INFO]     [creator]     Paketo Buildpack for CA Certificates 3.10.1
[INFO]     [creator]       https://github.com/paketo-buildpacks/ca-certificates
[INFO]     [creator]       Build Configuration:
[INFO]     [creator]         $BP_EMBED_CERTS                    false  Embed certificates into the image
[INFO]     [creator]         $BP_ENABLE_RUNTIME_CERT_BINDING    true   Deprecated: Enable/disable certificate helper layer to add certs at runtime       
[INFO]     [creator]         $BP_RUNTIME_CERT_BINDING_DISABLED  false  Disable certificate helper layer to add certs at runtime
[INFO]     [creator]       Launch Helper: Contributing to layer
[INFO]     [creator]         Creating /layers/paketo-buildpacks_ca-certificates/helper/exec.d/ca-certificates-helper
[INFO]     [creator]     
[INFO]     [creator]     Paketo Buildpack for BellSoft Liberica 11.2.1
[INFO]     [creator]       https://github.com/paketo-buildpacks/bellsoft-liberica
[INFO]     [creator]       Build Configuration:
[INFO]     [creator]         $BP_JVM_JLINK_ARGS           --no-man-pages --no-header-files --strip-debug --compress=1  configure custom link arguments (--output must be omitted)
[INFO]     [creator]         $BP_JVM_JLINK_ENABLED        false                                                        enables running jlink tool to generate custom JRE
[INFO]     [creator]         $BP_JVM_TYPE                 JRE                                                          the JVM type - JDK or JRE
[INFO]     [creator]         $BP_JVM_VERSION              21                                                           the Java version
[INFO]     [creator]       Launch Configuration:
[INFO]     [creator]         $BPL_DEBUG_ENABLED           false                                                        enables Java remote debugging support
[INFO]     [creator]         $BPL_DEBUG_PORT              8000                                                         configure the remote debugging port
[INFO]     [creator]         $BPL_DEBUG_SUSPEND           false                                                        configure whether to suspend execution until a debugger has attached
[INFO]     [creator]         $BPL_HEAP_DUMP_PATH                                                                       write heap dumps on error to this path
[INFO]     [creator]         $BPL_JAVA_NMT_ENABLED        true                                                         enables Java Native Memory Tracking (NMT)
[INFO]     [creator]         $BPL_JAVA_NMT_LEVEL          summary                                                      configure level of NMT, summary or detail
[INFO]     [creator]         $BPL_JFR_ARGS                                                                             configure custom Java Flight Recording (JFR) arguments
[INFO]     [creator]         $BPL_JFR_ENABLED             false                                                        enables Java Flight Recording (JFR)
[INFO]     [creator]         $BPL_JMX_ENABLED             false                                                        enables Java Management Extensions (JMX)
[INFO]     [creator]         $BPL_JMX_PORT                5000                                                         configure the JMX port
[INFO]     [creator]         $BPL_JVM_HEAD_ROOM           0                                                            the headroom in memory calculation
[INFO]     [creator]         $BPL_JVM_LOADED_CLASS_COUNT  35% of classes                                               the number of loaded classes in memory calculation
[INFO]     [creator]         $BPL_JVM_THREAD_COUNT        250                                                          the number of threads in memory calculation
[INFO]     [creator]         $JAVA_TOOL_OPTIONS                                                                        the JVM launch flags
[INFO]     [creator]         Using Java version 21 extracted from MANIFEST.MF
[INFO]     [creator]       BellSoft Liberica JRE 21.0.7: Contributing to layer
[INFO]     [creator]         Downloading from https://github.com/bell-sw/Liberica/releases/download/21.0.7+9/bellsoft-jre21.0.7+9-linux-amd64.tar.gz     
[INFO]     [creator]         Verifying checksum
[INFO]     [creator]         Expanding to /layers/paketo-buildpacks_bellsoft-liberica/jre
[INFO]     [creator]         Adding 146 container CA certificates to JVM truststore
[INFO]     [creator]         Writing env.launch/BPI_APPLICATION_PATH.default
[INFO]     [creator]         Writing env.launch/BPI_JVM_CACERTS.default
[INFO]     [creator]         Writing env.launch/BPI_JVM_CLASS_COUNT.default
[INFO]     [creator]         Writing env.launch/BPI_JVM_SECURITY_PROVIDERS.default
[INFO]     [creator]         Writing env.launch/JAVA_HOME.default
[INFO]     [creator]         Writing env.launch/JAVA_TOOL_OPTIONS.append
[INFO]     [creator]         Writing env.launch/JAVA_TOOL_OPTIONS.delim
[INFO]     [creator]         Writing env.launch/MALLOC_ARENA_MAX.default
[INFO]     [creator]       Launch Helper: Contributing to layer
[INFO]     [creator]         Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/java-opts
[INFO]     [creator]         Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/jvm-heap
[INFO]     [creator]         Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/link-local-dns
[INFO]     [creator]         Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/memory-calculator
[INFO]     [creator]         Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/security-providers-configurer
[INFO]     [creator]         Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/jmx
[INFO]     [creator]         Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/jfr
[INFO]     [creator]         Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/openssl-certificate-loader
[INFO]     [creator]         Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/security-providers-classpath-9
[INFO]     [creator]         Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/debug-9
[INFO]     [creator]         Creating /layers/paketo-buildpacks_bellsoft-liberica/helper/exec.d/nmt
[INFO]     [creator]       Java Security Properties: Contributing to layer
[INFO]     [creator]         Writing env.launch/JAVA_SECURITY_PROPERTIES.default
[INFO]     [creator]         Writing env.launch/JAVA_TOOL_OPTIONS.append
[INFO]     [creator]         Writing env.launch/JAVA_TOOL_OPTIONS.delim
[INFO]     [creator]     
[INFO]     [creator]     Paketo Buildpack for Syft 2.12.0
[INFO]     [creator]       https://github.com/paketo-buildpacks/syft
[INFO]     [creator]         Downloading from https://github.com/anchore/syft/releases/download/v1.23.0/syft_1.23.0_linux_amd64.tar.gz
[INFO]     [creator]         Verifying checksum
[INFO]     [creator]         Writing env.build/SYFT_CHECK_FOR_APP_UPDATE.default
[INFO]     [creator]
[INFO]     [creator]     Paketo Buildpack for Executable JAR 6.13.0
[INFO]     [creator]       https://github.com/paketo-buildpacks/executable-jar
[INFO]     [creator]       Class Path: Contributing to layer
[INFO]     [creator]         Writing env/CLASSPATH.delim
[INFO]     [creator]         Writing env/CLASSPATH.prepend
[INFO]     [creator]       Process types:
[INFO]     [creator]         executable-jar: java org.springframework.boot.loader.launch.WarLauncher (direct)
[INFO]     [creator]         task:           java org.springframework.boot.loader.launch.WarLauncher (direct)
[INFO]     [creator]         web:            java org.springframework.boot.loader.launch.WarLauncher (direct)
[INFO]     [creator]
[INFO]     [creator]     Paketo Buildpack for Spring Boot 5.33.0
[INFO]     [creator]       https://github.com/paketo-buildpacks/spring-boot
[INFO]     [creator]       Build Configuration:
[INFO]     [creator]         $BPL_JVM_CDS_ENABLED                 false  whether to enable CDS optimizations at runtime
[INFO]     [creator]         $BPL_SPRING_AOT_ENABLED              false  whether to enable Spring AOT at runtime
[INFO]     [creator]         $BP_JVM_CDS_ENABLED                  false  whether to enable CDS & perform JVM training run
[INFO]     [creator]         $BP_SPRING_AOT_ENABLED               false  whether to enable Spring AOT
[INFO]     [creator]         $BP_SPRING_CLOUD_BINDINGS_DISABLED   false  whether to contribute Spring Boot cloud bindings support
[INFO]     [creator]         $BP_SPRING_CLOUD_BINDINGS_VERSION    1      default version of Spring Cloud Bindings library to contribute
[INFO]     [creator]       Launch Configuration:
[INFO]     [creator]         $BPL_SPRING_CLOUD_BINDINGS_DISABLED  false  whether to auto-configure Spring Boot environment properties from bindings      
[INFO]     [creator]         $BPL_SPRING_CLOUD_BINDINGS_ENABLED   true   Deprecated - whether to auto-configure Spring Boot environment properties from bindings
[INFO]     [creator]       Creating slices from layers index
[INFO]     [creator]         dependencies (64.7 MB)
[INFO]     [creator]         spring-boot-loader (459.3 KB)
[INFO]     [creator]         snapshot-dependencies (0.0 B)
[INFO]     [creator]         application (66.6 KB)
[INFO]     [creator]       Spring Cloud Bindings 2.0.4: Contributing to layer
[INFO]     [creator]         Downloading from https://repo1.maven.org/maven2/org/springframework/cloud/spring-cloud-bindings/2.0.4/spring-cloud-bindings-2.0.4.jar
[INFO]     [creator]         Verifying checksum
[INFO]     [creator]         Copying to /layers/paketo-buildpacks_spring-boot/spring-cloud-bindings
[INFO]     [creator]       Web Application Type: Contributing to layer
[INFO]     [creator]         Non-web application detected
[INFO]     [creator]         Writing env.launch/BPL_JVM_THREAD_COUNT.default
[INFO]     [creator]       Launch Helper: Contributing to layer
[INFO]     [creator]         Creating /layers/paketo-buildpacks_spring-boot/helper/exec.d/spring-cloud-bindings
[INFO]     [creator]       4 application slices
[INFO]     [creator]       Image labels:
[INFO]     [creator]         org.opencontainers.image.title
[INFO]     [creator]         org.opencontainers.image.version
[INFO]     [creator]         org.springframework.boot.version
[INFO]     [creator]     ===> EXPORTING
[INFO]     [creator]     Adding layer 'paketo-buildpacks/ca-certificates:helper'
[INFO]     [creator]     Adding layer 'paketo-buildpacks/bellsoft-liberica:helper'
[INFO]     [creator]     Adding layer 'paketo-buildpacks/bellsoft-liberica:java-security-properties'
[INFO]     [creator]     Adding layer 'paketo-buildpacks/bellsoft-liberica:jre'
[INFO]     [creator]     Adding layer 'paketo-buildpacks/executable-jar:classpath'
[INFO]     [creator]     Adding layer 'paketo-buildpacks/spring-boot:helper'
[INFO]     [creator]     Adding layer 'paketo-buildpacks/spring-boot:spring-cloud-bindings'
[INFO]     [creator]     Adding layer 'paketo-buildpacks/spring-boot:web-application-type'
[INFO]     [creator]     Adding layer 'buildpacksio/lifecycle:launch.sbom'
[INFO]     [creator]     Added 5/5 app layer(s)
[INFO]     [creator]     Adding layer 'buildpacksio/lifecycle:launcher'
[INFO]     [creator]     Adding layer 'buildpacksio/lifecycle:config'
[INFO]     [creator]     Adding layer 'buildpacksio/lifecycle:process-types'
[INFO]     [creator]     Adding label 'io.buildpacks.lifecycle.metadata'
[INFO]     [creator]     Adding label 'io.buildpacks.build.metadata'
[INFO]     [creator]     Adding label 'io.buildpacks.project.metadata'
[INFO]     [creator]     Adding label 'org.opencontainers.image.title'
[INFO]     [creator]     Adding label 'org.opencontainers.image.version'
[INFO]     [creator]     Adding label 'org.springframework.boot.version'
[INFO]     [creator]     Setting default process type 'web'
[INFO]     [creator]     Saving docker.io/library/ds-insurance-api:0.0.1-SNAPSHOT...
[INFO]     [creator]     *** Images (0fa27a0042c5):
[INFO]     [creator]           docker.io/library/ds-insurance-api:0.0.1-SNAPSHOT
[INFO]     [creator]     Adding cache layer 'paketo-buildpacks/syft:syft'
[INFO]     [creator]     Adding cache layer 'paketo-buildpacks/spring-boot:spring-cloud-bindings'
[INFO]     [creator]     Adding cache layer 'buildpacksio/lifecycle:cache.sbom'
[INFO] 
[INFO] Successfully built image 'docker.io/library/ds-insurance-api:0.0.1-SNAPSHOT'
[INFO]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  39.951 s
[INFO] Finished at: 2025-05-04T15:26:48+07:00

ถ้าลอง docker images จะพบ image ของเรา

docker images
REPOSITORY                            TAG              IMAGE ID       CREATED         SIZE
ds-insurance-api                      0.0.1-SNAPSHOT   0fa27a0042c5   3 minute ago    314MB

เพียงเท่านี้เราเอา image ไปใช้งานใน docker compose ต่อได้แล้ว อย่างผมใช้วิธีให้มันลบทิ้ง pack และ run compose ขึ้นมาใช้งานครับ

docker compose -f docker/docker-compose.yaml down --remove-orphans
docker rmi best_insurance/api:latest
.\mvnw spring-boot:build-image 
docker compose -f docker/docker-compose.yaml up

ที่เหลือสำหรับการตั้งค่าในเคสอื่นๆ

- Set Specific Java version

กำหนด env > BP_JVM_VERSION ตามตัวอย่าง

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<image>
						<name>ds_insurance/api</name>
						<env>
						    <BP_JVM_VERSION>17</BP_JVM_VERSION>
						    <JAVA_TOOL_OPTIONS>-XX:+UseZGC</JAVA_TOOL_OPTIONS>
						</env>
					</image>
				</configuration>
			</plugin>
		</plugins>
	</build>
- Configure the JVM

กำหนด param ของ JVM เช่น พวก Memory, Garbage Collector, heap size เป็นต้น กำหนดได้จาก JAVA_TOOL_OPTIONS

  • ปกติทำประมาณนี้
JAVA_TOOL_OPTIONS="-XX:+UseZGC -Xms512m" mvn spring-boot:build-image
  • เติมไว้ใน pom ก็ได้นะ
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <environment>
                    <JAVA_TOOL_OPTIONS>-XX:+UseZGC -Xms512m</JAVA_TOOL_OPTIONS>
                </environment>
            </configuration>
        </plugin>
    </plugins>
</build>

จริงมันกำหนดตอน Run ได้นะ เติม -e JAVA_TOOL_OPTIONS="-XX:+UseZGC -Xms512m" หรือ จะลองแปลงเป็น env ใน deployment ของ k8s ก็ได้นะ

...
env:
  - name: JAVA_TOOL_OPTIONS
    value: "-XX:+UseZGC -Xms512m"
...
- Select Builder เพื่อลดขนาด image

มันมีหลายเจ้า หรือ จะ Custom ได้ เช่น Alpaquita Buildpacks based on Liberica JDK (มันเป็น Image เค้าแนะนำว่าสำหรับ Spring ก็ลองได้ตามนี้

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <image>
                    <builder>bellsoft/buildpacks.builder:musl</builder>
                </image>
            </configuration>
        </plugin>
    </plugins>
</build>
- ถ้าจะใช้ AOT / CDS เปิด option ตามนี้ครับ
  • CDS-Class Data Sharing
  • AOT-Ahead-of-time
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <image>
                        <env>
                            <BP_SPRING_AOT_ENABLED>true</BP_SPRING_AOT_ENABLED>
                            <BP_JVM_CDS_ENABLED>true</BP_JVM_CDS_ENABLED>
                        </env>
                    </image>
                </configuration>
                <executions>
                    <execution>
                        <goals>
                            <goal>process-aot</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>

จริงๆ แล้วตัว Build Packs รองรับได้ภาษา Go / dotnet / Node ก็มีนะ ลองดูเพิ่มได้ที่ https://paketo.io/docs/

Reference


Discover more from naiwaen@DebuggingSoft

Subscribe to get the latest posts sent to your email.