El ecosistema de plugins: la carta del restaurante
Tiempo de lectura: 22 min · Itinerarios: cocinero jefe, consultor, CTO. Lee el apéndice :cap-F antes si nunca has tocado un plugin de Backstage.
|
Objetivos del capítulo
Al terminar este capítulo vas a poder:
|
El menú ya no es solo el Software Catalog
En el capítulo :cap-04 vimos el corazón de Backstage: el Software Catalog. Pero un restaurante con un único plato no sobrevive. Backstage vive o muere por su ecosistema de plugins — la capacidad de añadir Kubernetes, Jenkins, ArgoCD, Snyk, Tech Radar, Cost Insights, Datadog, GitHub Actions y cien integraciones más sin reescribir el core.
Spotify descubrió esto en 2020: su instancia interna tiene más de 200 plugins. La comunidad open source mantiene otros tantos. Backstage es, técnicamente, un agregador de UIs sobre datos que ya tienes en otros sistemas. Su valor está en la densidad del ecosistema, no en lo que hace el core.
Backstage sin plugins es como un restaurante que solo sirve agua. Sí, técnicamente cumple con "dar de beber", pero nadie vuelve.
Anatomía de un plugin
Un plugin de Backstage no es "un componente React". Es un paquete npm con un manifiesto concreto. Hay cuatro tipos:
| Tipo | Qué hace | Dónde corre |
|---|---|---|
Frontend plugin |
Añade rutas, páginas, tarjetas al portal |
En el bundle del frontend (Vite/Webpack) |
Backend plugin |
Expone APIs REST/GraphQL a otros plugins |
En el proceso Node del backend |
Módulo (backend module) |
Extiende un backend plugin sin reemplazarlo |
En el mismo proceso backend |
Scaffolder action |
Acción reutilizable dentro de un template |
En el Scaffolder backend |
Cada tipo tiene un factory concreto:
// Frontend plugin (legacy App Router)
import { createPlugin, createRouteRef } from '@backstage/core-plugin-api';
export const miPlugin = createPlugin({
id: 'mi-plugin',
routes: {
root: createRouteRef({ id: 'mi-plugin/root' }),
},
});
export const MiPluginPage = miPlugin.provide(
createRoutableExtension({
name: 'MiPluginPage',
component: () => import('./components/Page').then(m => m.Page),
mountPoint: miPlugin.routes.root,
}),
);
// Backend plugin (New Backend System)
import { createBackendPlugin, coreServices } from '@backstage/backend-plugin-api';
export const miBackendPlugin = createBackendPlugin({
pluginId: 'mi-plugin',
register(env) {
env.registerInit({
deps: {
logger: coreServices.logger,
httpRouter: coreServices.httpRouter,
},
async init({ logger, httpRouter }) {
httpRouter.use(await createRouter({ logger }));
},
});
},
});
// Módulo (extiende catalog sin reemplazarlo)
import { createBackendModule, coreServices } from '@backstage/backend-plugin-api';
export const miModulo = createBackendModule({
pluginId: 'catalog',
moduleId: 'mi-modulo',
register(env) {
env.registerInit({
deps: { catalog: catalogServiceRef },
async init({ catalog }) {
catalog.addProcessor(new MiProcessor());
},
});
},
});
|
Si vienes de un plugin de la versión 1.x, el patrón "old school" era crear un Esto te permite crear plugins que el equipo A no sabe que existen, instalarlos sin tocar el |
El catálogo oficial de plugins
Backstage no tiene una "App Store" cerrada como Slack. Tiene un marketplace abierto en el monorepo oficial, con ~250 plugins mantenidos por la comunidad. Los más populares, que verás en cualquier instalación real:
| Plugin | Categoría | Para qué sirve |
|---|---|---|
|
DevOps |
Ver pods, deployments, services del cluster desde Backstage |
|
CI |
Estado de jobs, última build, links a artefactos |
|
CI |
Estado de workflows de GitHub Actions por repo |
|
GitOps |
Estado de aplicaciones ArgoCD, sync status, manifests |
|
Arquitectura |
Visualizar adopciones, trials, holds de tecnologías |
|
FinOps |
Coste por equipo, alertas de overspend |
|
Seguridad |
Vulnerabilidades por repo, severidad, fixes disponibles |
|
Calidad |
Auditorías de performance y accesibilidad por URL |
Spotify mantiene los del core (catalog, scaffolder, techdocs, kubernetes, etc.). La comunidad mantiene los demás, cada uno con su cadencia de releases.
|
¿Puedes usar plugins de fuera del monorepo oficial? Sí, de hecho es lo recomendado. Para plugins internos de tu empresa (mi-equipo-dashboard, mi-cost-tracker, mi-deploy-bot) lo correcto es publicarlos en tu propio registro npm privado o en GitHub Packages. Backstage los descubre si configuras |
Plugin-paths: cómo descubre Backstage plugins externos
Por defecto, Backstage solo mira dentro de packages/ y plugins/ de su propio monorepo. Si quieres instalar plugins de fuera (un paquete publicado en npm), tienes dos opciones:
Opción 1: pluginPackages en app-config.yaml
# app-config.yaml
dynamicPlugins:
frontend:
backstage-plugin-kubernetes:
mountPoints:
- mountPoint: entity.catalog/overview/item
module: KubernetesContent
importName: EntityKubernetesContent
Esto le dice a Backstage: "el plugin Kubernetes monta su contenido en la pestaña de overview del catalog". El NFS (New Frontend System) lo lee al arrancar y enlaza las extensiones.
Opción 2: pluginPaths (legacy App Router)
// packages/app/src/index.ts (legacy)
import { registerPluginImportSideEffects } from '@backstage/frontend-plugin-api';
registerPluginImportSideEffects([
{
importName: 'KubernetesPlugin',
module: () => import('@backstage/plugin-kubernetes'),
},
]);
pluginPaths carga dinámicamente plugins externos. Es la antesala de los dynamic plugins que llegaron con Backstage 1.30.
Dynamic plugins (Backstage 1.30+)
Antes de 1.30, instalar un plugin en producción requería recompilar el bundle del frontend y hacer un redeploy completo. Backstage 1.30 introduce dynamic plugins: plugins empaquetados como bundles pre-compilados que se montan en caliente sin recompilar nada.
Dynamic plugins es lo que pasa cuando el equipo de plataforma decide que recompilar el frontend cada vez que alguien quiere un plugin es tan 2019 como usar npm install sin package-lock.json.
|
El NFS es el camino a seguir para todo proyecto nuevo. Spotify recomienda que adoptes el NFS desde el día uno si empiezas en 1.30+. Si vienes del legacy App Router, la migración es un trabajo de uno o dos sprints: sustituir |
El "metadata" de un plugin publicable
Para que un plugin sea descubrible, instalable y mantenible, su package.json debe declarar un bloque backstage:
{
"name": "@mi-empresa/plugin-deploy-bot",
"version": "1.2.3",
"description": "Inicia deploys a Kubernetes desde Backstage",
"backstage": {
"role": "frontend-plugin",
"pluginId": "deploy-bot",
"pluginPackages": [
"@mi-empresa/plugin-deploy-bot",
"@mi-empresa/plugin-deploy-bot-backend"
]
},
"keywords": [
"backstage",
"backstage-plugin",
"kubernetes",
"deploy"
]
}
Este manifiesto permite:
-
Que el
backstage-clireconozca el plugin sin configuración adicional. -
Que la búsqueda de plugins en el marketplace filtre por compatibilidad.
-
Que las herramientas de auditoría (
backstage-plugin-audit,dependency-cruiser) detecten versiones incompatibles.
|
Regla de oro: si tu plugin no tiene bloque |
Escribir un plugin que el equipo vecino pueda instalar
El test definitivo de un plugin es: ¿puede otro equipo instalarlo sin tocar tu packages/app/src/index.ts? Si la respuesta es sí, has ganado. Vamos a verlo con un ejemplo:
Estructura del repo
mi-plugin-deploy/
├── packages/
│ ├── deploy-bot/ # frontend plugin
│ │ ├── package.json # bloque "backstage"
│ │ ├── src/
│ │ │ ├── plugin.ts # createFrontendPlugin
│ │ │ └── components/
│ │ └── README.md
│ └── deploy-bot-backend/ # backend plugin
│ ├── package.json
│ ├── src/
│ │ ├── module.ts # createBackendPlugin
│ │ └── router.ts
│ └── README.md
└── README.md
Frontend plugin (NFS)
// packages/deploy-bot/src/plugin.ts
import { createFrontendPlugin, createEntityCardExtension } from '@backstage/frontend-plugin-api';
export const deployBotPlugin = createFrontendPlugin({
pluginId: 'deploy-bot',
extensions: [
createEntityCardExtension({
name: 'DeployBotCard',
// El equipo vecino lo monta desde su app-config.yaml
// sin tocar este código.
}),
],
});
export default deployBotPlugin;
Backend plugin
// packages/deploy-bot-backend/src/router.ts
import { httpRouterServiceRef } from '@backstage/backend-plugin-api';
import express from 'express';
export async function createRouter() {
const router = express.Router();
router.get('/health', (_req, res) => res.json({ status: 'ok' }));
return router;
}
// packages/deploy-bot-backend/src/module.ts
import { createBackendPlugin, coreServices } from '@backstage/backend-plugin-api';
import { createRouter } from './router';
export const deployBotBackend = createBackendPlugin({
pluginId: 'deploy-bot',
register(env) {
env.registerInit({
deps: {
logger: coreServices.logger,
httpRouter: coreServices.httpRouter,
},
async init({ logger, httpRouter }) {
logger.info('Deploy bot backend starting');
httpRouter.use(await createRouter());
},
});
},
});
Cómo lo instala el equipo vecino
Solo añaden dos líneas a su packages/backend/src/index.ts:
// En el backend del equipo vecino
backend.add(import('@mi-empresa/plugin-deploy-bot-backend'));
Y en su app-config.yaml:
dynamicPlugins:
frontend:
'@mi-empresa/plugin-deploy-bot':
mountPoints:
- mountPoint: entity.catalog/overview/item
module: DeployBotPlugin
importName: DeployBotCard
Listo. Cero cambios en su App.tsx. El plugin declara sus propias extensiones y el NFS las monta en la pestaña de overview del catalog. Esa es la diferencia entre un plugin moderno y un plugin legacy.
Errores comunes
|
Errores típicos al empezar con plugins
|
Cuándo construir un plugin vs cuándo comprar SaaS
Si alguien te dice "construyamos un panel de Kubernetes en Backstage", la respuesta correcta es casi siempre "ya existe y se llama `@backstage/plugin-kubernetes`". Solo constrúyelo si el oficial no cubre el 80% de tu caso. La regla del restaurante: no inventes un plato si ya hay uno en la carta.
Las señales de que deberías construir tu propio plugin:
-
El dato que necesitas no existe en ningún sistema que Backstage pueda consultar.
-
El flujo de trabajo es único de tu empresa y tiene 0% de uso fuera de ella.
-
Ningún plugin oficial tiene más del 30% de overlap con tu necesidad.
Las señales de que NO deberías:
-
Spotify/Roadie/Red Hat ya lo construyeron.
-
Estarías reimplementando un dashboard que Kibana/Grafana/Datadog ya hace mejor.
-
El coste de mantenerlo supera el coste de comprar un SaaS.
Resumen
-
Backstage es un agregador de UIs. Su valor está en la densidad de plugins, no en el core.
-
Hay cuatro tipos: frontend plugin, backend plugin, módulo, scaffolder action.
-
El New Frontend System (≥ 1.30) reemplaza el App Router clásico: plugins declaran extensiones y el integrador las activa desde YAML.
-
Dynamic plugins te permiten instalar sin recompilar.
-
Un plugin publicable necesita un bloque
backstageenpackage.json, no asumir rutas hardcoded, y respetarpeerDependencies.
Glosario del capítulo
- Plugin
-
Paquete npm con un bloque
backstageenpackage.jsonque añade funcionalidad a Backstage. - Backend plugin
-
Plugin que expone APIs REST/GraphQL desde el proceso backend.
- Frontend plugin
-
Plugin que añade rutas, páginas, tarjetas al bundle del frontend.
- Módulo
-
Extensión de un plugin existente sin reemplazarlo (ej: añadir un processor al catalog).
- Scaffolder action
-
Acción reutilizable dentro de un template.
- New Frontend System (NFS)
-
Sistema de extensiones que reemplaza el App Router clásico desde 1.30.
- Dynamic plugin
-
Plugin empaquetado como bundle pre-compilado que se monta sin recompilar.
- App Router clásico
-
Sistema de routing legacy donde el integrador importa cada plugin manualmente.
Próximo capítulo
Capítulo 18 — Seguridad: cuando el portal tiene las llaves de la cocina — cerraduras, modelado de amenazas, secretos del Scaffolder y cómo defender tu IDP de los CVEs reales (CVE-2025-32791, CVE-2025-55285, etc.) que ya han aparecido en producción.