0018 — Habilitação GraalVM native-image do auth¶
Status: Aprovado · Responsável: Gustavo Madruga · Atualizado em: 2026-09-11 · Decidido em: 2026-08-19
Atualização (reconciliação — modelo de build convergiu): esta decisão descreve o build native via
dockerBuildNative(pluginio.micronaut.application/wolfi) como O método. A casa depois convergiu para o modelo Docker: umDockerfile.nativehand-written na raiz (buildernative-image-community:25i2-ol8→./gradlew nativeCompile -PnativeCI→ runtimeubuntu:24.04), buildado e publicado pela CI (jobbuild_nativedo.github/workflows/pipeline.yml, tagsv*) emfonte.xadm.biz/xadm/central-backend:native-amd64, que o Coolify puxa via Build Pack Docker Image (decisão central 0022 §Topologia). O que segue abaixo vale — perfil de otimização (-Os/-Ob,--gc=serial,-march=compatibility), o gate e2e, os achados de reflect-config e/health/liveness— só a ferramenta de build mudou (dedockerBuildNativeparanativeCompiledentro doDockerfile.native). O histórico abaixo é preservado.
Contexto¶
O auth roda como fat JAR na JVM (Temurin 25). É o SPOF do ecossistema (emissão/validação de
JWT do PowerSync, login pAbast/Firebase, broker da Central) e um container always-on no Coolify —
onde a RAM da JVM (~300–500 MB) pesa no custo e o boot na casa dos segundos alarga a janela de
indisponibilidade em restart/rollback. GraalVM native-image resolve os três: memória ~5–10× menor,
boot em ms, e alinha o auth ao padrão native já piloteado na casa (bi-transporte-xls).
O risco: AOT quebra o que depende de reflection/resource em runtime, e num broker isso só aparece
no path exercitado. As superfícies temidas eram: AWS SDK v2 S3Presigner (reflection pesada),
json-smart do Nimbus (JWKS/JOSE), resources do Flyway (db/migration/*.sql) e o
SentryAppender do logback. Firebase foi confirmado sem Admin SDK (validação é Nimbus +
JWKS via HttpClient), o que já reduziu a superfície.
Decisão¶
Habilitar o build native via o plugin io.micronaut.application 5.0.0 (que aplica o
native-build-tools transitivamente — o accessor graalvmNative {} não é gerado no Kotlin DSL, daí
configure<GraalVMExtension>). Perfil de otimização: -PnativeQuick → -Ob (build rápido pro
loop de dev/e2e) e, sem a flag, -Os (imagem menor, prod), sempre com --gc=serial. CE 25
não tem G1/PGO/build-report (Oracle-only). Build off-host (dockerBuildNative); o Coolify puxa
a imagem pronta.
O gate de adoção é o e2e native (etc/tests/native/e2e-central-backend/, irmão do piloto): sobe a imagem
native em Docker e exercita as 3 superfícies de risco — login pAbast → JWT → verificação da
assinatura, validação de Firebase ID token (via stub de JWKS), e presign S3 full-path. Nenhum
reflect-config é adivinhado em massa: a lista sai do loop (roda → quebra → registra → repete).
Consequências¶
- A conversão foi quase de graça. Das superfícies temidas, só o
SentryAppenderprecisou de reflect-config manual —META-INF/native-image/br.com.xadm.integracao/central/reflect-config.json, herdado do piloto (o logback instancia o appender por reflexão no boot). Emenda 2026-09-11: a cópia registrava só a classe, e no native o<options>dologback.xmlera ignorado em silêncio (evento semenvironment); a hint passou a vir daxadm-comum-web0.9.1, com as três entradas (appender,SentryOptions,Level.valueOf), e a cópia local saiu. AWS SDK v2, json-smart e Flyway não precisaram de config manual: - AWS SDK v2 (
s3:2.44.13): a reachability-metadata embarcada é suficiente — oS3Presignerassina SigV4 (X-Amz-Algorithm=AWS4-HMAC-SHA256) em native sem config. Era o maior risco previsto. - json-smart (Nimbus): o JWKS (
{"keys":[...]}comalg=RS256) serializa em native; JWT RS256 é emitido e a assinatura verifica (openssl dgst -verify= Verified OK). - Flyway:
io.micronaut.flyway.StaticResourceFeatureregistra as migrations automaticamente — as 9 migrations aplicam no boot native. - JCA é stock JDK:
SunRsaSign(RSA/SHA256withRSA) +SunJCE(AES-256-GCM doAesCipher, HmacSHA256 do SigV4). Sem BouncyCastle, sem provider custom a registrar. - Métricas medidas (imagem
-Ob): boot/healthem ~1 s, RSS ~43 MiB, imagem 214 MB. - Novo seam de produção (baixo risco):
auth.firebase-jwks-url(AUTH_FIREBASE_JWKS_URL, default = Google). Existe porque a imagem native é black-box e não expõe o seam in-process (FirebaseTokenValidatorService.jwksUrl) que os testes JVM usam; o e2e native aponta o JWKS para um stub. Default preserva o comportamento de produção. - Paridade
/health/liveness(achado do rollout canário): sob AOT o grupo liveness default fica sem indicador (o detector de deadlock viaThreadMXBeannão reporta) →/health/liveness=UNKNOWNno native vsUPna JVM (todo o resto é idêntico). Não quebra deploy (/healthusa os indicadores JDBC/disk e ficaUP; UNKNOWN é 200, não DOWN), mas rompe a paridade e enganaria um probe que checastatus==UP. Fix: umLivenessHealthIndicator@Liveness @Singletonexplícito (bean compile-time → native-safe, sem reflect-config) que reportaUPnos dois runtimes. Descartado registrar oThreadMXBean/java.lang.managementno reflect-config (reintroduz reflection AOT por um ganho marginal num broker stateless). Emenda 2026-09-11: o indicador saiu quando o/healthvirou o flat da lib (e708fdb/6f4138b, ago/2026, com o management desligado) — este app não expõe mais/health/liveness. - O
DockerfileJVM permanece — é o lado JVM do rollout canário (ver runbook).
Alternativas consideradas¶
- Adivinhar reflect-config em massa (AWS SDK, json-smart, Flyway) antes do e2e: descartado — teria inflado a config com entradas desnecessárias; o e2e provou que nenhuma era precisa.
- Cortar 100% pra native sem canário: inaceitável num SPOF — ver o runbook (rollout canário JVM+native, collapse só após validação em produção).
-Ostambém no loop de dev: mais lento por iteração;-PnativeQuick/-Obacelera o loop e o perfil final de prod (-Osvs-Ob) fica decidido ao medir tamanho/boot no corte.