Engenharia de plataforma e IDP: conceitos e aplicação prática

1. Visão geral

Quando o número de serviços cresce, cada time precisa se preocupar com CI/CD, infraestrutura, logging, segurança e documentação. Eles gastam muito tempo com tarefas repetitivas em vez de escrever funcionalidades. Engenharia de plataforma resolve esse problema ao agrupar essas tarefas em uma plataforma interna para desenvolvedores (IDP) para que os desenvolvedores se sirvam.

Depois de ler este artigo, você entenderá o conceito e por que está gerando atenção. Você também será capaz de construir um pequeno “caminho ouro” usando um Spring Boot starter, catálogo de serviços e uma verificação de política simples. No final do artigo estão os casos em que você não deveria investir em uma plataforma.

2. O que é engenharia de plataforma?

Engenharia de plataforma é quando um grupo especializado (platform team) constrói e opera um conjunto de ferramentas, processos e serviços internos. Os times de produto (stream-aligned team) usam esse conjunto para construir, fazer deploy e operar serviços sem precisar entender profundamente cada camada de infraestrutura por baixo.

2.1. Plataforma interna para desenvolvedores

A IDP é o produto que o platform team cria. Geralmente não é uma ferramenta única. É uma camada que integra várias coisas já existentes: Git, CI, Kubernetes, nuvem, gerenciador de secrets, observabilidade. Os desenvolvedores interagem com essa camada através de um portal, CLI, template ou apenas um arquivo de configuração no repositório.

2.2. Três ideias principais

  • Caminho ouro (paved road): o caminho recomendado e bem suportado para fazer algo comum, por exemplo “criar um novo serviço REST”. Seguir este caminho é rápido e seguro. Você pode seguir outro caminho, mas o time assume a responsabilidade.
  • Autoatendimento: desenvolvedores criam serviços, bancos de dados ou ambientes de teste por conta própria, sem precisar abrir um ticket e esperar alguém.
  • Plataforma como um produto: desenvolvedores internos são clientes. O platform team precisa entrevistá-los, medir o uso e priorizar o backlog como um time de produto real.

2.3. Qual é a diferença com relação a DevOps?

DevOps é uma cultura: o time que escreve código também é responsável por operá-lo (“você constrói, você executa”). Engenharia de plataforma não substitui DevOps. É uma maneira de colocar DevOps em prática em grande escala. Em vez de exigir que cada time se torne um especialista em Kubernetes, Terraform e IAM, o platform team encapsula esse conhecimento em abstrações fáceis de usar.

3. Por que este tópico está recebendo atenção agora?

Há várias razões práticas, não dependentes de uma ferramenta específica:

  1. Carga cognitiva aumentada: um serviço moderno pode precisar de Dockerfile, manifesto Kubernetes, pipeline, dashboard, alertas, políticas de segurança e configuração de secrets. Cada coisa tem muitas opções.
  2. Cada time do seu jeito: sem um padrão comum, cada serviço registra em um formato diferente e nomeia métricas de maneiras diferentes. Quando há um incidente, fica difícil para quem está de plantão descobrir qual time é responsável.
  3. Requisitos de segurança e conformidade: verificar dependências, assinar artefatos, controlar acesso. Essas tarefas são fáceis de aplicar uniformemente se forem incorporadas ao caminho ouro.
  4. Integração lenta: novos membros precisam saber “onde criar um serviço, como fazer deploy, onde ver logs”. Uma boa plataforma responde essas perguntas em um único lugar.

4. Componentes comuns de uma IDP

Componente Papel
Catálogo de serviços Lista de serviços, proprietário, documentação, dependências
Template de software Criar novo projeto seguindo o padrão com apenas alguns parâmetros
CI/CD compartilhado Pipeline com build, testes, scan e deploy já prontos
Infraestrutura autoatendimento Criar banco de dados, fila, bucket via configuração declarativa
Observabilidade padrão Log, métrica, trace já disponíveis quando o serviço inicia
Guardrail / política Verificar automaticamente regras obrigatórias

Você não precisa ter tudo de uma vez no início. As seções a seguir ilustram três pedaços pequenos: um Spring Boot starter para o caminho ouro, um descritor de catálogo e uma verificação de política.

5. Caminho ouro com Spring Boot starter

A forma mais barata de padronizar serviços Java é lançar um starter interno. Qualquer serviço que adicione esse starter terá automaticamente metadados padronizados: nome, time proprietário, nível de criticidade. Os metadados são exibidos através do actuator.

5.1. Dependências

Um starter interno precisa das seguintes dependências:

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-autoconfigure</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-actuator</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

Actuator está marcado como optional. Serviços que não usam actuator podem usar o starter mesmo assim.

5.2. Propriedades e descritor

package dev.nonla.platform;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties("platform.service")
public record PlatformServiceProperties(String name, String owner, String tier) {

    public PlatformServiceProperties {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("platform.service.name é obrigatório");
        }
        if (owner == null || owner.isBlank()) {
            throw new IllegalArgumentException("platform.service.owner é obrigatório");
        }
        if (tier == null || tier.isBlank()) {
            tier = "standard";
        }
    }
}
package dev.nonla.platform;

public record ServiceDescriptor(String name, String owner, String tier) {
}

O construtor compacto do record faz duas coisas:

  • Torna obrigatório declarar name e owner. Se um deles faltar, a aplicação não inicia. Este é um guardrail: nenhum serviço “órfão”.
  • Atribui o valor padrão standard para tier se não for declarado.

5.3. Auto-configuration

package dev.nonla.platform;

import java.util.Map;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@AutoConfiguration
@EnableConfigurationProperties(PlatformServiceProperties.class)
public class PlatformServiceAutoConfiguration {

    @Bean
    @ConditionalOnMissingBean
    ServiceDescriptor serviceDescriptor(PlatformServiceProperties props) {
        return new ServiceDescriptor(props.name(), props.owner(), props.tier());
    }

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(InfoContributor.class)
    static class ActuatorConfiguration {

        @Bean
        InfoContributor platformInfoContributor(ServiceDescriptor descriptor) {
            return builder -> builder.withDetail("platform", Map.of(
                    "name", descriptor.name(),
                    "owner", descriptor.owner(),
                    "tier", descriptor.tier()));
        }
    }
}

Explicação de cada parte:

  • @ConditionalOnMissingBean permite que o serviço defina seu próprio ServiceDescriptor. Este é o “escape” do caminho ouro quando realmente necessário.
  • A classe aninhada ActuatorConfiguration é carregada apenas quando InfoContributor está no classpath. Dessa forma, serviços que não usam actuator não encontrarão ClassNotFoundException.

Para que Spring Boot 3 encontre essa auto-configuration, registre-a no arquivo src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:

dev.nonla.platform.PlatformServiceAutoConfiguration

Do lado do serviço, por exemplo order-service, você apenas precisa adicionar o starter e algumas linhas de configuração:

platform.service.name=order-service
platform.service.owner=team-checkout
platform.service.tier=critical
management.endpoints.web.exposure.include=health,info
management.info.env.enabled=false

Chamar GET /actuator/info retornará:

{"platform":{"name":"order-service","owner":"team-checkout","tier":"critical"}}

A partir disso, ferramentas de monitoramento ou catálogo podem fazer varredura desse endpoint para descobrir quem é o proprietário de cada serviço.

5.4. Testando o comportamento do starter

O time de plataforma precisa testar o starter bem. Um erro aqui afetará todos os serviços que usam esse starter. ApplicationContextRunner ajuda a testar auto-configuration rapidamente sem precisar iniciar um servidor web inteiro:

package dev.nonla.platform;

import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.springframework.boot.actuate.info.InfoContributor;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;

class PlatformServiceAutoConfigurationTest {

    private final ApplicationContextRunner runner = new ApplicationContextRunner()
            .withConfiguration(AutoConfigurations.of(PlatformServiceAutoConfiguration.class));

    @Test
    void dadoNomeEProprietario_aoIniciar_entaoDescriptorUsaTierPadrao() {
        runner.withPropertyValues(
                        "platform.service.name=order-service",
                        "platform.service.owner=team-checkout")
                .run(ctx -> {
                    assertThat(ctx).hasSingleBean(ServiceDescriptor.class)
                            .hasSingleBean(InfoContributor.class);
                    assertThat(ctx.getBean(ServiceDescriptor.class).tier())
                            .isEqualTo("standard");
                });
    }

    @Test
    void dadoProprietarioAusente_aoIniciar_entaoContextoFalha() {
        runner.withPropertyValues("platform.service.name=order-service")
                .run(ctx -> {
                    assertThat(ctx).hasFailed();
                    assertThat(ctx.getStartupFailure())
                            .rootCause()
                            .hasMessageContaining("platform.service.owner");
                });
    }
}

O primeiro teste confirma o valor padrão e o registro do InfoContributor. O segundo teste confirma o guardrail: se owner estiver faltando, o contexto não inicia e a mensagem de erro aponta exatamente qual atributo está faltando.

6. Catálogo de serviços e guardrail

6.1. Descritor de catálogo no repositório

A maioria das IDPs armazena metadados do serviço dentro do repositório, ao lado do código. Abaixo está um descritor de um tipo comumente visto em portais de código aberto:

apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: order-service
  description: Processa pedidos e pagamentos
  tags:
    - java
    - spring-boot
spec:
  type: service
  lifecycle: production
  owner: team-checkout
  system: checkout
  dependsOn:
    - resource:orders-db
  providesApis:
    - order-api

Armazenar esse arquivo no Git tem dois benefícios. Os metadados são revisados junto com o código e não ficam desatualizados como uma página de wiki esquecida. O portal apenas precisa ler esse arquivo para exibir o proprietário, dependências e APIs do serviço.

6.2. Verificação de política simples

Guardrail não precisa necessariamente usar um motor de política complexo. No início, um trecho de código executado no CI é suficiente para bloquear configurações ruins:

package dev.nonla.platform.policy;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

public class ServicePolicy {

    private static final Set<String> ALLOWED_TIERS = Set.of("critical", "standard", "experimental");

    public record ServiceSpec(String name, String owner, String tier, int replicas) {
    }

    public List<String> violations(ServiceSpec spec) {
        List<String> errors = new ArrayList<>();
        if (!spec.name().matches("[a-z][a-z0-9-]{2,40}")) {
            errors.add("nome deve estar em kebab-case, 3-41 caracteres");
        }
        if (!spec.owner().startsWith("team-")) {
            errors.add("proprietário deve ser um time, não uma pessoa");
        }
        if (!ALLOWED_TIERS.contains(spec.tier())) {
            errors.add("tier desconhecido: " + spec.tier());
        }
        if ("critical".equals(spec.tier()) && spec.replicas() < 2) {
            errors.add("serviços críticos precisam de pelo menos 2 réplicas");
        }
        return errors;
    }
}

Com a entrada new ServiceSpec("Order_Service", "alice", "critical", 1), a função retorna:

[nome deve estar em kebab-case, 3-41 caracteres, proprietário deve ser um time, não uma pessoa, serviços críticos precisam de pelo menos 2 réplicas]

O ponto importante é que a mensagem de erro deve deixar claro o que é preciso corrigir. Um guardrail bom orienta o desenvolvedor a consertar o erro, não apenas bloqueia.

7. Aplicação prática: por onde começar?

  1. Entreviste desenvolvedores primeiro. Pergunte onde eles gastam mais tempo: criando novo serviço, aguardando provisionamento de ambiente ou debugando pipeline. Priorize o problema mais crítico, não a ferramenta mais interessante.
  2. Comece com a “plataforma viável mais enxuta”. Às vezes, um template de repositório, um pipeline compartilhado e uma página de documentação são suficientes. O portal pode vir depois.
  3. Escolha um único caminho ouro. Por exemplo, “serviço REST Spring Boot deploy em Kubernetes”. Faça esse caminho bem feito primeiro, depois expanda.
  4. Meça. Acompanhe quantos times usam a plataforma, o tempo desde criar o repositório até o primeiro deploy e feedback regularmente dos desenvolvedores. Se ninguém usa voluntariamente, esse é um sinal de que o produto ainda não está bom.
  5. Mantenha um escape. O caminho ouro deve ser o mais fácil, não o único obrigatório. Como visto em 5.3, @ConditionalOnMissingBean é um pequeno exemplo desse princípio.
  6. Versionamento e deprecação claros. Starters, templates e pipelines compartilhados são APIs. Mudanças que quebram compatibilidade precisam ser comunicadas com um cronograma de atualização.

8. Erros comuns e quando não usar

Erros comuns:

  • Construir o portal antes de ter conteúdo. Um portal bonito mas com catálogo vazio e templates desatualizados será rapidamente abandonado.
  • Renomear o time de ops para platform team. Se os desenvolvedores ainda precisam abrir um ticket e esperar, isso não é autoatendimento.
  • Abstração muito grossa. Esconder completamente Kubernetes impede que desenvolvedores façam debug quando há um problema. A plataforma deve reduzir carga cognitiva, não remover a capacidade de ver o que está acontecendo em baixo nível.
  • Forçar com regulamentos em vez de valor. Se as pessoas precisam ser mandadas, elas só usam se obrigadas, o que significa que a plataforma ainda não resolveu o problema certo.
  • Starter fazendo de tudo. Um starter que puxa muitas dependências e configuração implícita causa conflitos de versão e é difícil de atualizar.

Nota: Engenharia de plataforma tem um custo fixo significativo. Você precisa de pessoas para construir, operar e suportar a própria plataforma.

Quando não investir em uma IDP própria:

  • Empresa pequena, apenas alguns times e poucos serviços. Um bom README junto com um pipeline compartilhado geralmente é o suficiente.
  • Produto ainda está procurando product-market fit e a arquitetura muda frequentemente. Padronizar muito cedo vai travar decisões que serão mudadas.
  • Os serviços gerenciados da nuvem já satisfazem as necessidades. Construir uma camada similar novamente apenas adiciona trabalho de manutenção.
  • Ninguém está disposto a ser product owner da plataforma. Sem alguém ouvindo os desenvolvedores, a plataforma vira uma coleção de ferramentas que ninguém quer usar.

9. Conclusão

Engenharia de plataforma não é uma ferramenta nova. É uma forma de organizar o trabalho: um time trata a experiência dos desenvolvedores internos como um produto e encapsula o conhecimento de infraestrutura em caminhos ouro e autoatendimento. Você viu três pequenas peças: um Spring Boot starter que padroniza metadados com testes, um descritor de catálogo que fica no repositório e uma verificação de política executada no CI.

Comece pequeno, meça a realidade e só expanda quando desenvolvedores realmente usarem. Se sua organização ainda é pequena, um template bom e um pipeline compartilhado podem fornecer a maioria dos benefícios sem precisar de uma plataforma completa.

platform-engineeringdevopsspring-bootdeveloper-experiencetech-trends