Capstone — do jar ao cluster
TL;DR
Este é o fio que costura o galho inteiro. Pegamos um serviço Spring Boot neutro — o
order-service— e o levamos a produção ponta a ponta:jarem camadas → imagem (Dockerfile, Buildpacks ou Jib) → JVM ciente do container → probes + graceful shutdown →Deploymentno Kubernetes → métricas/traces/logs → CI/CD. A tese que atravessa tudo: “production-ready” não é uma feature que se liga, é uma sequência de contratos — com o build, com a JVM, com o orquestrador, com o coletor de observabilidade. Cada nota do galho é uma estação dessa linha. Aqui você vê o trajeto completo e ganha as tabelas de decisão (Dockerfile vs Buildpacks vs Jib; JVM vs native; head vs tail sampling) e o cheatsheet “problema → nota” que vira munição direta em entrevista de plataforma/SRE.
O que é
Uma capstone não ensina nada novo. Ela monta o que as 21 notas anteriores ensinaram em pedaços e mostra o sistema funcionando como um todo. A pergunta que ela responde não é “o que é um probe?” nem “o que é um layered jar?” — essas já foram respondidas. A pergunta é:
“Eu tenho um
order-service.jarcompilado. O que precisa acontecer, em ordem, para ele estar rodando num cluster, observável, atualizável sem downtime e com diagnóstico sob carga?”
A resposta é uma linha de montagem de contratos. Cada estação entrega o artefato para a próxima num formato que ela entende:
- Build entrega um
jarem camadas (layered jar) — para que a imagem seja eficiente. - Empacotamento entrega uma imagem OCI enxuta — via Dockerfile, Buildpacks ou Jib.
- A JVM dentro do container precisa enxergar os limites do cgroup — senão dimensiona heap pela máquina inteira.
- Probes entregam ao orquestrador um contrato de saúde — liveness e readiness separados.
- Graceful shutdown entrega um encerramento limpo no SIGTERM — drena conexões antes de morrer.
- O
DeploymentYAML entrega ao cluster o estado desejado — réplicas, recursos, config. - Observabilidade entrega ao operador os três sinais — métricas, traces e logs.
- CI/CD entrega tudo isso de forma repetível — sem humano colando comando no terminal.
O
order-serviceé um domínio neutro de propósitoNão é um sistema real de ninguém. É um serviço genérico — recebe pedidos, persiste, expõe HTTP — escolhido só para ter um nome concreto nos exemplos. Substitua mentalmente por qualquer serviço Spring que você opere.
Por que importa
Quem domina uma estação parece competente. Quem domina a linha inteira parece sênior. A diferença em entrevista de plataforma/SRE é exatamente essa: o júnior sabe escrever um Dockerfile; o sênior sabe por que aquele Dockerfile tem multi-stage, como ele interage com o dimensionamento de heap da JVM, o que o readiness probe protege durante o rollout, e onde o trace gerado por esse pod vai parar.
A capstone também expõe os acoplamentos invisíveis entre estações — as decisões que parecem locais mas vazam:
- Escolher native image (estação 2) muda o profiling (estação 8): você perde JFR e ganha outro ferramental.
- Configurar readiness errado (estação 4) quebra o deploy sem downtime (estação 6): o cluster manda tráfego antes da app estar pronta.
- Esquecer de a JVM ler os limites do cgroup (estação 3) faz o pod ser morto por OOM independentemente de quão bons sejam seus probes.
Feynman: a fábrica de carros
Pense numa linha de montagem de carro. A lataria (jar) sai da prensa; a pintura (imagem) só funciona se a lataria veio limpa; o motor (JVM) precisa caber no compartimento (cgroup); os sensores de bordo (probes) avisam à central (orquestrador) se o carro liga; a inspeção final (CI/CD) garante que todo carro sai igual. Ninguém entrega um carro montando peças à mão a cada unidade — e ninguém deveria fazer deploy assim.
Como funciona
O fluxo ponta a ponta, estação por estação
O percurso canônico, do jar ao tráfego em produção:
- Compilar e empacotar em camadas. O Spring Boot já gera um layered jar por padrão. As camadas (dependências, dependências-snapshot, recursos da aplicação, classes da aplicação) mudam em ritmos diferentes — dependências raramente, código a cada commit. Separá-las faz o cache de imagem reaproveitar o que não mudou. Detalhe em Dockerfile na prática.
- Construir a imagem. Três caminhos (decididos na tabela abaixo):
Dockerfilemulti-stage, Buildpacks (mvn spring-boot:build-image), ou Jib. Todos produzem uma imagem OCI; diferem em controle, reprodutibilidade e dependência do Docker daemon. - Enxugar e escanear. Base distroless ou enxuta, usuário não-root, scan de CVE no pipeline — ver Imagem enxuta e segura.
- A JVM dentro do container. A JVM moderna é container-aware: lê os limites do cgroup e dimensiona heap por
MaxRAMPercentage. Sem isso, ela acha que tem a RAM da máquina inteira e o kernel mata o pod. Ver A JVM dentro de um container. - Probes. O Actuator expõe
/actuator/health/livenesse/actuator/health/readinesscomo contratos distintos: liveness = “estou vivo, não me reinicie”; readiness = “posso receber tráfego agora?“. Ver Health e probes. - Config e recursos. Config vem de fora (ConfigMap/Secret → variáveis de ambiente), e
requests/limitsdeclaram o apetite do pod. Ver Config e recursos no Kubernetes. - Graceful shutdown. No SIGTERM, o app marca readiness como down, drena conexões em voo e morre dentro do timeout — casado com o
preStopeterminationGracePeriodSecondsdo pod. Ver Graceful shutdown e deploy sem downtime. - Observabilidade. Os três seams: métricas via Micrometer → Prometheus; traces via OpenTelemetry → Collector; logs estruturados (JSON) no stdout. Panorama em Observabilidade — o panorama.
- CI/CD. Um pipeline constrói, testa, escaneia, publica a imagem e aplica o manifesto. Ver CI-CD e o caminho até produção.
As escolhas no caminho
A linha não é única — ela bifurca em pontos de decisão, e a sabedoria sênior é escolher conscientemente:
- Como construir a imagem? Dockerfile (controle total, mais manutenção) vs Buildpacks (zero Dockerfile, opinativo) vs Jib (daemonless, ótimo em CI). Tabela abaixo.
- JVM ou native? A JVM dá peak throughput e ferramental de diagnóstico maduro (JFR, async-profiler); o native dá startup quase instantâneo e footprint baixo, ao custo de build lento, reflexão fechada por hints e profiling diferente. A decisão honesta está em Native vs JVM e o conceito em GraalVM Native Image — conceito.
- Quanto trace coletar? Head sampling decide cedo (barato, mas pode descartar o trace de um erro raro); tail sampling decide depois de ver o trace inteiro no Collector (caro, mas pega os outliers). Ver OpenTelemetry Collector e sampling.
O que produção exige (e dev não cobra)
Em dev, o serviço “funciona”. Produção cobra contratos que dev nunca testa:
- Morrer limpo. Um pod é descartável; SIGTERM chega o tempo todo (rollout, autoscaling, drain de nó). Quem não drena conexões corta requisições no meio.
- Caber no orçamento.
limitsde memória + JVM container-aware: passou do limite, o kernel mata sem aviso (OOMKilled), sem stack trace. - Declarar saúde com honestidade. Readiness que mente (“estou pronto” sem o banco conectado) faz o cluster mandar tráfego para um pod que vai falhar.
- Ser observável sem
kubectl exec. Quando algo trava às 3h da manhã, você precisa de métricas, traces e logs já fluindo — não de abrir um shell no pod. Inclui profiling sob carga (Profiling e diagnóstico sob carga) e continuous profiling (Continuous profiling no cluster).
Na prática
O diagrama do trajeto completo:
┌─────────────── BUILD ───────────────┐
código ──▶ mvn package ──▶ order-service.jar (layered)
│
┌───────────────────────────┴───────────────────────────┐
▼ ▼ ▼
Dockerfile Buildpacks Jib
(multi-stage) mvn spring-boot:build-image daemonless, em CI
└───────────────────────────┬───────────────────────────┘
▼
imagem OCI (distroless, não-root, escaneada)
│ docker push registry/order-service:sha
▼
┌──────────────────────────── KUBERNETES ────────────────────────────┐
│ Deployment ──▶ Pod │
│ ├─ JVM container-aware (MaxRAMPercentage lê o cgroup) │
│ ├─ env ◀── ConfigMap + Secret │
│ ├─ readinessProbe ──▶ /actuator/health/readiness │
│ ├─ livenessProbe ──▶ /actuator/health/liveness │
│ └─ preStop + graceful shutdown (drena no SIGTERM) │
└────────────────────────────────┬───────────────────────────────────┘
▼
┌─────────────────────── OBSERVABILIDADE ────────────────────────────┐
│ /actuator/prometheus ──▶ Prometheus ──▶ Grafana (dashboards/alertas)│
│ OTLP traces ──▶ OpenTelemetry Collector ──▶ backend de tracing │
│ stdout (JSON) ──▶ coletor de logs ──▶ backend de logs │
└─────────────────────────────────────────────────────────────────────┘
▲
│ tudo orquestrado por
─── CI/CD pipeline ───Trecho 1 — Dockerfile multi-stage com layered jar (estações 1-3):
# --- stage de extração: quebra o jar em camadas ---
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app
COPY order-service.jar order-service.jar
RUN java -Djarmode=tools -jar order-service.jar extract --layers --destination extracted
# --- stage final: distroless, não-root ---
FROM gcr.io/distroless/java21-debian12:nonroot
WORKDIR /app
COPY --from=builder /app/extracted/dependencies/ ./
COPY --from=builder /app/extracted/spring-boot-loader/ ./
COPY --from=builder /app/extracted/snapshot-dependencies/ ./
COPY --from=builder /app/extracted/application/ ./
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", "-jar", "order-service.jar"]
jarmode=toolsé o sucessor delayertoolsO Spring Boot 3.3+ promove
java -Djarmode=tools ... extractcomo forma canônica. A sintaxe antiga-Djarmode=layertoolsainda existe, mas a documentação atual mostratools. Confirme a versão do seu Boot antes de copiar.
Trecho 2 — application.yml: probes, prometheus e shutdown (estações 4, 5, 7, 8):
server:
shutdown: graceful # drena requisições em voo no SIGTERM
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
management:
endpoint:
health:
probes:
enabled: true # liga os grupos liveness/readiness
health:
livenessState:
enabled: true
readinessState:
enabled: true
endpoints:
web:
exposure:
include: health,prometheus # /actuator/prometheus expostoTrecho 3 — Deployment Kubernetes (estações 4-7):
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
spec:
replicas: 3
strategy:
type: RollingUpdate # deploy sem downtime
rollingUpdate:
maxUnavailable: 0
maxSurge: 1
template:
spec:
terminationGracePeriodSeconds: 45 # > timeout-per-shutdown-phase
containers:
- name: order-service
image: registry/order-service:sha-abc123
resources:
requests: { cpu: "500m", memory: "512Mi" }
limits: { memory: "768Mi" } # casa com MaxRAMPercentage
envFrom:
- configMapRef: { name: order-service-config }
- secretRef: { name: order-service-secrets }
readinessProbe:
httpGet: { path: /actuator/health/readiness, port: 8080 }
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet: { path: /actuator/health/liveness, port: 8080 }
initialDelaySeconds: 30
periodSeconds: 10
lifecycle:
preStop:
exec:
command: ["sh", "-c", "sleep 5"] # janela p/ o endpoint sair do LBTabela de decisão
Como construir a imagem?
| Critério | Dockerfile multi-stage | Buildpacks (build-image) | Jib |
|---|---|---|---|
| Controle sobre a imagem | Total (você escreve cada camada) | Baixo (opinativo) | Médio |
| Precisa de Docker daemon? | Sim | Sim (por padrão) | Não (daemonless) |
| Reprodutibilidade | Depende da disciplina | Alta | Alta |
| Esforço de manutenção | Maior (Dockerfile vive) | Mínimo (zero Dockerfile) | Mínimo |
| Bom para | Times com requisitos específicos de imagem | Quem quer convenção sobre configuração | CI sem Docker, builds rápidos |
| Detalhe | nota 04 | nota 06 | nota 07 |
JVM ou native image?
| Critério | JVM (HotSpot) | GraalVM Native Image |
|---|---|---|
| Startup | Segundos (warmup do JIT) | ~Milissegundos |
| Footprint de memória | Maior | Menor |
| Peak throughput | Maior (JIT otimiza em runtime) | Geralmente menor |
| Tempo de build | Rápido | Lento (minutos) |
| Reflexão/proxies dinâmicos | Livres | Exigem hints (Spring AOT) |
| Diagnóstico | JFR, async-profiler maduros | Ferramental diferente/limitado |
| Bom para | Serviços de longa duração, alto tráfego | Serverless, escala-a-zero, CLI |
| Detalhe | nota 21 | nota 09 |
Head vs tail sampling de traces?
| Critério | Head sampling | Tail sampling |
|---|---|---|
| Quando decide | No início do trace (na app) | Depois do trace completo (no Collector) |
| Custo | Baixo (descarta cedo) | Alto (bufferiza o trace todo) |
| Pega erros raros? | Não garantido | Sim (decide vendo o resultado) |
| Onde mora a lógica | Na aplicação / SDK | No OpenTelemetry Collector |
| Bom para | Volume alto, orçamento apertado | Quando outliers/erros são o que importa |
| Detalhe | nota 16 | idem |
Armadilhas
(1) “Production-ready é só adicionar o Actuator”
O raciocínio falho: “adicionei spring-boot-starter-actuator, expus /actuator/health, pronto — está production-ready.” O Actuator expõe os contratos, mas não os cumpre sozinho. Liveness e readiness vêm desligados como grupos até você ligar management.endpoint.health.probes.enabled. O readiness padrão não sabe que seu banco caiu se você não plugar o indicador. Graceful shutdown não acontece sem server.shutdown: graceful e um terminationGracePeriodSeconds compatível no pod. O Actuator é o painel de instrumentos — instalar o painel não conserta o motor.
(2) “Tudo deve ser native”
O raciocínio falho: “native tem startup instantâneo e usa menos RAM, então é objetivamente melhor — migre tudo.” Native image é um trade-off de plataforma, não um upgrade. Você troca peak throughput (o JIT da JVM otimiza com base no que vê em runtime — o native compila uma vez, fechado) e troca ferramental de diagnóstico (perde JFR maduro) por startup e footprint. Para um serviço de longa duração e alto tráfego, o native pode ser mais lento no regime permanente. Native brilha onde o startup domina o custo: serverless, escala-a-zero, jobs curtos, CLIs. Para o order-service rodando 24/7 com três réplicas quentes, a JVM provavelmente vence. A decisão é por perfil de carga, não por moda.
(3) “Observabilidade é só ligar o Prometheus”
O raciocínio falho: “expus /actuator/prometheus, o Prometheus raspa, tenho observabilidade.” Métrica é um dos três sinais e responde “o quê?” (“a latência p99 subiu”). Ela não responde “por quê?” nem “onde, naquela requisição específica?” — isso é trace. E não te dá o contexto do evento exato — isso é log estruturado. Sem os três seams e sem correlação entre eles (o traceId no log, o trace ligado à métrica), você tem um gráfico bonito subindo e nenhuma forma de descer até a causa. Observabilidade é a tríade correlacionada, não um endpoint raspado.
Em entrevista
Frase pronta (inglês)
“Taking a Spring Boot service to production isn’t a single step — it’s a chain of contracts. I start from a layered jar, build an OCI image (Dockerfile, Buildpacks, or Jib depending on how much control I need), and make sure the JVM is container-aware so it sizes the heap from the cgroup, not the host. In the cluster, I wire liveness and readiness as separate contracts, enable graceful shutdown so SIGTERM drains in-flight requests, and externalize config through ConfigMaps and Secrets. For observability I treat metrics, traces, and logs as three correlated seams — Micrometer to Prometheus, OpenTelemetry to a Collector, and structured JSON logs to stdout. The whole thing is driven by a CI/CD pipeline, never by hand. The judgment calls I always flag are JVM versus native — a platform trade-off, not an upgrade — and head versus tail sampling, depending on whether I’m optimizing for cost or for catching rare errors.”
Vocabulário
| Termo (EN) | Significado |
|---|---|
| layered jar | jar dividido em camadas por ritmo de mudança, para cache eficiente de imagem |
| container-aware JVM | JVM que lê os limites do cgroup e dimensiona heap por MaxRAMPercentage |
| liveness vs readiness | ”estou vivo, não reinicie” vs “posso receber tráfego agora?“ |
| graceful shutdown | drenar requisições em voo no SIGTERM antes de encerrar |
| chain of contracts | a sequência de acordos (build, JVM, orquestrador, coletor) que torna o app production-ready |
| three seams (signals) | métricas, traces e logs como sinais correlacionados de observabilidade |
| head vs tail sampling | decidir amostrar o trace no início (barato) vs após vê-lo inteiro (pega outliers) |
| native image trade-off | trocar peak throughput e diagnóstico por startup e footprint |
Cheatsheet nota→problema
Veja também
- Production-ready e cloud-native (a tese)
- Dockerfile na prática (empacotamento)
- Health e probes (contrato com o orquestrador)
- Observabilidade — os 3 seams
- CI-CD e o caminho até produção
- Native vs JVM (a decisão honesta)
- Cloud-native e produção (MOC do galho)
- Trilha Java
Referências
- Spring Boot — Reference Documentation — sintaxe canônica de probes (
management.endpoint.health.probes.enabled), graceful shutdown (server.shutdown,spring.lifecycle.timeout-per-shutdown-phase), Prometheus (/actuator/prometheus) e empacotamento. - Spring Boot — Container Images / Layered Jars —
java -Djarmode=tools ... extracte Buildpacks (spring-boot:build-image). - Spring Boot — GraalVM Native Image — Spring AOT e
native:compile. - The Twelve-Factor App — os contratos de build/release/run, config, disposability e logs.
- Kubernetes — Configure Liveness, Readiness and Startup Probes — semântica dos probes do lado do orquestrador.
- OpenTelemetry — Sampling — head vs tail sampling.