Plataforma de Desarrollador Interna IDP: Construyendo un Portal de Desarrollador Empresarial con Backstage 2026

DevOps

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

  1. 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
  2. Documentación dispersa: Confluence, GitLab Wiki, Notion... no se encuentra, no se entiende
  3. Incorporación lenta de proyectos: Reconstruir el scaffolding desde cero cada vez, CI/CD, monitoreo, configuración de logging es repetitivo
  4. Cadena de herramientas fragmentada: Jenkins, ArgoCD, Grafana, Jira... el cambio constante de contexto mata la productividad
  5. 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

Prueba estas herramientas que se ejecutan en tu navegador — no requieren registro →

#内部开发者平台#Backstage#IDP#开发者门户#平台工程#2026#DevOps运维