Backstage Series #2: Genera manifiestos de Kubernetes y crea PRs desde tu portal

Actualizado en marzo 2026: Este artículo usa Backstage v1.49+, el plugin de Kubernetes
@backstage/plugin-kubernetesactualizado, yautoscaling/v2para los manifiestos de HPA.
Lo que vas a necesitar
Esta es la segunda entrega de la serie de Backstage. Antes de arrancar, asegúrate de tener estos dos posts completados:
- Backstage Series #1: Primeros pasos con Backstage — instalación y primer template.
- Kubernetes: Qué es, para qué sirve y cómo montar tu primer cluster local — cluster local con Kind listo.
En el post anterior creamos nuestra instancia de Backstage y vimos cómo funcionan los Software Templates. Hoy vamos a conectar esas dos piezas: Backstage va a hablar con tu cluster de Kubernetes, y desde un template vas a poder generar manifiestos y enviarlos como Pull Request a un repositorio de configuración.
Pre-requisitos
| Herramienta | Versión | Verificar |
|---|---|---|
| Backstage | v1.49+ | yarn backstage-cli info |
| OrbStack (macOS) o Docker Desktop (Windows/Linux) | — | Runtime de contenedores |
| Kind | v0.20+ | kind version |
| kubectl | v1.35+ | kubectl version --client |
| GitHub Token | con permisos repo | Configurado en app-config.yaml |
Tu cluster de Kind debería estar corriendo. Si seguiste el post de Kubernetes ya tienes OrbStack (macOS) o Docker Desktop (Windows/Linux) + Kind configurados:
# Verificar que el cluster existe
kind get clusters
# Si no lo tienes, créalo
kind create cluster --name mi-clusterPaso 1: Crear un namespace manualmente
Antes de automatizar nada, vamos a crear un namespace a mano para entender qué es y para qué sirve. Un namespace en Kubernetes es como una carpeta que agrupa y aísla recursos. Permite que múltiples equipos o proyectos convivan en el mismo cluster sin pisarse.
# Crear un namespace
kubectl create namespace staging
# Verificar que se creó
kubectl get namespacesDeberías ver algo así:
NAME STATUS AGE
default Active 5d
kube-node-lease Active 5d
kube-public Active 5d
kube-system Active 5d
staging Active 3sTambién puedes crear un namespace con un manifiesto YAML:
apiVersion: v1
kind: Namespace
metadata:
name: staging
labels:
environment: stagingY aplicarlo con kubectl apply -f namespace.yaml. Esto es lo que haremos más adelante desde Backstage.
Creemos un par de namespaces más que usaremos después:
kubectl create namespace development
kubectl create namespace productionPaso 2: Instalar el plugin de Kubernetes en Backstage
Ahora vamos a darle ojos a Backstage para que pueda ver lo que hay dentro de tu cluster. El plugin de Kubernetes tiene dos partes: frontend (lo que ves en la UI) y backend (la conexión con el cluster).
Instalar las dependencias
# Frontend: agrega la pestaña de Kubernetes en las páginas de entidades
yarn --cwd packages/app add @backstage/plugin-kubernetes
# Backend: conecta Backstage con tu cluster
yarn --cwd packages/backend add @backstage/plugin-kubernetes-backendRegistrar el backend
En packages/backend/src/index.ts, agrega el plugin:
const backend = createBackend();
// ... otros plugins ...
backend.add(import('@backstage/plugin-kubernetes-backend'));
backend.start();Agregar la pestaña en la UI
En packages/app/src/components/catalog/EntityPage.tsx, agrega la pestaña de Kubernetes a las páginas de servicios:
import { EntityKubernetesContent } from '@backstage/plugin-kubernetes';
// Dentro de serviceEntityPage, agrega esta ruta:
<EntityLayout.Route path="/kubernetes" title="Kubernetes">
<EntityKubernetesContent refreshIntervalMs={30000} />
</EntityLayout.Route>Paso 3: Configurar la conexión al cluster
Abre tu app-config.yaml y agrega la configuración de Kubernetes:
kubernetes:
serviceLocatorMethod:
type: 'multiTenant'
clusterLocatorMethods:
- type: 'config'
clusters:
- url: https://127.0.0.1:<PUERTO_API>
name: mi-cluster
authProvider: 'serviceAccount'
skipTLSVerify: true
skipMetricsLookup: true
serviceAccountToken: ${K8S_SERVICE_ACCOUNT_TOKEN}Obtener el token y la URL
Para conectar con tu cluster de Kind necesitas el token de un Service Account y la URL del API server:
# Obtener la URL del API server
kubectl cluster-info
# Salida: Kubernetes control plane is running at https://127.0.0.1:XXXXX
# Crear un Service Account para Backstage
kubectl create serviceaccount backstage -n default
# Darle permisos de lectura al cluster
kubectl create clusterrolebinding backstage-reader \
--clusterrole=view \
--serviceaccount=default:backstage
# Generar un token
kubectl create token backstage -n default --duration=8760hCopia el token generado y expórtalo como variable de entorno antes de iniciar Backstage:
export K8S_SERVICE_ACCOUNT_TOKEN="eyJhbGciOiJS..."Verificar la conexión
Inicia Backstage con yarn start y navega a cualquier entidad del catálogo. Deberías ver la pestaña Kubernetes disponible. Si tienes deployments corriendo con la anotación correcta, verás los pods y su estado.
Para que una entidad del catálogo muestre sus recursos de Kubernetes, agrega estas anotaciones al catalog-info.yaml:
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: mi-servicio
annotations:
backstage.io/kubernetes-id: mi-servicio
backstage.io/kubernetes-namespace: staging
spec:
type: service
lifecycle: production
owner: platform-teamPaso 4: Crear el Software Template
Acá es donde todo se pone interesante. Vamos a crear un Software Template que:
- Le pide al usuario: nombre del servicio, namespace, imagen Docker, réplicas, etc.
- Genera los manifiestos de Kubernetes (Deployment, Service, HPA).
- Abre un Pull Request en un repositorio de configuración con los manifiestos generados.
Estructura del template
templates/
└── k8s-deployment/
├── template.yaml # Definición del template
└── skeleton/
└── manifests/
├── deployment.yaml # Template del Deployment
├── service.yaml # Template del Service
└── hpa.yaml # Template del HPAEl template principal: template.yaml
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: k8s-deployment-generator
title: Generar Deployment de Kubernetes
description: |
Genera manifiestos de Kubernetes (Deployment, Service, HPA) y
crea un Pull Request en el repositorio de configuración.
tags:
- kubernetes
- deployment
- gitops
spec:
owner: platform-team
type: service
parameters:
- title: Información del servicio
required:
- serviceName
- namespace
- image
properties:
serviceName:
title: Nombre del servicio
type: string
description: Nombre del deployment y service en Kubernetes
ui:autofocus: true
pattern: '^[a-z0-9-]+$'
namespace:
title: Namespace
type: string
description: Namespace donde se desplegará el servicio
enum:
- development
- staging
- production
default: staging
image:
title: Imagen Docker
type: string
description: "Imagen completa (ej: ghcr.io/mi-org/mi-app:latest)"
containerPort:
title: Puerto del contenedor
type: integer
default: 8080
- title: Escalado
properties:
replicas:
title: Réplicas iniciales
type: integer
default: 2
minimum: 1
maximum: 10
enableHPA:
title: Habilitar autoescalado (HPA)
type: boolean
default: true
minReplicas:
title: Mínimo de réplicas (HPA)
type: integer
default: 2
minimum: 1
maxReplicas:
title: Máximo de réplicas (HPA)
type: integer
default: 8
minimum: 2
maximum: 20
cpuTarget:
title: "Target CPU (%)"
type: integer
default: 50
minimum: 10
maximum: 90
- title: Repositorio destino
required:
- repoUrl
properties:
repoUrl:
title: Repositorio de configuración
type: string
ui:field: RepoUrlPicker
ui:options:
requestUserCredentials:
secretsKey: USER_OAUTH_TOKEN
allowedHosts:
- github.com
steps:
- id: fetch-manifests
name: Generar manifiestos de Kubernetes
action: fetch:template
input:
url: ./skeleton
targetPath: ./generated
values:
serviceName: ${{ parameters.serviceName }}
namespace: ${{ parameters.namespace }}
image: ${{ parameters.image }}
containerPort: ${{ parameters.containerPort }}
replicas: ${{ parameters.replicas }}
enableHPA: ${{ parameters.enableHPA }}
minReplicas: ${{ parameters.minReplicas }}
maxReplicas: ${{ parameters.maxReplicas }}
cpuTarget: ${{ parameters.cpuTarget }}
- id: create-pr
name: Crear Pull Request
action: publish:github:pull-request
input:
repoUrl: ${{ parameters.repoUrl }}
branchName: feat/deploy-${{ parameters.serviceName }}-${{ parameters.namespace }}
title: "feat: add K8s manifests for ${{ parameters.serviceName }} (${{ parameters.namespace }})"
description: |
## Manifiestos generados por Backstage
- **Servicio**: ${{ parameters.serviceName }}
- **Namespace**: ${{ parameters.namespace }}
- **Imagen**: ${{ parameters.image }}
- **Réplicas**: ${{ parameters.replicas }}
- **HPA**: ${{ parameters.enableHPA }}
Generado automáticamente desde el portal de Backstage.
sourcePath: ./generated
targetPath: services/${{ parameters.serviceName }}/
token: ${{ secrets.USER_OAUTH_TOKEN }}
output:
links:
- title: Ver Pull Request
url: ${{ steps['create-pr'].output.remoteUrl }}
- title: Ver en el catálogo
icon: catalog
entityRef: ${{ steps['create-pr'].output.targetBranchName }}Los skeleton templates
Estos son los archivos que se procesan con Nunjucks. Las variables ${{ values.xxx }} se reemplazan con lo que el usuario ingresó en el formulario.
skeleton/manifests/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ${{ values.serviceName }}
namespace: ${{ values.namespace }}
labels:
app: ${{ values.serviceName }}
managed-by: backstage
spec:
replicas: ${{ values.replicas }}
selector:
matchLabels:
app: ${{ values.serviceName }}
template:
metadata:
labels:
app: ${{ values.serviceName }}
spec:
containers:
- name: ${{ values.serviceName }}
image: ${{ values.image }}
ports:
- containerPort: ${{ values.containerPort }}
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "256Mi"
livenessProbe:
httpGet:
path: /health
port: ${{ values.containerPort }}
initialDelaySeconds: 15
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: ${{ values.containerPort }}
initialDelaySeconds: 5
periodSeconds: 5skeleton/manifests/service.yaml
apiVersion: v1
kind: Service
metadata:
name: ${{ values.serviceName }}
namespace: ${{ values.namespace }}
labels:
app: ${{ values.serviceName }}
managed-by: backstage
spec:
type: ClusterIP
ports:
- port: 80
targetPort: ${{ values.containerPort }}
protocol: TCP
selector:
app: ${{ values.serviceName }}skeleton/manifests/hpa.yaml
{%- if values.enableHPA %}
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: ${{ values.serviceName }}-hpa
namespace: ${{ values.namespace }}
labels:
app: ${{ values.serviceName }}
managed-by: backstage
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: ${{ values.serviceName }}
minReplicas: ${{ values.minReplicas }}
maxReplicas: ${{ values.maxReplicas }}
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: ${{ values.cpuTarget }}
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 70
{%- endif %}{%- if values.enableHPA %} del HPA. Si el usuario no marca la opción de autoescalado, el archivo se genera vacío. El - en {%- elimina el whitespace antes del bloque, así el YAML queda limpio.Paso 5: Registrar el template en Backstage
Agrega la ubicación del template en tu app-config.yaml:
catalog:
locations:
- type: file
target: ../../templates/k8s-deployment/template.yaml
rules:
- allow: [Template]Reinicia Backstage y ve a Create… → Generar Deployment de Kubernetes. Deberías ver un formulario con tres pasos:
- Información del servicio: nombre, namespace, imagen, puerto.
- Escalado: réplicas, HPA, targets de CPU.
- Repositorio destino: donde se abrirá el PR.
Paso 6: Probar el flujo completo
Vamos a probarlo de punta a punta:
Crea un repositorio en GitHub llamado
k8s-configs(o el nombre que prefieras). Este será tu repo de manifiestos.Ve a Backstage → Create → Generar Deployment de Kubernetes.
Llena el formulario:
- Servicio:
api-users - Namespace:
staging - Imagen:
ghcr.io/mi-org/api-users:v1.0.0 - Puerto:
8080 - Réplicas:
2 - HPA: habilitado
- Repositorio:
github.com?repo=k8s-configs&owner=tu-usuario
- Servicio:
Click en Create. Backstage ejecutará los pasos, generará los manifiestos y abrirá un PR en tu repo.
Revisa el PR en GitHub. Deberías ver la estructura:
services/
└── api-users/
└── manifests/
├── deployment.yaml
├── service.yaml
└── hpa.yamlCon los valores que ingresaste ya interpolados en los YAML.
El flujo completo con GitOps
Hasta aquí ya tienes un flujo potente: un desarrollador llena un formulario en Backstage, se genera un PR con manifiestos de Kubernetes, y alguien del equipo lo revisa y aprueba. Pero… ¿qué pasa después del merge?
Ahí es donde entra ArgoCD y el concepto de GitOps.
La idea es esta:
- Backstage genera los manifiestos y crea el PR (lo que hicimos hoy).
- El equipo revisa y aprueba el PR en GitHub.
- ArgoCD está escuchando el repositorio. Cuando detecta un cambio en
main, automáticamente sincroniza los manifiestos con el cluster de Kubernetes. - El cluster se actualiza sin que nadie tenga que correr
kubectl apply.
Esto es GitOps en su forma más pura: el repositorio es la fuente de verdad. Si quieres cambiar algo en el cluster, cambias el repo. Si quieres revertir, haces revert del commit. El estado del cluster siempre refleja lo que hay en Git.
Resumen
| Paso | Qué hicimos |
|---|---|
| 1 | Creamos namespaces manualmente en Kubernetes |
| 2 | Instalamos el plugin de Kubernetes en Backstage (frontend + backend) |
| 3 | Configuramos la conexión al cluster local (Service Account + token) |
| 4 | Creamos un Software Template que genera Deployment, Service y HPA |
| 5 | Registramos el template y probamos el formulario |
| 6 | Generamos manifiestos y abrimos un PR en GitHub desde Backstage |