Platform engineering e internal developer platform: conceptos y aplicación
1. Resumen general
Cuando el número de servicios crece, cada equipo debe encargarse del CI/CD, infraestructura, logging, seguridad y documentación. Gastan mucho tiempo en tareas repetitivas en lugar de escribir características. Platform engineering resuelve este problema agrupando esas tareas en una internal developer platform (IDP) de autoservicio para desarrolladores.
Después de leer este artículo, entenderás el concepto y por qué recibe tanta atención. También podrás construir un pequeño “golden path” usando Spring Boot starter, service catalog y una verificación de política simple. Al final, encontrarás casos en los que no deberías invertir en una plataforma.
2. ¿Qué es platform engineering?
Platform engineering es la labor de un equipo especializado (platform team) que construye y opera un conjunto de herramientas, procesos y servicios internos. Los equipos de producto (stream-aligned team) usan este conjunto para construir, desplegar y operar servicios sin necesidad de entender profundamente cada capa de infraestructura subyacente.
2.1. Internal developer platform
La IDP es el producto que crea el equipo de plataforma. Generalmente no es una herramienta única. Es una capa que integra múltiples componentes ya existentes: Git, CI, Kubernetes, cloud, secret manager, observability. Los desarrolladores interactúan con esta capa mediante un portal, CLI, plantillas o simplemente un archivo de configuración en el repositorio.
2.2. Tres ideas centrales
- Golden path (paved road): el camino recomendado y soportado para realizar una tarea común, por ejemplo “crear un nuevo servicio REST”. Seguir este camino es rápido y seguro. Desviarse es posible, pero el equipo asume la responsabilidad.
- Self-service: los desarrolladores crean servicios, bases de datos o entornos de prueba por sí mismos sin tener que abrir un ticket y esperar a que alguien lo haga.
- Platform as a product: los desarrolladores internos son los clientes. El equipo de plataforma debe entrevistarlos, medir el uso y priorizar el backlog como un verdadero equipo de producto.
2.3. ¿En qué se diferencia de DevOps?
DevOps es una cultura: el equipo que escribe código también es responsable de operarlo (“you build it, you run it”). Platform engineering no reemplaza DevOps. Es una forma de implementar DevOps a escala. En lugar de requerir que cada equipo sea experto en Kubernetes, Terraform e IAM, el equipo de plataforma encapsula ese conocimiento en abstracciones fáciles de usar.
3. ¿Por qué este tema recibe tanta atención actualmente?
Hay varias razones prácticas, independientes de cualquier herramienta específica:
- Aumento de carga cognitiva: un servicio moderno puede requerir Dockerfile, manifiestos de Kubernetes, pipeline, dashboard, alertas, políticas de seguridad y configuración de secretos. Cada uno de estos tiene múltiples opciones.
- Cada equipo a su manera: sin un estándar común, cada servicio registra logs en un formato diferente y nombra las métricas de forma distinta. Cuando hay un incidente, la persona de guardia tiene dificultad para identificar a qué equipo pertenece el servicio.
- Requisitos de seguridad y cumplimiento: escaneo de dependencias, firma de artefactos, control de acceso. Estas tareas se aplican uniformemente cuando están integradas en el golden path.
- Onboarding lento: los nuevos desarrolladores necesitan saber “dónde crear un servicio, cómo desplegarlo, dónde ver los logs”. Una buena plataforma responde estas preguntas en un solo lugar.
4. Componentes comunes de una IDP
| Componente | Rol |
|---|---|
| Service catalog | Lista de servicios, propietarios, documentación, dependencias |
| Software template | Crear nuevos proyectos siguiendo el estándar con solo unos parámetros |
| CI/CD compartido | Pipeline que incluye build, test, scan, deploy |
| Infraestructura self-service | Crear bases de datos, colas, buckets mediante configuración declarativa |
| Observability predeterminada | Log, métrica, trace disponibles cuando el servicio inicia |
| Guardrail / policy | Verificación automática de reglas obligatorias |
No necesitas todos desde el principio. Las secciones siguientes ilustran tres piezas pequeñas: un Spring Boot starter para el golden path, un catalog descriptor y una verificación de política.
5. Golden path con Spring Boot starter
La forma más económica de estandarizar servicios Java es publicar un starter interno. Cualquier servicio que agregue este starter tendrá automáticamente metadatos estándar: nombre, equipo propietario, nivel de criticidad. Los metadatos se exponen a través del actuator.
5.1. Dependencias
El starter interno necesita las siguientes dependencias:
<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. Los servicios que no usen actuator pueden seguir usando el starter.
5.2. Properties y descriptor
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 is required");
}
if (owner == null || owner.isBlank()) {
throw new IllegalArgumentException("platform.service.owner is required");
}
if (tier == null || tier.isBlank()) {
tier = "standard";
}
}
}
package dev.nonla.platform;
public record ServiceDescriptor(String name, String owner, String tier) {
}
El constructor compacto del record realiza dos funciones:
- Hace obligatorio declarar
nameyowner. Si falta uno de ellos, la aplicación no puede iniciarse. Este es un guardrail: no hay servicios “huérfanos”. - Asigna el valor por defecto
standardatiersi no se declara.
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()));
}
}
}
Explicación de cada parte:
@ConditionalOnMissingBeanpermite a los servicios definir su propioServiceDescriptor. Esta es la “puerta de salida” del golden path cuando realmente se necesita.- La clase anidada
ActuatorConfigurationsolo se carga cuandoInfoContributorestá en el classpath. De esta forma, los servicios que no usen actuator no tendránClassNotFoundException.
Para que Spring Boot 3 encuentre esta auto-configuration, debes registrarla en el archivo src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports:
dev.nonla.platform.PlatformServiceAutoConfiguration
En el lado del servicio, por ejemplo order-service, solo necesita agregar el starter y unas líneas de configuración:
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
Llamar a GET /actuator/info devolverá:
{"platform":{"name":"order-service","owner":"team-checkout","tier":"critical"}}
Desde aquí, una herramienta de monitoreo o el catálogo puede consultar este endpoint para saber quién es propietario de cada servicio.
5.4. Probar el comportamiento del starter
El equipo de plataforma debe probar el starter exhaustivamente. Un error aquí afectará a todos los servicios que lo usan. ApplicationContextRunner ayuda a probar la auto-configuration rápidamente sin necesidad de iniciar todo el servidor web:
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 givenNameAndOwner_whenStarting_thenDescriptorUsesDefaultTier() {
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 givenMissingOwner_whenStarting_thenContextFails() {
runner.withPropertyValues("platform.service.name=order-service")
.run(ctx -> {
assertThat(ctx).hasFailed();
assertThat(ctx.getStartupFailure())
.rootCause()
.hasMessageContaining("platform.service.owner");
});
}
}
La primera prueba confirma el valor por defecto y el registro de InfoContributor. La segunda prueba confirma el guardrail: si falta owner, el contexto no inicia y el mensaje de error especifica qué atributo está faltando.
6. Service catalog y guardrail
6.1. Catalog descriptor en el repositorio
La mayoría de IDPs almacenan los metadatos del servicio directamente en el repositorio, junto al código. A continuación hay un descriptor de un estilo común en portales de código abierto:
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: order-service
description: Procesa pedidos y pagos
tags:
- java
- spring-boot
spec:
type: service
lifecycle: production
owner: team-checkout
system: checkout
dependsOn:
- resource:orders-db
providesApis:
- order-api
Guardar este archivo en Git tiene dos ventajas. Los metadatos se revisan junto con el código, y no se desactualiza como una página wiki olvidada. El portal solo necesita leer este archivo para mostrar propietarios, dependencias y APIs del servicio.
6.2. Verificación de política simple
Un guardrail no necesariamente requiere un motor de políticas complejo. Para empezar, un fragmento de código que se ejecuta en CI es suficiente para bloquear configuraciones incorrectas:
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("name must be kebab-case, 3-41 chars");
}
if (!spec.owner().startsWith("team-")) {
errors.add("owner must be a team, not a person");
}
if (!ALLOWED_TIERS.contains(spec.tier())) {
errors.add("unknown tier: " + spec.tier());
}
if ("critical".equals(spec.tier()) && spec.replicas() < 2) {
errors.add("critical services need at least 2 replicas");
}
return errors;
}
}
Con la entrada new ServiceSpec("Order_Service", "alice", "critical", 1), la función devuelve:
[name must be kebab-case, 3-41 chars, owner must be a team, not a person, critical services need at least 2 replicas]
Lo importante es que el mensaje de error diga claramente qué necesita arreglarse. Un buen guardrail guía a los desarrolladores para corregir el error, no solo los bloquea.
7. Aplicación práctica: ¿por dónde empezar?
- Entrevista a los desarrolladores primero. Pregunta dónde pierden más tiempo: creando servicios nuevos, esperando acceso a entornos o depurando pipelines. Prioriza el problema más doloroso, no la herramienta más interesante.
- Comienza con la “plataforma viable más pequeña”. A veces solo necesitas un repositorio plantilla, un pipeline compartido y una página de documentación. El portal puede venir después.
- Elige un único golden path. Por ejemplo “servicio REST Spring Boot desplegado en Kubernetes”. Perfecciona este camino antes de expandir.
- Mide. Realiza seguimiento del número de equipos que usan la plataforma, el tiempo desde crear el repositorio hasta el primer despliegue y recopila retroalimentación regular de los desarrolladores. Si nadie la usa voluntariamente, es señal de que el producto aún no está listo.
- Mantén las salidas. El golden path debería ser el camino más fácil, no el único camino obligatorio. Como en la sección 5.3,
@ConditionalOnMissingBeanes un pequeño ejemplo de este principio. - Versionado y deprecación claros. Los starters, plantillas y pipelines compartidos son APIs. Los cambios que rompen compatibilidad deben ser comunicados y tener un plan de migración.
8. Errores comunes y cuándo no usar esto
Errores frecuentes:
- Construir el portal antes de tener contenido. Un portal bonito pero con catálogo vacío y plantillas anticuadas será rápidamente olvidado.
- Renombrar el equipo de ops a equipo de plataforma. Si los desarrolladores siguen teniendo que abrir un ticket y esperar, eso no es self-service.
- Abstracciones demasiado gruesas. Ocultar completamente Kubernetes impide que los desarrolladores depuren cuando hay problemas. La plataforma debe reducir carga cognitiva, no eliminar la capacidad de ver las capas inferiores.
- Imponer mediante regulación en lugar de valor. Si hay que mandar, es porque la plataforma aún no resuelve el problema real.
- Un starter que lo hace todo. Un starter con demasiadas dependencias y configuración implícita causará conflictos de versión y será difícil de actualizar.
Nota: Platform engineering tiene un costo fijo significativo. Necesitas personas que la construyan, la operen y proporcionen soporte.
Cuándo no deberías invertir en una IDP propia:
- La empresa es pequeña, con solo algunos equipos y servicios. Generalmente un README bien hecho junto con un pipeline compartido es suficiente.
- El producto aún busca encajar en el mercado y la arquitectura cambia frecuentemente. Estandarizar demasiado pronto bloqueará decisiones que pronto cambiarán.
- Los servicios gestionados de la nube ya cubren tus necesidades. Reconstruir una capa similar solo añade mantenimiento.
- No hay nadie dispuesto a ser product owner de la plataforma. Sin alguien escuchando a los desarrolladores, la plataforma se convierte en una colección de herramientas que nadie quiere usar.
9. Conclusión
Platform engineering no es una herramienta nueva. Es una forma de organizar el trabajo: un equipo que trata la experiencia de los desarrolladores internos como un producto, luego encapsula el conocimiento de infraestructura en golden paths y self-service. Has visto tres piezas pequeñas: un Spring Boot starter que estandariza metadatos con pruebas, un descriptor de catálogo en el repositorio y una verificación de política que se ejecuta en CI.
Comienza en pequeño, mide de verdad, y solo expande cuando los desarrolladores realmente usan la plataforma. Si tu organización aún es pequeña, una buena plantilla y un pipeline compartido pueden proporcionar la mayoría de los beneficios sin necesidad de una plataforma completa.