Existe uma forma de gerenciar toda a infraestrutura Kubernetes com código Java real, tipado, testável e versionável no Git. A maioria das equipes Spring Boot ainda não sabe que o Pulumi Java SDK permite isso. Sem YAML de 400 linhas. Sem "funciona na minha máquina". Infra como código de verdade.
Publicado há três dias, o roadmap 2026 da Pulumi deixa claro: o ciclo do YAML está chegando ao fim. Nesse artigo vou mostrar como um dev Java pode substituir arquivos Kubernetes YAML por código Java compilado, com um exemplo completo de deployment Spring Boot no K8s.
O problema que o YAML criou no Kubernetes (e que ninguém fala)
Você já abriu o repositório do seu time e viu uma pasta k8s/ com 23 arquivos YAML? Deployment, Service, Ingress, ConfigMap, Secret, HorizontalPodAutoscaler, cada um com seu próprio YAML. Multiplique isso por 5 microserviços e você tem mais de 100 arquivos que ninguém garante que estão consistentes entre si.
O problema não é o YAML em si. O problema é que YAML é uma linguagem de configuração, não de programação. E quando você usa configuração para descrever infraestrutura complexa, as limitações aparecem rápido:
- Sem type safety: errar a indentação destrói o arquivo sem aviso. O YAML aceita qualquer coisa, compila qualquer coisa e quebra só em runtime no cluster
- Sem reutilização real: precisa de uma variável compartilhada entre dois arquivos YAML? Boa sorte sem Helm. E Helm é outra linguagem para aprender
- Sem testes: não existe "rodar o teste de unidade" de um arquivo YAML antes de aplicar no cluster. Você descobre o bug em produção
- Sem autocomplete do IDE: IntelliJ não sabe a diferença entre um campo válido e um campo inexistente no Kubernetes API
- Drift silencioso: alguém fez
kubectl edit deployment/meu-appdireto no cluster e ninguém sabe. O YAML no Git está errado há semanas
Como Tech Leader, já vi equipes inteiras passando horas depurando problemas que um compilador Java teria pego em segundos. O YAML quebrado que subiu para produção não tinha nenhum erro de sintaxe. Era semanticamente errado. E não havia como detectar antes de aplicar.
Tem uma saída melhor.
O que é o Pulumi e por que Java developers deveriam prestar atenção
Pulumi é uma ferramenta de IaC (Infrastructure as Code) que permite descrever infraestrutura usando linguagens de programação reais: Python, TypeScript, Go e, desde 2021, Java. Com suporte oficial para o Kubernetes Provider, o Pulumi Java SDK permite que você escreva toda a infraestrutura K8s em Java, com as mesmas ferramentas que já usa no Spring Boot.
Certinho, bora comparar rapidamente com as alternativas:
- Terraform/OpenTofu: usa HCL, outra linguagem específica para aprender. Tem state management robusto, mas ainda é configuração, não programação
- AWS CDK: suporte a Java, mas focado em AWS. Portabilidade limitada
- Helm: templates YAML com Go templating. Resolve reutilização mas adiciona complexidade de outra camada de abstração
- Pulumi Java SDK: Java puro. Seu IDE funciona. Seu compilador funciona. Seus testes JUnit funcionam. A infra é código como qualquer outro código da sua equipe
O Pulumi Java SDK mantém o mesmo modelo de state que o Terraform: rastreia o que foi criado, o que mudou e o que precisa ser destruído. A diferença está em como você descreve o que quer criar.
Na prática: configurando o Pulumi Java no seu projeto
Antes do código, as dependências. No seu pom.xml:
<dependencies>
<!-- Pulumi core SDK -->
<dependency>
<groupId>com.pulumi</groupId>
<artifactId>pulumi</artifactId>
<version>1.0.0</version>
</dependency>
<!-- Pulumi Kubernetes Provider -->
<dependency>
<groupId>com.pulumi</groupId>
<artifactId>kubernetes</artifactId>
<version>4.18.0</version>
</dependency>
</dependencies>
E no Pulumi.yaml na raiz do projeto de infra:
name: minha-infra-spring
runtime: java
description: Infraestrutura do MeuApp Spring Boot no Kubernetes
Pronto. A partir daqui, tudo é Java.
O deployment Spring Boot completo com Pulumi Java
Bora ver como fica um deployment real. O equivalente ao YAML que você conhece, escrito em Java:
import com.pulumi.Pulumi;
import com.pulumi.kubernetes.apps.v1.Deployment;
import com.pulumi.kubernetes.apps.v1.DeploymentArgs;
import com.pulumi.kubernetes.core.v1.Service;
import com.pulumi.kubernetes.core.v1.ServiceArgs;
import com.pulumi.kubernetes.meta.v1.inputs.ObjectMetaArgs;
import java.util.Map;
public class InfraApp {
public static void main(String[] args) {
Pulumi.run(ctx -> {
var appLabels = Map.of("app", "meu-app-spring");
// Deployment com 3 replicas para alta disponibilidade
var deployment = new Deployment("spring-boot-deployment",
DeploymentArgs.builder()
.metadata(ObjectMetaArgs.builder()
.name("meu-app-spring")
.labels(appLabels)
.build())
.spec(DeploymentSpecArgs.builder()
.replicas(3)
.selector(LabelSelectorArgs.builder()
.matchLabels(appLabels)
.build())
.template(PodTemplateSpecArgs.builder()
.metadata(ObjectMetaArgs.builder()
.labels(appLabels)
.build())
.spec(PodSpecArgs.builder()
.containers(ContainerArgs.builder()
.name("spring-app")
.image("minha-empresa/meu-app-spring:latest")
.ports(ContainerPortArgs.builder()
.containerPort(8080)
.build())
.env(
EnvVarArgs.builder()
.name("SPRING_PROFILES_ACTIVE")
.value("prod")
.build(),
EnvVarArgs.builder()
.name("DB_HOST")
.value("postgres-svc:5432")
.build()
)
.resources(ResourceRequirementsArgs.builder()
.requests(Map.of(
"cpu", "250m",
"memory", "512Mi"
))
.limits(Map.of(
"cpu", "500m",
"memory", "1Gi"
))
.build())
.build())
.build())
.build())
.build())
.build()
);
// Service tipo LoadBalancer para expor na porta 80
var service = new Service("spring-boot-svc",
ServiceArgs.builder()
.metadata(ObjectMetaArgs.builder()
.name("meu-app-spring-svc")
.build())
.spec(ServiceSpecArgs.builder()
.selector(appLabels)
.type("LoadBalancer")
.ports(ServicePortArgs.builder()
.port(80)
.targetPort(8080)
.build())
.build())
.build()
);
// Exportar o IP para smoke tests no pipeline CI
ctx.export("serviceEndpoint",
service.status().applyValue(s ->
s.get().loadBalancer().get()
.ingress().get(0).ip().get()
)
);
});
}
}
Olha o que acontece quando você roda pulumi up:
Updating (prod):
Type Name Status
+ pulumi:pulumi:Stack minha-infra-spring-prod created
+ ├─ kubernetes:apps/v1:Deployment spring-boot-deployment created
+ └─ kubernetes:core/v1:Service spring-boot-svc created
Outputs:
serviceEndpoint: "34.95.112.43"
Resources:
+ 3 created
Duration: 28s
O IP do LoadBalancer sai direto como output. Você pode pegar esse valor no seu CI/CD para rodar smoke tests logo depois do deploy. Não precisa de script shell fazendo kubectl get svc -o jsonpath. Certinho?
Testando a infraestrutura como se fosse código de produção
Aqui é onde a coisa fica interessante. Com YAML, não tem como testar antes de aplicar. Com Pulumi Java, você testa com JUnit como qualquer outro código:
import org.junit.jupiter.api.Test;
import com.pulumi.test.Mocks;
import com.pulumi.test.TestOptions;
import static org.assertj.core.api.Assertions.*;
class InfraTest {
@Test
void deploymentDeveTerTresReplicas() throws Exception {
var resources = Mocks.withMocks(
new TestOptions().setIsPreview(false),
() -> {
InfraApp.main(new String[]{});
return List.of();
}
);
var deployment = resources.stream()
.filter(r -> r instanceof Deployment)
.map(r -> (Deployment) r)
.findFirst().orElseThrow();
// Garantir que replicas nunca desce para 1 acidentalmente
deployment.spec().applyValue(spec -> {
assertThat(spec.get().replicas().get()).isEqualTo(3);
return null;
});
}
@Test
void servicePrecisaExpor80ERedirecionarPara8080() throws Exception {
var resources = Mocks.withMocks(
new TestOptions().setIsPreview(false),
() -> {
InfraApp.main(new String[]{});
return List.of();
}
);
var svc = resources.stream()
.filter(r -> r instanceof Service)
.map(r -> (Service) r)
.findFirst().orElseThrow();
svc.spec().applyValue(spec -> {
var porta = spec.get().ports().get().get(0);
assertThat(porta.port()).isEqualTo(80);
return null;
});
}
}
Isso roda em mvn test sem precisar de cluster Kubernetes nem de kubectl instalado. O Pulumi sobe mocks internos que simulam o estado do cloud provider.
Pensa na analogia: é como ter um teste de contrato para a sua infra. Da mesma forma que você testa se o endpoint REST retorna 200, agora você testa se o deployment tem 3 replicas e se o Service está na porta certa. Antes de qualquer pulumi up.
Na empresa em que trabalhei como Tech Leader, a maior fonte de incidentes em produção não era bug de código. Era drift de configuração: alguém tinha editado o Deployment diretamente no cluster e o Git estava desatualizado. Com Pulumi, o próprio pulumi refresh detecta esse drift e o teste de CI falha antes de você subir algo inconsistente.
Integração com Spring Boot: configurando via ConfigMap
Uma dúvida comum: como injetar configurações do application.properties do Spring Boot via Pulumi? Você usa um ConfigMap:
import com.pulumi.kubernetes.core.v1.ConfigMap;
import com.pulumi.kubernetes.core.v1.ConfigMapArgs;
// Dentro do Pulumi.run():
var appConfig = new ConfigMap("spring-config",
ConfigMapArgs.builder()
.metadata(ObjectMetaArgs.builder()
.name("spring-app-config")
.build())
.data(Map.of(
"SPRING_DATASOURCE_URL", "jdbc:postgresql://postgres-svc:5432/meubanco",
"SPRING_JPA_HIBERNATE_DDL_AUTO", "validate",
"LOGGING_LEVEL_ROOT", "INFO",
"MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE", "health,metrics,prometheus"
))
.build()
);
// Referenciar o ConfigMap nas envvars do container
EnvVarArgs.builder()
.name("SPRING_DATASOURCE_URL")
.valueFrom(EnvVarSourceArgs.builder()
.configMapKeyRef(ConfigMapKeySelectorArgs.builder()
.name(appConfig.metadata().applyValue(m -> m.get().name().get()))
.key("SPRING_DATASOURCE_URL")
.build())
.build())
.build()
O benefício aqui é que o name(appConfig.metadata()...) cria uma referência tipada entre o ConfigMap e o Deployment. Se você renomear o ConfigMap, o compilador quebra antes de você chegar no cluster. Com YAML, esse tipo de refactoring silenciosamente cria uma config inválida que só explode em runtime.
Para mais sobre como configurar o Spring Boot com variáveis de ambiente em produção, veja o artigo sobre ConfigMaps e Secrets no Spring Boot com Kubernetes. E se quiser entender como a observabilidade se encaixa nessa stack, veja Spring Boot Actuator com OpenTelemetry em produção.
O que muda no dia a dia da equipe
Depois de migrar a infra de um microserviço Spring Boot de YAML puro para Pulumi Java em um projeto real, os ganhos práticos foram esses:
- Onboarding de novos devs: saiu de "3 dias para entender os 23 YAMLs" para "abre o projeto Maven no IntelliJ, segue o código". O IDE ajuda. A documentação Java já conhecida ajuda
- Refactoring de infra: mudar o nome de um label de 15 YAMLs virou um Rename do IntelliJ. Tudo atualiza junto, compilador confirma
- Review de PR: diff de código Java mostra intenção. Diff de YAML mostra indentação
- Detecção de drift:
pulumi refreshno pipeline detecta qualquer mudança manual no cluster
Para saber como conectar o pulumi up ao seu CI/CD, confira o artigo sobre Deploy Kubernetes com Azure DevOps e Pulumi Java.
Quando Pulumi é a escolha certa:
- Equipes Java que precisam de IaC mas não querem aprender HCL ou Go
- Projetos com lógica condicional complexa na infra (ex: criar infra diferente por ambiente)
- Times que já têm pipeline de testes e querem cobrir infra também
Quando manter YAML puro pode fazer sentido:
- Infra muito simples (1 deployment, 1 service) que raramente muda
- Equipes sem background em programação que mantêm a infra separada do time de desenvolvimento
- Projetos que precisam ser portáveis para times que não têm Java instalado
Perguntas Frequentes
Preciso saber Kubernetes para usar Pulumi Java?
Você precisa entender os conceitos básicos: Deployment, Service, Pod, ConfigMap. O Pulumi não abstrai o Kubernetes, ele só muda a linguagem em que você descreve esses recursos. Se você já entende o que um Deployment faz, vai se virar bem com o Pulumi Java SDK. Docs oficiais ajudam bastante nessa transição inicial.
Pulumi funciona com AWS e Azure, não só K8s?
Sim. O Pulumi tem providers para AWS, Azure, GCP, Cloudflare e dezenas de outros. O Java SDK funciona com todos eles. Você pode criar um S3 bucket, configurar um ECS service e fazer deploy no Kubernetes tudo no mesmo programa Java. Isso é uma vantagem grande em comparação com ferramentas K8s-only.
Posso migrar minha infra YAML existente para Pulumi sem começar do zero?
Existe o pulumi convert --from kubernetes que tenta converter YAMLs existentes para código Pulumi. O resultado não é perfeito mas serve como ponto de partida. Para infras maiores, o caminho mais seguro é importar os recursos existentes com pulumi import e deixar o Pulumi assumir o gerenciamento gradualmente, serviço por serviço.
Pulumi é open source ou pago?
O Pulumi core é open source (Apache 2.0). O Pulumi Cloud (backend de state gerenciado) tem plano gratuito para uso individual e times pequenos. Você pode usar backend alternativo como S3 ou Azure Blob Storage se não quiser o Pulumi Cloud. Para a maioria das equipes pequenas o plano gratuito é suficiente para começar.
Para levar na mochila
- YAML sprawl é um problema real que cresce com a equipe e com o número de serviços. Sem type safety, sem testes, sem IDE, a infra vira uma caixa preta
- Pulumi Java SDK permite descrever toda a infra K8s em Java compilado, testável com JUnit e refatorável com IntelliJ
- A curva de aprendizado é baixa para quem já conhece Spring Boot e K8s. Você usa o que já sabe
Você já usa IaC no seu projeto Spring Boot? Está com YAML puro, Helm ou já migrou para Pulumi? Conta nos comentários como está sendo na prática. Quero saber os maiores desafios que sua equipe enfrenta nessa parte de infraestrutura.
Na próxima semana vou mostrar como configurar o Pulumi com GitOps: acionar o pulumi up automaticamente via ArgoCD quando um PR é mergeado. A infra vira um cidadão de primeira classe no seu workflow de deploy.