13 Kasım 2022 Pazar

Multi-staged Docker + layertools ve SpringBoot

Giriş
Burada amaç üretilen fat jar'ı katmanlara bölmek ve bunları layer olarak Docker image'a kopyalamak. Açıklaması şöyle
This uber jar can easily reach 400MB or more. This means that for every new build, even for the simplest code change in our application:

- A new 400MB layer will be created
- The layer will be uploaded to your OCI registry
- When Kubernetes pulls the latest image to the node that runs the container, the entire 400 MB layer will need to be pulled.

This will happen on every single code change and rebuild of the image. In reality, only a very small sliver of compiled code has changed. Dockerfile treats each new line as a new layer, so it would make a lot more sense to put the third-party dependencies in their own layer and have our custom code in its own layer. Spring boot provides a way to explode the jar into layers from 2.3 onwards.
Fat jar'ı katmanlara bölmek için komut şöyle. Eğer sadece listelemek istersek extract yerine list yaparız
RUN java -Djarmode=layertools -jar /home/app/target/*.jar extract
Ortaya çıkan layer'lar şöyle
Dependencies — the Spring Boot and other frameworks’ release-based dependencies. These only change when we upgrade to a Spring Boot version or the third-party framework version.
Spring Boot Loader — this is the code that loads our Spring Boot app into the JVM to manage the bean lifecycle, among other things. This also rarely changes.
Snapshot Dependencies — these dependencies are changing much more often. It’s possible that on each new build it would require us to pull the latest snapshot. As a result, this layer is the closest to our application code.
Application — This is our application code from src/main/java (in the case of maven).
Eğer custom layer yaratmak istersek layers.xml dosyası tanımlarız. Şöyle yaparız
<layers xmlns="http://www.springframework.org/schema/boot/layers" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/boot/layers
  https://www.springframework.org/schema/boot/layers/layers-2.7.xsd">
  <application>
    <into layer="spring-boot-loader">
      <include>org/springframework/boot/loader/**</include>
    </into>
    <into layer="application" />
  </application>
  <dependencies>
    <into layer="snapshot-dependencies">
      <include>*:*:*SNAPSHOT</include>
    </into>
    <into layer="custom-dependencies">
      <include>com.company:*</include>
    </into>
    <into layer="dependencies" />
  </dependencies>
  <layerOrder>
    <layer>dependencies</layer>
    <layer>spring-boot-loader</layer>
    <layer>snapshot-dependencies</layer>
    <layer>custom-dependencies</layer>
    <layer>application</layer>
  </layerOrder>
</layers>
Şöyle yaparız
<plugin>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-maven-plugin</artifactId>
  <configuration>
    <excludes>
      <exclude>
        <groupId>org.projectlombok</groupId>
        <artifacdId>lombok</artifacdId>
      </exclude>
    </excludes>
    <layers>
      <enabled>true</enabled>
      <configuration>${project.basedir}/src/layers.xml</configuration>
    </layers>
  </configuration>
</plugin>
Örnek
Şöyle yaparız
#Multi Stage Docker Build

#STAGE 1 - Build the layered jar
#Use Maven base image 
FROM maven-3.8.3-openjdk-17 AS builder
COPY src /home/app/src
COPY pom.xml /home/app
#Build an uber jar
RUN mvn -f /home/app/pom.xml package
WORKDIR /home/app/target
#Extract the uber jar into layers
RUN java -Djarmode=layertools -jar /home/app/target/*.jar extract

#STAGE 2 - Use the layered jar to run Spring Boot app
#Use OpenJDK17 base image
FROM openjdk:17-alpine
USER root
#Copy individual layers one by one 
COPY --from=builder /home/app/target/dependencies/ ./
#Add this to fix a bug which happens during sequential copy commands
RUN true
COPY --from=builder /home/app/target/spring-boot-loader/ ./
RUN true
COPY --from=builder /home/app/target/snapshot-dependencies/ ./
RUN true
COPY --from=builder /home/app/target/custom-dependencies/ ./
RUN true
COPY --from=builder /home/app/target/application/ ./
#Expose port on which Spring Boot app will run
EXPOSE 8080
#Switch to non root user
USER 1001
#Start Spring Boot app
ENTRYPOINT ["java","org.springframework.boot.loader.JarLauncher"]
Örnek - adoptopenjdk/maven-openjdk11
Şöyle yaparız
# STAGE 1
FROM adoptopenjdk/maven-openjdk11 as builder
WORKDIR application
COPY mvnw .
COPY .mvn .mvn
COPY pom.xml .
COPY src src
RUN ./mvnw install -DskipTests
RUN java -Djarmode=layertools -jar target/rest-service-0.0.1-SNAPSHOT.jar extract

# STAGE 2
FROM adoptopenjdk:11-jre-hotspot
WORKDIR application
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
Açıklaması şöyle. Bu yöntemin özelliği Docker cache özelliğinin de devreye girmesiyle, derleme zamanının çok hızlanması
This approach uses a maven image in Stage 1. Therefore, you do not need to have maven installed on your machine to run the maven build commands.

This approach uses a jre base image in Stage 2. Furthermore it uses layers as opposed to a fat jar.

There are four layers consisting of the following:
  • java dependencies
  • Spring Boot loader
  • snapshot dependencies
  • application code

These four dependencies are organized in order of least likely to change. Naturally, the application code is ordered last since it changes the most often. Therefore, when application code changes, only the java files that have changed need to be compiled and added to the new image during a build. The docker build engine can use the cached layers created from previous build to reuse the other layers. 
Örnek -  openjdk:17-alpine
Şöyle yaparız. Burada dockerfile-maven-plugin kullanılıyor.  maven install komutu ile önce jar yapılandırılıyor. En son olarak ta Docker dosyası çalıştırılıyor
FROM openjdk:17-alpine as builder

WORKDIR application

ARG JAR_FILE=target/*.jar

COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

FROM openjdk:17-alpine
WORKDIR application
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
Örnek - eclipse-temurin:17.0.4_8-jdk-alpine
Şöyle yaparız
# Making here use of a docker multi-stage build
# https://docs.docker.com/develop/develop-images/multistage-build/

# Build-time container
FROM eclipse-temurin:17.0.4_8-jdk-alpine as builder
ARG JAR_FILE
WORKDIR application
COPY $JAR_FILE application.jar
COPY build_application.sh ./
RUN sh build_application.sh

# Run-time container
FROM alpine:3.16.2

ARG APP_NAME=app
ARG USER=exie
ARG GROUP=party
ARG LOG_FOLDER=/srv/app/logs

ENV APP=$APP_NAME \
    JAVA_HOME=/opt/java \
    PATH="${JAVA_HOME}/bin:${PATH}"

## Adding programs for operation
RUN apk update && \
    apk --no-cache add dumb-init curl jq jattach && \
    rm -rf /var/cache/apk/*

WORKDIR /srv/$APP

COPY run_application.sh /etc

RUN addgroup -S $GROUP && \
    adduser -S -D -H $USER -G $GROUP && \
    mkdir -p $LOG_FOLDER && \
    chgrp $GROUP $LOG_FOLDER && \
    chmod g+rwx $LOG_FOLDER && \
    chmod +x /etc/run_application.sh

## Application-specif created JRE
COPY --from=builder /opt/java-runtime $JAVA_HOME

## Spring Boot Layers
COPY --from=builder application/spring-boot-loader/ ./
COPY --from=builder application/dependencies/ ./
COPY --from=builder application/snapshot-dependencies/ ./
COPY --from=builder application/application/ ./

USER $USER
ENTRYPOINT ["/usr/bin/dumb-init", "--"]
CMD ["/etc/run_application.sh"]
build.sh şöyle
#!/bin/sh

JAR_FILE=application.jar
if [ ! -f "$JAR_FILE" ]; then
    echo "build.error: application jar is missing!"
    exit 1
fi

jar xf application.jar
REQUIRED_JAVA_MODULES="$(jdeps \
                            --print-module-deps \
                            --ignore-missing-deps \
                            --recursive \
                            --multi-release 17 \
                            --class-path="./BOOT-INF/lib/*" \
                            --module-path="./BOOT-INF/lib/*" \
                            ./application.jar)"

if [ -z "$REQUIRED_JAVA_MODULES" ]; then
    echo "build.error: required java modules are not determined!"
    exit 1
fi

jlink \
  --no-header-files \
  --no-man-pages \
  --compress 2 \
  --add-modules "${REQUIRED_JAVA_MODULES}" \
  --output /opt/java-runtime

java -Djarmode=layertools -jar application.jar extract

exit 0
run_application.sh şöyle
#!/bin/sh

JAVA_OPTS="-Dfile.encoding=UTF-8 -Duser.timezone=UTC -XX:NativeMemoryTracking=summary -XX:+HeapDumpOnOutOfMemoryError"
PORT="8080"

cd /srv/"$APP" || exit
/opt/java/bin/java $JAVA_OPTS org.springframework.boot.loader.JarLauncher --bind 0.0.0.0:$PORT



Hiç yorum yok:

Yorum Gönder