September heeft ons de release van Java 17 gegeven, en daarmee ook veel nieuwe features. Er zijn tal van goede redenen om te upgraden naar JDK 17, onder andere ook omdat het de nieuwe LTS-versie is, maar veel ontwikkelaars en bedrijven twijfelen nog of wachten nog even af met het migreren van hun Java 11 applicatie. Of ze zijn door hun tooling momenteel nog gelimiteerd tot JDK 11, al dan niet omdat hun build pipeline het afdwingt. Wat kan je in zo’n situatie doen? En is er een manier om alvast een kleine stap te nemen richting Java 17?
Java 17 en Docker
Java is backwards compatible, wat betekent dat bestaande applicaties perfect op een nieuwere JVM kunnen draaien zonder verdere wijzigingen. De meest voor de hand liggende eerste stap is dan ook om je huidige Java 11 applicatie te houden zoals die is, maar om deze te deployen op JRE 17. Op vlak van ontwikkeling verandert er dus niets, aangezien je nog steeds compileert met JDK 11, maar je geniet wel van de voordelen die de nieuwste runtime te bieden heeft.
We gaan er hier even van uit dat we willen deployen op een container-gebaseerd platform zoals Kubernetes. Daarnaast kiezen we er ook voor om enkel de runtime mee te nemen in de container image. Door niet vanaf een volledige JDK base image te vertrekken zijn applicaties kleiner, waardoor ze ook minder kwetsbaarheden bevatten en bijgevolg veiliger zijn. In een JDK image zit namelijk veel tooling die potentieel misbruikt kan worden door hackers of malware. Bij Java 16 of eerder kon je makkelijk een standaard JRE base image gebruiken van Adoptium/OpenJDK, zoals met de instructie hieronder die je aan het begin van je Dockerfile zet.
FROM openjdk:11-jre-slim
Voor Java 17 zijn er echter geen Adoptium/OpenJDK images beschikbaar die enkel de JRE bevatten, waardoor je container image onnodig groot wordt en waardoor het risico groter is dat er kwetsbaarheden aanwezig zijn. De oplossing hiervoor is zelf een image bouwen die slechts het minimale bevat dat je absoluut nodig hebt. We willen dus eigenlijk nog een stap verder gaan dan de base image die bijvoorbeeld voor JRE 11 bestaat. Gelukkig bestaat er sinds Java 9 een makkelijke manier om selectief een JRE samen te stellen: JLink.
JRE 17 maken met JLink
Maar wat is JLink precies? JLink is een command line tool die sinds Java 9 aanwezig is in de JDK. Het laat je toe een eigen JRE op te bouwen vanaf nul, waarbij je specifieert welke modules je nodig hebt. Je maakt dus als het ware een subset van een volledige JRE.
Laten we vooraleer te beginnen eerst nog even kort samenvatten wat we precies willen bereiken. We willen een container image die zo klein mogelijk is, die onze eigen JRE 17 bevat en die zo min mogelijk binaries/libraries beschikbaar stelt die potentieel misbruikt kunnen worden. Alpine is om die reden een goede kandidaat om te gebruiken als base image/platform voor onze custom Docker image. Alpine is net als Ubuntu een Linux distro, maar Alpine legt de focus op het OS zo veilig mogelijk houden. We beginnen dus vanaf een minimaal, veilig OS.
De volgende stap is onze eigen JRE samenstellen. Dit proces kan je volgen in de code snippet hieronder. Om de JRE op te bouwen, beginnen we vanaf een Eclipse Temurin JDK image, waarin we enkele utilities installeren zodat we het jlink commando zonder problemen kunnen uitvoeren. Vervolgens geven we mee welke modules we nodig hebben en plaatsen we het uiteindelijke resultaat onder /java-runtime. Dit vormt de eerste fase van onze multi-stage Docker build.
FROM eclipse-temurin:17-alpine AS jre-build RUN apk add binutils # Create a custom Java runtime using JLink RUN $JAVA_HOME/bin/jlink \ --add-modules java.base,java.desktop,java.instrument,java.logging,java.management,java.management.rmi,java.naming,java.net.http,java.prefs,java.rmi,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.unsupported,jdk.crypto.ec \ --strip-debug \ --no-man-pages \ --no-header-files \ --compress=2 \ --output /java-runtime FROM alpine:3.14 ENV JAVA_HOME=/opt/java/openjdk ENV PATH "${JAVA_HOME}/bin:${PATH}" COPY --from=jre-build /java-runtime $JAVA_HOME ADD target/spring-app-executable.jar /opt/app/app.jar EXPOSE 8080 ENTRYPOINT java $JAVA_OPTS -jar /opt/app/app.jar CMD []
Bij de volgende FROM instructie stappen we over naar onze echte productie-image, waarin enkel Alpine als basis OS zit. Na het zetten van een aantal environment variables, kopiëren we de Java 17 runtime van de tussentijdse build-image naar de finale productie-image. Vervolgens voegen we onze Java applicatie nog toe. Deze hebben we eerder gecompileerd met JDK 11 via Maven en bijgevolg is deze beschikbaar onder de target directory. We geven aan dat de applicatie bereikbaar is op poort 8080 en configureren het commando dat uitgevoerd moet worden bij het opstarten van de container.
Conclusie
Met het voorbeeld hierboven bouw je zelf een JRE die je kan gebruiken om een Spring Boot applicatie in een Docker container aan te bieden. We moeten er echter wel bij vermelden dat we relatief veel modules toevoegen, omdat een standaard Spring Boot webapplicatie al vanaf het begin veel modules nodig heeft. Het is belangrijk om voor jouw toepassing af te wegen welke modules relevant zijn en welke je nu echt nodig hebt. Als je deze principes goed volgt, ben je alvast een stap voor op de rest als het neerkomt op het aanbieden van een lightweight en veilige applicatie. Want zelfs al gebruik je de nieuwe features van Java 17 (nog) niet, is het alsnog een goed idee om je runtime up to date te houden. Laat ons tenslotte niet vergeten dat er elke dag nieuwe kwetsbaarheden gevonden worden en dat het de taak van ontwikkelaars is om hier correct mee om te gaan.