Plataforma de Desarrollador Interna IDP: Construyendo un Portal de Desarrollador Empresarial con Backstage 2026
Plataforma de Desarrollador Interna IDP: Construyendo un Portal de Desarrollador Empresarial con Backstage
Acabas de unirte a una empresa con 200 microservicios. El primer día, tu líder dice "despliega order-service". Luego pasas 3 horas buscando el repositorio Git, 2 horas encontrando la configuración CI/CD, 1 hora encontrando la documentación de la API — el día se ha ido antes de escribir una sola línea de código.
Esta es la vida diaria sin una Plataforma de Desarrollador Interna (IDP). Backstage, el framework de portal de desarrolladores de código abierto creado por Spotify, se está convirtiendo en el estándar de facto para la ingeniería de plataformas. Este artículo recorre 5 patrones principales para construir un IDP de nivel empresarial desde cero.
Conceptos Clave de un Vistazo
| Concepto | Descripción | Analogía |
|---|---|---|
| IDP (Plataforma de Desarrollador Interna) | Portal de autoservicio unificado para desarrolladores | "App Store" empresarial |
| Backstage | Framework IDP de código abierto de Spotify | El Kubernetes del IDP |
| Software Catalog | Registro de metadatos para todos los servicios | "Páginas Amarillas" para servicios |
| TechDocs | Sistema de documentación técnica basado en Markdown | Wiki de nivel de ingeniería |
| Software Template | Creación estandarizada de proyectos con un clic | CLI de scaffolding basado en web |
| Plugin | Módulo de extensión de Backstage | Extensión de navegador |
5 Principales Puntos de Dolor Sin un IDP
- Dificultad en el descubrimiento de servicios: Cientos de microservicios, no se puede encontrar quién es el dueño de qué, cómo desplegar, dónde monitorear
- Documentación dispersa: Confluence, GitLab Wiki, Notion... no se encuentra, no se entiende
- Incorporación lenta de proyectos: Reconstruir el scaffolding desde cero cada vez, CI/CD, monitoreo, configuración de logging es repetitivo
- Cadena de herramientas fragmentada: Jenkins, ArgoCD, Grafana, Jira... el cambio constante de contexto mata la productividad
- Pesadilla de auditoría de cumplimiento: Quién cambió qué, quién tiene acceso, el grafo de dependencias de servicios es un desastre
Patrón 1: Arquitectura e Instalación de Backstage
Backstage utiliza una arquitectura monolítica basada en plugins. El núcleo consta de cuatro módulos: Catalog + TechDocs + Scaffold + Search, extendidos a través de plugins.
# Instalación e inicialización de Backstage
# Entorno: Node.js 20.x+ / yarn 1.22+ / Git 2.40+
# Dependencias: @backstage/cli 1.32+ / @backstage/core 1.30+
# 1. Crear aplicación Backstage
npx @backstage/create-app@latest --name my-idp
cd my-idp
# 2. Estructura del proyecto
# my-idp/
# ├── app/ # Aplicación React frontend
# │ ├── src/
# │ │ ├── components/ # Componentes personalizados
# │ │ ├── plugins/ # Plugins frontend
# │ │ └── App.tsx # Punto de entrada de la aplicación
# ├── packages/
# │ ├── backend/ # Servicio backend Node.js
# │ └── app/ # Paquete de construcción frontend
# ├── plugins/ # Directorio de plugins personalizados
# ├── catalog-info.yaml # Definiciones de entidades del catálogo
# └── app-config.yaml # Configuración de la aplicación
# 3. Iniciar servidor de desarrollo
yarn dev
# 4. Construir versión de producción
yarn build:backend
# app-config.yaml: Configuración principal de Backstage
# Entorno: Backstage 1.32+
app:
title: Portal de Desarrollador Empresarial
baseUrl: http://localhost:3000
organization:
name: MyCompany
backend:
baseUrl: http://localhost:7007
listen:
port: 7007
csp:
connect-src: ["'self'", 'http:', 'https:']
cors:
origin: http://localhost:3000
methods: [GET, HEAD, PATCH, POST, PUT, DELETE]
credentials: true
database:
client: pg
connection:
host: ${POSTGRES_HOST}
port: ${POSTGRES_PORT:-5432}
user: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}
database: backstage_plugin
integrations:
github:
- host: github.com
token: ${GITHUB_TOKEN}
gitlab:
- host: gitlab.mycompany.com
token: ${GITLAB_TOKEN}
auth:
environment: development
providers:
github:
development:
clientId: ${GITHUB_CLIENT_ID}
clientSecret: ${GITHUB_CLIENT_SECRET}
oidc:
production:
metadataUrl: ${OIDC_METADATA_URL}
clientId: ${OIDC_CLIENT_ID}
clientSecret: ${OIDC_CLIENT_SECRET}
catalog:
rules:
- allow: [Component, System, API, Resource, Location, Template]
locations:
- type: url
target: https://github.com/mycompany/catalog/blob/main/catalog-info.yaml
- type: url
target: https://gitlab.mycompany.com/platform/catalog/-/blob/main/all.yaml
techdocs:
builder: 'local'
generator:
runIn: 'docker'
publisher:
type: 'local'
Patrón 2: Software Catalog
El Catalog es el corazón de Backstage — el registro de metadatos para todos los servicios. Define entidades a través de archivos descriptores YAML, con descubrimiento e indexación automática de servicios.
# catalog-info.yaml: Registro de componente de servicio
# Entorno: Backstage 1.32+ / Catalog API v1
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: order-service
description: Servicio de pedidos principal que gestiona la creación, pago y cumplimiento de pedidos
annotations:
backstage.io/source-location: url:https://github.com/mycompany/order-service
backstage.io/techdocs-ref: dir:.
github.com/project-slug: mycompany/order-service
gitlab.com/project-id: "12345"
jira.com/project-key: ORD
grafana/dashboard-tag: order-service
prometheus.io/alert: order-service-alerts
tags:
- java
- spring-boot
- microservice
- tier-1
links:
- url: https://grafana.mycompany.com/d/order-service
title: Monitoreo Grafana
icon: dashboard
- url: https://argocd.mycompany.com/applications/order-service
title: Despliegue ArgoCD
icon: cloud
- url: https://order-service.mycompany.com/swagger
title: Docs API
icon: api
spec:
type: service
lifecycle: production
owner: team-order
system: order-system
dependsOn:
- component:payment-service
- component:inventory-service
- resource:order-db
providesApis:
- order-api
consumesApis:
- payment-api
- inventory-api
---
apiVersion: backstage.io/v1alpha1
kind: API
metadata:
name: order-api
description: API REST del Servicio de Pedidos
spec:
type: openapi
lifecycle: production
owner: team-order
system: order-system
definition:
$text: https://raw.githubusercontent.com/mycompany/order-service/main/openapi.yaml
---
apiVersion: backstage.io/v1alpha1
kind: System
metadata:
name: order-system
description: Sistema de pedidos incluyendo servicios de pedidos, pagos e inventario
spec:
owner: team-order
domain: commerce
---
apiVersion: backstage.io/v1alpha1
kind: Group
metadata:
name: team-order
description: Equipo de Pedidos
spec:
type: team
parent: dept-commerce
children: []
members:
- john.doe
- jane.smith
- bob.wilson
// TypeScript: Procesador personalizado de entidades del Catalog
// Entorno: Node.js 20.x+ / @backstage/plugin-catalog-node 1.12+
import {
CatalogProcessor,
CatalogProcessorEmit,
processingResult,
} from '@backstage/plugin-catalog-node';
import { LocationSpec, Location } from '@backstage/plugin-catalog-common';
import { Entity } from '@backstage/catalog-model';
export class CustomServiceProcessor implements CatalogProcessor {
getProcessorName(): string {
return 'CustomServiceProcessor';
}
async validateLocationKind(location: Location): Promise<boolean> {
return location.type === 'url';
}
async preProcessEntity(
entity: Entity,
location: LocationSpec,
emit: CatalogProcessorEmit,
): Promise<Entity> {
// Agregar automáticamente etiquetas de cumplimiento para todos los servicios en producción
if (
entity.kind === 'Component' &&
entity.spec?.lifecycle === 'production'
) {
const annotations = entity.metadata.annotations ?? {};
annotations['compliance.mycompany.com/scan-required'] = 'true';
annotations['compliance.mycompany.com/last-scan'] = new Date().toISOString();
entity.metadata.annotations = annotations;
}
// Generar automáticamente relaciones de dependencia del servicio
if (entity.kind === 'Component' && entity.spec?.dependsOn) {
const deps = entity.spec.dependsOn as string[];
for (const dep of deps) {
emit(
processingResult.relation({
source: {
name: entity.metadata.name,
namespace: entity.metadata.namespace ?? 'default',
kind: entity.kind,
},
target: {
name: dep.replace('component:', ''),
namespace: 'default',
kind: 'Component',
},
type: 'dependsOn',
}),
);
}
}
return entity;
}
}
Patrón 3: TechDocs
TechDocs está construido sobre MkDocs + Markdown, siguiendo la filosofía Docs-as-Code — la documentación vive junto al código en el mismo ciclo de vida del repositorio.
# mkdocs.yaml: Configuración de TechDocs
# Entorno: MkDocs 1.6+ / mkdocs-material 9.5+
site_name: Documentación Técnica del Servicio de Pedidos
site_description: Documentación técnica completa para el Servicio de Pedidos
nav:
- Inicio: index.md
- Primeros Pasos: getting-started.md
- Arquitectura:
- Resumen del Sistema: architecture/overview.md
- Modelo de Datos: architecture/data-model.md
- Diseño de API: architecture/api-design.md
- Operaciones:
- Guía de Despliegue: ops/deployment.md
- Monitoreo y Alertas: ops/monitoring.md
- Solución de Problemas: ops/troubleshooting.md
- Desarrollo:
- Estándares de Código: dev/coding-standards.md
- Flujo de Git: dev/git-workflow.md
- Estrategia de Testing: dev/testing.md
plugins:
- techdocs-core:
add_mermaid: true
add_plantuml: true
markdown_extensions:
- admonition
- codehilite
- footnotes
- meta
- toc:
permalink: true
- pymdownx.superfences:
custom_fences:
- name: mermaid
class: mermaid
format: !!python/name:pymdownx.superfences.fence_code_format
<!-- docs/index.md: Página principal de TechDocs -->
# Servicio de Pedidos
## Resumen
El Servicio de Pedidos es el microservicio principal del sistema de pedidos, que gestiona la creación, pago y cumplimiento de pedidos.
## Enlaces Rápidos
| Recurso | Enlace |
|----------|------|
| Código Fuente | [GitHub](https://github.com/mycompany/order-service) |
| CI/CD | [Jenkins](https://jenkins.mycompany.com/job/order-service) |
| Monitoreo | [Grafana](https://grafana.mycompany.com/d/order-service) |
| Docs API | [Swagger](https://order-service.mycompany.com/swagger) |
## Arquitectura
```mermaid
graph TB
A[API Gateway] --> B[Order Service]
B --> C[Payment Service]
B --> D[Inventory Service]
B --> E[(Order DB)]
B --> F[Redis Cache]
B --> G[Kafka]
Métricas Clave
| Métrica | Objetivo | Actual |
|---|---|---|
| Latencia P99 | <200ms | 150ms |
| Disponibilidad | 99.95% | 99.97% |
| Tasa de Error | <0.1% | 0.05% |
## Patrón 4: Desarrollo de Plugins
El sistema de plugins de Backstage es su principal ventaja competitiva. Los plugins personalizados permiten que el IDP se adapte verdaderamente a la cadena de herramientas de tu empresa.
```typescript
// TypeScript: Plugin personalizado de Backstage - Panel de Verificación de Salud del Servicio
// Entorno: Node.js 20.x+ / @backstage/core-plugin-api 1.9+ / React 18.x
// --- Plugin Frontend ---
// plugins/service-health/src/plugin.ts
import {
createPlugin,
createRoutableExtension,
} from '@backstage/core-plugin-api';
export const serviceHealthPlugin = createPlugin({
id: 'service-health',
routes: {
root: rootRouteRef,
},
});
export const ServiceHealthPage = serviceHealthPlugin.provide(
createRoutableExtension({
name: 'ServiceHealthPage',
component: () =>
import('./components/ServiceHealthPanel').then(
(m) => m.ServiceHealthPanel,
),
mountPoint: rootRouteRef,
}),
);
// plugins/service-health/src/components/ServiceHealthPanel.tsx
import React from 'react';
import { useEntity } from '@backstage/plugin-catalog-react';
import {
Header,
Page,
Content,
InfoCard,
Progress,
StatusOK,
StatusError,
StatusWarning,
} from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
import { serviceHealthApiRef } from '../api';
interface HealthCheckResult {
name: string;
status: 'healthy' | 'degraded' | 'down';
latency_ms: number;
last_check: string;
message: string;
}
export const ServiceHealthPanel: React.FC = () => {
const { entity } = useEntity();
const healthApi = useApi(serviceHealthApiRef);
const [healthData, setHealthData] = React.useState<HealthCheckResult[]>([]);
const [loading, setLoading] = React.useState(true);
React.useEffect(() => {
const serviceName = entity.metadata.name;
healthApi
.getServiceHealth(serviceName)
.then((data) => {
setHealthData(data.checks);
setLoading(false);
})
.catch(() => setLoading(false));
}, [entity, healthApi]);
if (loading) return <Progress />;
const StatusIcon = ({ status }: { status: string }) => {
switch (status) {
case 'healthy':
return <StatusOK />;
case 'degraded':
return <StatusWarning />;
case 'down':
return <StatusError />;
default:
return null;
}
};
return (
<Page themeId="tool">
<Header title={`Verificación de Salud de ${entity.metadata.name}`} />
<Content>
{healthData.map((check) => (
<InfoCard key={check.name} title={check.name}>
<StatusIcon status={check.status} />
<p>Estado: {check.status}</p>
<p>Latencia: {check.latency_ms}ms</p>
<p>Última Verificación: {check.last_check}</p>
<p>Mensaje: {check.message}</p>
</InfoCard>
))}
</Content>
</Page>
);
};
// TypeScript: Plugin backend de Backstage
// Entorno: Node.js 20.x+ / @backstage/backend-common 0.25+
// plugins/service-health-backend/src/plugin.ts
import {
createBackendPlugin,
coreServices,
} from '@backstage/backend-plugin-api';
import { createRouter } from './router';
export const serviceHealthBackendPlugin = createBackendPlugin({
pluginId: 'service-health',
register(env) {
env.registerInit({
deps: {
logger: coreServices.logger,
config: coreServices.rootConfig,
httpRouter: coreServices.httpRouter,
discovery: coreServices.discovery,
},
async init({ logger, config, httpRouter, discovery }) {
const router = await createRouter({
logger,
config,
discovery,
});
httpRouter.use(router);
httpRouter.addAuthPolicy({
path: '/health',
allow: 'unauthenticated',
});
logger.info('Plugin de Salud del Servicio inicializado');
},
});
},
});
// plugins/service-health-backend/src/router.ts
import express, { Router } from 'express';
import { LoggerService, RootConfigService, DiscoveryService } from '@backstage/backend-plugin-api';
interface RouterOptions {
logger: LoggerService;
config: RootConfigService;
discovery: DiscoveryService;
}
export async function createRouter(
options: RouterOptions,
): Promise<Router> {
const { logger, config } = options;
const router = Router();
router.get('/health', (_req, res) => {
res.json({ status: 'ok' });
});
router.get('/services/:serviceName/health', async (req, res) => {
const { serviceName } = req.params;
try {
const healthChecks = await fetchServiceHealth(config, serviceName);
res.json({
service: serviceName,
checks: healthChecks,
timestamp: new Date().toISOString(),
});
} catch (error) {
logger.error(`Error al obtener salud de ${serviceName}: ${error}`);
res.status(500).json({ error: 'Health check failed' });
}
});
return router;
}
async function fetchServiceHealth(
config: RootConfigService,
serviceName: string,
): Promise<Array<{
name: string;
status: 'healthy' | 'degraded' | 'down';
latency_ms: number;
last_check: string;
message: string;
}>> {
const prometheusUrl = config.getString('monitoring.prometheus.url');
return [
{
name: 'HTTP Endpoint',
status: 'healthy',
latency_ms: 45,
last_check: new Date().toISOString(),
message: 'Todos los endpoints responden normalmente',
},
{
name: 'Database Connection',
status: 'healthy',
latency_ms: 12,
last_check: new Date().toISOString(),
message: 'Pool de conexiones saludable',
},
{
name: 'Kafka Consumer',
status: 'degraded',
latency_ms: 250,
last_check: new Date().toISOString(),
message: 'El lag del consumidor excede el umbral',
},
];
}
Patrón 5: Despliegue de IDP de Nivel Producción
# docker-compose.yml: Despliegue en producción de Backstage
# Entorno: Docker Compose v2.30+ / PostgreSQL 16
version: "3.8"
services:
backstage:
image: mycompany/backstage:1.32.0
ports:
- "7007:7007"
environment:
APP_CONFIG_app_baseUrl: https://idp.mycompany.com
APP_CONFIG_backend_baseUrl: https://idp.mycompany.com
APP_CONFIG_backend_database_client: pg
APP_CONFIG_backend_database_connection_host: postgres
APP_CONFIG_backend_database_connection_port: "5432"
APP_CONFIG_backend_database_connection_user: ${POSTGRES_USER}
APP_CONFIG_backend_database_connection_password: ${POSTGRES_PASSWORD}
POSTGRES_HOST: postgres
POSTGRES_PORT: "5432"
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
GITHUB_TOKEN: ${GITHUB_TOKEN}
GITLAB_TOKEN: ${GITLAB_TOKEN}
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:7007/healthcheck"]
interval: 30s
timeout: 10s
retries: 3
postgres:
image: postgres:16-alpine
environment:
POSTGRES_DB: backstage_plugin
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
volumes:
postgres_data:
# kubernetes/backstage-deployment.yaml: Despliegue K8s en producción
# Entorno: Kubernetes 1.30+ / Helm 3.15+
apiVersion: apps/v1
kind: Deployment
metadata:
name: backstage
namespace: idp
labels:
app: backstage
spec:
replicas: 3
selector:
matchLabels:
app: backstage
template:
metadata:
labels:
app: backstage
spec:
containers:
- name: backstage
image: mycompany/backstage:1.32.0
ports:
- containerPort: 7007
env:
- name: APP_CONFIG_app_baseUrl
value: "https://idp.mycompany.com"
- name: APP_CONFIG_backend_baseUrl
value: "https://idp.mycompany.com"
- name: APP_CONFIG_backend_database_client
value: "pg"
- name: APP_CONFIG_backend_database_connection_host
valueFrom:
secretKeyRef:
name: backstage-db-secret
key: host
- name: APP_CONFIG_backend_database_connection_password
valueFrom:
secretKeyRef:
name: backstage-db-secret
key: password
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "2000m"
memory: "2Gi"
livenessProbe:
httpGet:
path: /healthcheck
port: 7007
initialDelaySeconds: 30
periodSeconds: 30
readinessProbe:
httpGet:
path: /healthcheck
port: 7007
initialDelaySeconds: 10
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: backstage
namespace: idp
spec:
selector:
app: backstage
ports:
- port: 80
targetPort: 7007
type: ClusterIP
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: backstage
namespace: idp
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
tls:
- hosts:
- idp.mycompany.com
secretName: backstage-tls
rules:
- host: idp.mycompany.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: backstage
port:
number: 80
# Despliegue con Helm (recomendado)
# Entorno: Helm 3.15+ / Kubernetes 1.30+
# 1. Agregar repositorio Helm de Backstage
helm repo add backstage https://backstage.github.io/charts
helm repo update
# 2. Crear archivo de valores override
cat > backstage-values.yaml << 'EOF'
backstage:
image:
repository: mycompany/backstage
tag: 1.32.0
extraAppConfig:
- configMapRef: backstage-config
extraEnvVarsSecrets:
- backstage-secrets
ingress:
enabled: true
host: idp.mycompany.com
tls:
enabled: true
secretName: backstage-tls
postgresql:
enabled: true
primary:
persistence:
size: 20Gi
auth:
existingSecret: backstage-db-secret
EOF
# 3. Desplegar
helm install backstage backstage/backstage \
-n idp \
--create-namespace \
-f backstage-values.yaml
# 4. Verificar
kubectl get pods -n idp
kubectl port-forward svc/backstage 7007:80 -n idp
Guía de Errores Comunes: 5 Trampas Frecuentes
Trampa 1: Entidad del Catalog No Aparece Después del Registro
❌ Incorrecto: Solo crear catalog-info.yaml localmente sin registrar un Location
✅ Correcto: Agregar locations en app-config.yaml, o registrar Location en la UI
Trampa 2: Fallos en la Construcción de TechDocs
❌ Incorrecto: Ejecutar MkDocs directamente en el host, las dependencias faltantes causan fallos de construcción
✅ Correcto: Configurar techdocs.generator.runIn: 'docker' para construcciones en contenedor
Trampa 3: Permisos Insuficientes del Token de Integración GitHub/GitLab
# ❌ Incorrecto: Token solo tiene permiso repo:read
# ✅ Correcto: El Token necesita estos permisos
# GitHub: repo, read:org, read:user
# GitLab: api, read_repository, read_api
Trampa 4: Hot Reload del Plugin No Funciona
# ❌ Incorrecto: No reiniciar el servidor de desarrollo después de modificar el código del plugin
# ✅ Correcto: Asegurarse de que yarn dev esté ejecutándose, Backstage soporta HMR hot reload
# Si HMR no funciona, limpiar caché y reiniciar:
yarn cache clean
yarn dev
Trampa 5: Agotamiento del Pool de Conexiones de Base de Datos en Producción
# app-config.yaml: Configurar pool de conexiones
backend:
database:
client: pg
connection:
host: postgres
port: 5432
user: ${POSTGRES_USER}
password: ${POSTGRES_PASSWORD}
pool:
min: 5
max: 20
idleTimeoutMillis: 30000
connectionTimeoutMillis: 5000
Tabla de Solución de Errores
| Mensaje de Error | Causa | Solución |
|---|---|---|
Failed to fetch catalog entities |
Token de GitHub/GitLab inválido o permisos insuficientes | Verificar permisos del Token, asegurar repo y read:org |
ECONNREFUSED 127.0.0.1:5432 |
PostgreSQL no está ejecutándose o configuración de conexión incorrecta | Verificar servicio de base de datos y parámetros de conexión |
TechDocs generation failed |
Fallo en construcción MkDocs, dependencias faltantes | Configurar runIn: 'docker', o instalar paquetes Python faltantes |
Plugin not found: xxx |
Plugin no registrado en el backend | Importar y registrar plugin en packages/backend/src/index.ts |
Unauthorized: Invalid token |
Configuración incorrecta de OIDC/OAuth | Verificar clientId/clientSecret y URL de callback |
CORS error |
Incompatibilidad de dominio frontend/backend | Configurar backend.cors.origin para coincidir con la URL del frontend |
Location not found |
URL de catalog-info.yaml inaccesible | Verificar accesibilidad de la URL y permisos del Token |
Out of memory |
Memoria heap de Node.js insuficiente | Configurar NODE_OPTIONS=--max-old-space-size=4096 |
Schema validation failed |
Error de formato en catalog-info.yaml | Validar con backstage-cli catalog-validate |
Template execution timeout |
Tiempo de espera agotado en ejecución de Software Template | Verificar solicitudes HTTP en el template, aumentar configuración de timeout |
Optimización Avanzada: 5 Consejos de Nivel Producción
Consejo 1: Auto-Descubrimiento del Catalog
// TypeScript: Procesador de auto-descubrimiento de GitLab
// Entorno: @backstage/plugin-catalog-backend-module-gitlab 0.8+
import { GitLabDiscoveryProcessor } from '@backstage/plugin-catalog-backend-module-gitlab';
const config = {
catalog: {
processors: [
GitLabDiscoveryProcessor.fromConfig(env.config, {
logger: env.logger,
schedule: {
frequency: { minutes: 30 },
timeout: { minutes: 3 },
initialDelay: { seconds: 15 },
},
}),
],
},
};
Consejo 2: Control de Permisos RBAC
# app-config.yaml: Configuración RBAC
# Entorno: @backstage/plugin-permission-backend 0.6+
permission:
enabled: true
rbac:
policies:
- name: 'role:default/developer'
permissions:
- 'catalog.entity.read'
- 'techdocs.read'
- 'scaffolder.template.execute'
- name: 'role:default/admin'
permissions:
- 'catalog.entity.create'
- 'catalog.entity.delete'
- 'catalog.entity.update'
- 'techdocs.write'
- 'scaffolder.template.create'
policyFile: ./rbac-policy.csv
Consejo 3: Mejora de Búsqueda
# app-config.yaml: Configuración del motor de búsqueda
# Entorno: @backstage/plugin-search-backend-module-elasticsearch 1.6+
search:
collators:
techdocs:
schedule:
frequency: { minutes: 10 }
timeout: { minutes: 5 }
initialDelay: { seconds: 30 }
elasticsearch:
url: ${ELASTICSEARCH_URL}
auth:
username: ${ELASTICSEARCH_USER}
password: ${ELASTICSEARCH_PASSWORD}
indexPrefix: backstage_search_
Consejo 4: Integración ArgoCD Multi-Cluster
# app-config.yaml: Integración multi-cluster de ArgoCD
# Entorno: @backstage/plugin-argocd 2.5+
argocd:
appLocatorMethods:
- type: 'config'
instances:
- name: production
url: https://argocd.mycompany.com
token: ${ARGOCD_TOKEN_PROD}
- name: staging
url: https://argocd-staging.mycompany.com
token: ${ARGOCD_TOKEN_STAGING}
Consejo 5: Plantillas Golden Path
# templates/java-spring-boot/template.yaml: Plantilla de scaffolding Golden Path
# Entorno: @backstage/plugin-scaffolder 1.25+
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: java-spring-boot-service
title: Plantilla de Microservicio Java Spring Boot
description: Crear un microservicio Spring Boot estandarizado con CI/CD, monitoreo y documentación
spec:
owner: platform-team
type: service
parameters:
- title: Información del Servicio
required:
- name
- owner
properties:
name:
title: Nombre del Servicio
type: string
pattern: '^[a-z][a-z0-9-]*$'
description: Solo letras minúsculas, números y guiones
owner:
title: Equipo Propietario
type: string
ui:field: OwnerPicker
ui:options:
allowedKinds:
- Group
description:
title: Descripción del Servicio
type: string
description: Descripción de una línea de la funcionalidad del servicio
database:
title: Tipo de Base de Datos
type: string
default: postgresql
enum:
- postgresql
- mysql
- mongodb
- none
- title: Configuración CI/CD
properties:
ciProvider:
title: Herramienta CI
type: string
default: github-actions
enum:
- github-actions
- gitlab-ci
- jenkins
deployTarget:
title: Objetivo de Despliegue
type: string
default: kubernetes
enum:
- kubernetes
- ecs
- cloud-run
steps:
- id: fetch-template
name: Obtener Código de Plantilla
action: fetch:template
input:
url: ./skeleton
values:
name: ${{ parameters.name }}
owner: ${{ parameters.owner }}
description: ${{ parameters.description }}
database: ${{ parameters.database }}
ciProvider: ${{ parameters.ciProvider }}
deployTarget: ${{ parameters.deployTarget }}
- id: publish
name: Publicar en Repositorio Git
action: publish:github
input:
allowedHosts: ['github.com']
description: ${{ parameters.description }}
repoUrl: github.com?owner=mycompany&repo=${{ parameters.name }}
defaultBranch: main
protectDefaultBranch: true
- id: register
name: Registrar en el Catalog
action: catalog:register
input:
repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
catalogInfoPath: '/catalog-info.yaml'
output:
links:
- title: URL del Repositorio
url: ${{ steps.publish.output.remoteUrl }}
- title: Abrir en el Catalog
url: https://idp.mycompany.com/catalog/default/component/${{ parameters.name }}
Comparación de Plataformas IDP
| Dimensión | Backstage | Port | Humanitec |
|---|---|---|---|
| Código Abierto | ✅ Apache 2.0 | ❌ SaaS Comercial | ❌ SaaS Comercial |
| Self-Hosted | ✅ Control total | ❌ Alojado en la nube | ❌ Alojado en la nube |
| Ecosistema de Plugins | 200+ plugins comunitarios | Integraciones integradas | Integraciones integradas |
| Curva de Aprendizaje | Media-Alta (TypeScript) | Baja (configuración visual) | Media (ingeniería de plataformas) |
| Personalización | Muy Alta (nivel de código) | Media (extensión API) | Media (definición Score) |
| Software Catalog | ✅ Función principal | ✅ Función principal | ✅ Función principal |
| TechDocs | ✅ Soporte nativo | ❌ Necesita integración | ❌ Necesita integración |
| Costo | Esfuerzo de ingeniería | $50-200/usuario/mes | $100-500/usuario/mes |
| Escala Adecuada | 50-5000+ desarrolladores | 10-500 desarrolladores | 100-5000+ desarrolladores |
| Clientes Destacados | Spotify, American Airlines | Lyft, Palo Alto | BMW, Qonto |
Conclusión
Backstage es la opción óptima para construir una Plataforma de Desarrollador Interna en 2026 — de código abierto, autoalojable, con un rico ecosistema de plugins. Pero ten en cuenta:
- Equipos pequeños (<50): Considera primero Port o soluciones SaaS similares; el mantenimiento de Backstage puede no ser rentable
- Equipos medianos (50-500): Backstage es la mejor opción, 2-3 ingenieros para mantenimiento
- Equipos grandes (500+): Backstage + plantillas Golden Path + auto-descubrimiento para un sistema completo de ingeniería de plataformas
Factores clave de éxito: Comienza con Catalog y TechDocs, agrega plugins gradualmente, luego construye plantillas Golden Path. No intentes hervir el océano.
Herramientas Online Recomendadas
- /es-419/json/format - Formateador JSON para conversión YAML-a-JSON de entidades del Catalog
- /es-419/dev/curl-to-code - Generador de código desde cURL para llamadas a la API de Backstage
- /es-419/encode/hash - Calculadora de hash para generar identificadores únicos de servicios
- /es-419/text/diff - Diff de texto para comparar cambios en catalog-info.yaml
Prueba estas herramientas que se ejecutan en tu navegador — no requieren registro →