Files
Adrien d8bcdce879 Squashed commit of the following:
commit 0d624137c2557c6eeb87020749e4977b821c2b5c
Author: Adrien <adrien.cesaro@proton.me>
Date:   Thu Apr 9 11:55:22 2026 +0200

    backend native image setup
2026-04-09 12:05:02 +02:00

263 lines
9.0 KiB
Markdown

# Research: Native Image Deployment
**Branch**: `005-native-image-deployment` | **Date**: 2026-04-07
## Decision Log
---
### 1. GraalVM Version and Java 25 Support
**Decision**: Use GraalVM JDK 25 (Oracle GraalVM or GraalVM CE 25).
**Rationale**: Oracle GraalVM 25 ships with Java 25 support and includes the
`native-image` tool. GraalVM CE 25 is available via sdkman (`sdk install java 25-graalce`).
Both support the `native-maven-plugin` 0.10.x workflow.
**Alternatives considered**:
- GraalVM 21 with Java 21: would require downgrading `java.version` in pom.xml — rejected
because pom.xml already targets Java 25.
- Mandrel (Red Hat distribution): Red Hat Mandrel 25 may lag behind Oracle GraalVM 25 release;
acceptable fallback but Oracle GraalVM 25 is preferred.
---
### 2. Native Image Build Toolchain
**Decision**: Use `native-maven-plugin` (0.10.x) inside a Maven `native` profile, combined
with Spring Boot's AOT processing via `spring-boot-maven-plugin` `process-aot` execution.
**Rationale**: This is the canonical Spring Boot 4.x native image approach:
1. `mvn -Pnative package` runs AOT → generates source under `target/spring-aot/`
2. `native:compile` compiles the AOT-generated sources + app into a native executable.
The `native-maven-plugin` coordinates with GraalVM `native-image` CLI automatically.
**Alternatives considered**:
- Paketo Buildpacks (`spring-boot:build-image -Pnative`): does not use Jib; rejected because
the user explicitly wants Jib.
- Manual `native-image` CLI: fragile, no Maven lifecycle integration — rejected.
---
### 3. Jib + Native Image Integration
**Decision**: Use `jib-maven-plugin` (3.4.x) with the `jib-native-image-extension-maven`
extension (0.1.0).
**Rationale**: The Jib native image extension packages a pre-built native executable into a
Docker image without requiring a Docker daemon. The workflow is:
```
mvn -Pnative native:compile # step 1: build native executable
mvn -Pnative jib:dockerBuild # step 2: package into Docker image
```
Or combined (with `process-aot` wired in `native` profile):
```
mvn -Pnative package jib:dockerBuild
```
Jib uses the extension to locate the native executable under `target/` and set it as the
container entrypoint.
**Key configuration**:
```xml
<plugin>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-maven-plugin</artifactId>
<version>3.4.5</version>
<configuration>
<from>
<!-- distroless for security; or cgr.dev/chainguard/static for even smaller -->
<image>gcr.io/distroless/base-nossl-debian12</image>
</from>
<pluginExtensions>
<pluginExtension>
<implementation>
com.google.cloud.tools.jib.maven.extension.nativeimage.JibNativeImageExtension
</implementation>
</pluginExtension>
</pluginExtensions>
</configuration>
<dependencies>
<dependency>
<groupId>com.google.cloud.tools</groupId>
<artifactId>jib-native-image-extension-maven</artifactId>
<version>0.1.0</version>
</dependency>
</dependencies>
</plugin>
```
**Alternatives considered**:
- Custom Dockerfile for native image: more control but manual, diverges from Jib workflow —
rejected in favor of Jib as specified.
- `jib:build` (push to registry) vs `jib:dockerBuild` (local daemon): default to
`jib:dockerBuild` for dev; `jib:build` for CI with registry configured.
---
### 4. Docker Base Image for Native Executable
**Decision**: `gcr.io/distroless/base-nossl-debian12` (Debian 12 glibc variant).
**Rationale**: GraalVM native images compiled on Linux link against glibc by default.
Distroless provides glibc without a shell, minimizing attack surface.
Important: the native executable compiled on the host must target the same glibc ABI as the
base image. Since the build is on Linux x86_64 this matches Debian 12's glibc 2.36.
**Alternatives considered**:
- `alpine` (musl libc): requires `--static` or `--libc=musl` native-image flag; more complex —
rejected for simplicity (KISS).
- `gcr.io/distroless/java-base`: includes JVM libs we don't need — rejected (pure native).
- `scratch`: too minimal; requires fully static binary with all libs included — rejected.
---
### 5. Spring AI 2.0.0-M4 Native Image Compatibility
**Decision**: Rely on Spring AI's built-in AOT hints; add manual `RuntimeHints` only for gaps
found during build.
**Rationale**: Spring AI 2.0.0-M4 includes `@NativeHint` registrations for the OpenAI
integration (HTTP client, response DTOs) and pgvector store. Spring Boot's AOT processor
picks these up automatically. M4 milestone has known native image improvements.
**Known gaps** (require manual RuntimeHints):
- `spring-ai-pdf-document-reader`: PDFBox font/resource loading uses `Class.forName()` and
classpath resource scanning. Need to register font resources and PDFBox parser classes.
- `spring-ai-advisors-vector-store`: generally AOT-compatible via Spring AI hints.
**Approach**: Add a `NativeHintsConfig` class annotated with `@ImportRuntimeHints` that
registers PDFBox fonts and any other gaps found during a `-Ob` (quick) native build.
---
### 6. PDFBox Reflection Hints
**Decision**: Register PDFBox font resources and parser classes via a `RuntimeHintsRegistrar`.
**Rationale**: PDFBox 3.x loads fonts from classpath resources (`/org/apache/pdfbox/resources/`)
and uses reflection for CMap/encoding classes. Without hints the native build fails at runtime
with `ClassNotFoundException` or missing resource errors.
**Minimum hints needed**:
```java
hints.resources().registerPattern("org/apache/pdfbox/resources/**");
hints.reflection().registerType(org.apache.pdfbox.pdmodel.font.encoding.GlyphList.class,
MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS);
// Additional classes as discovered during smoke tests
```
---
### 7. AWS SDK v2 Native Image Support
**Decision**: Add `software.amazon.awssdk:aws-crt-client` is NOT needed; instead add
`software.amazon.awssdk:netty-nio-client` or use URL connection client.
Add AWS SDK v2 native image support via `aws-sdk-java-v2-native-bridge` if available,
otherwise register reflection manually.
**Rationale**: AWS SDK v2 2.30.x provides native image support via:
- `software.amazon.awssdk:apache-client` or `url-connection-client` (simpler, fewer reflection
needs than netty)
- The SDK's `SdkPojo` interfaces generate reflection entries; Spring Boot AOT may not cover all.
**Minimum approach**: Switch S3 HTTP client from default (netty) to `url-connection-client`
for simplicity in native mode, register `SdkPojo` subtypes via `RuntimeHints`.
**Alternative**: If S3 is used infrequently and only for figure uploads, mark S3Client as a
conditional bean initialized lazily — reflection issues then surface only on first use (not
startup), making native hints easier to discover iteratively.
---
### 8. Flyway in Native Mode
**Decision**: No special configuration needed; Flyway 10.x has native image support.
**Rationale**: Spring Boot 4.x AOT includes Flyway native hints. PostgreSQL JDBC driver
and Flyway 10 have been tested with GraalVM native. No manual hints required.
---
### 9. Spring Security in Native Mode
**Decision**: No special configuration needed.
**Rationale**: Spring Security 7.x (bundled with Spring Boot 4.x) includes AOT-compatible
configuration. HTTP Basic auth does not require dynamic proxies at runtime.
---
### 10. Build Command Summary
```bash
# Install GraalVM 25
sdk install java 25-graalce
# Build native executable + Docker image (local daemon)
cd backend
mvn -Pnative package jib:dockerBuild
# Or push to registry (CI)
mvn -Pnative package jib:build \
-Djib.to.image=ghcr.io/your-org/ai-teacher-backend:native-latest \
-Djib.to.auth.username=$CI_USER \
-Djib.to.auth.password=$CI_TOKEN
```
---
### 11. Maven Profile Structure
```xml
<profiles>
<profile>
<id>native</id>
<build>
<plugins>
<!-- AOT processing -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>process-aot</id>
<goals><goal>process-aot</goal></goals>
</execution>
</executions>
</plugin>
<!-- Native compile -->
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.10.6</version>
<executions>
<execution>
<id>add-reachability-metadata</id>
<goals><goal>add-reachability-metadata</goal></goals>
</execution>
<execution>
<id>compile</id>
<goals><goal>compile-no-fork</goal></goals>
<phase>package</phase>
</execution>
</executions>
<configuration>
<imageName>ai-teacher-backend</imageName>
<buildArgs>
<buildArg>--initialize-at-build-time=org.slf4j</buildArg>
<buildArg>-H:+ReportExceptionStackTraces</buildArg>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>
</profile>
</profiles>
```