Pular para conteúdo

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 (plugin io.micronaut.application/wolfi) como O método. A casa depois convergiu para o modelo Docker: um Dockerfile.native hand-written na raiz (builder native-image-community:25i2-ol8 → ./gradlew nativeCompile -PnativeCI → runtime ubuntu:24.04), buildado e publicado pela CI (job build_native do .github/workflows/pipeline.yml, tags v*) em fonte.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 (de dockerBuildNative para nativeCompile dentro do Dockerfile.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 SentryAppender precisou 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> do logback.xml era ignorado em silêncio (evento sem environment); a hint passou a vir da xadm-comum-web 0.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 — o S3Presigner assina SigV4 (X-Amz-Algorithm=AWS4-HMAC-SHA256) em native sem config. Era o maior risco previsto.
  • json-smart (Nimbus): o JWKS ({"keys":[...]} com alg=RS256) serializa em native; JWT RS256 é emitido e a assinatura verifica (openssl dgst -verify = Verified OK).
  • Flyway: io.micronaut.flyway.StaticResourceFeature registra as migrations automaticamente — as 9 migrations aplicam no boot native.
  • JCA é stock JDK: SunRsaSign (RSA/SHA256withRSA) + SunJCE (AES-256-GCM do AesCipher, HmacSHA256 do SigV4). Sem BouncyCastle, sem provider custom a registrar.
  • Métricas medidas (imagem -Ob): boot /health em ~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 via ThreadMXBean não reporta) → /health/liveness = UNKNOWN no native vs UP na JVM (todo o resto é idêntico). Não quebra deploy (/health usa os indicadores JDBC/disk e fica UP; UNKNOWN é 200, não DOWN), mas rompe a paridade e enganaria um probe que checa status==UP. Fix: um LivenessHealthIndicator @Liveness @Singleton explícito (bean compile-time → native-safe, sem reflect-config) que reporta UP nos dois runtimes. Descartado registrar o ThreadMXBean/java.lang.management no reflect-config (reintroduz reflection AOT por um ganho marginal num broker stateless). Emenda 2026-09-11: o indicador saiu quando o /health virou o flat da lib (e708fdb/6f4138b, ago/2026, com o management desligado) — este app não expõe mais /health/liveness.
  • O Dockerfile JVM 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).
  • -Os também no loop de dev: mais lento por iteração; -PnativeQuick/-Ob acelera o loop e o perfil final de prod (-Os vs -Ob) fica decidido ao medir tamanho/boot no corte.