Software Templates avanzados: el golden path completo
|
Objetivos del capítulo
Al terminar este capítulo vas a poder:
|
El golden path: cuando el template hace TODO
Un golden path es el camino óptimo que tu organización quiere que cualquier developer siga para crear un nuevo servicio. En Backstage, un Software Template es la implementación literal: pulsas un botón y obtienes un servicio listo para producción con todo lo que necesitas.
Un golden path sin automatizar es como tener la receta de la paella en una pizarra: todo el mundo sabe que existe, pero nadie la sigue porque requiere ir al mercado, cortar cebollas, etc. Un golden path automatizado es pedir la paella por teléfono: llega en 30 minutos.
El template mínimo que vimos en :cap-12 creaba solo un repo de GitHub. Un golden path real hace mucho más:
|
Golden path completo: 7 cosas que un developer necesita el día 1
|
En este capítulo vamos a construir un template que provisiona todo eso en un solo click.
Anatomía de un template complejo
Un template tiene dos partes:
-
template.yaml: definición declarativa del form, pasos, owner, tipo. Es el manifiesto. -
skeleton/: directorio con el código base que se copia al repo. Contiene.github/workflows/,Dockerfile,mkdocs.yml, etc.
# template.yaml — golden path completo
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: golden-path-service
title: Crear un nuevo servicio (golden path)
description: |
Provisiona un servicio completo: repo + CI + catalog + namespace + docs.
Para equipos que siguen las prácticas de platform engineering.
tags:
- recommended
- production-ready
spec:
owner: platform/team-x
type: service
parameters:
- title: Información básica
required:
- name
- owner
- repoUrl
properties:
name:
title: Nombre del servicio
type: string
description: |
kebab-case, sin prefijos del equipo. Ej: payments-svc.
ui:field: EntityNamePicker
ui:options:
# Validar formato
pattern: '^[a-z][a-z0-9-]{2,40}$'
description:
title: Descripción
type: string
ui:autofocus: true
owner:
title: Equipo owner
type: string
ui:field: OwnerPicker
ui:options:
catalogFilter:
kind: Group
repoUrl:
title: Repositorio de GitHub
type: string
ui:field: RepoUrlPicker
ui:options:
allowedHosts:
- github.com
- title: Stack técnico
properties:
language:
title: Lenguaje
type: string
enum:
- typescript
- python
- go
- rust
default: typescript
database:
title: Base de datos
type: string
enum:
- postgres
- mongodb
- none
default: postgres
observability:
title: Stack de observabilidad
type: string
enum:
- datadog
- grafana-stack
default: datadog
- title: Producción
required:
- tier
properties:
tier:
title: Tier del servicio
type: string
enum:
- tier-1 # crítico
- tier-2 # importante
- tier-3 # desarrollo
default: tier-2
region:
title: Región de despliegue
type: string
enum:
- eu-west-1
- us-east-1
- ap-southeast-1
default: eu-west-1
steps:
# 1. Crear repo
- id: create-repo
name: Crear repositorio en GitHub
action: publish:github
input:
repoUrl: ${{ parameters.repoUrl }}
description: ${{ parameters.description }}
defaultBranch: main
repoVisibility: private
allowAutoMerge: true
deleteBranchOnMerge: true
# 2. Esperar a que GitHub cree el repo
- id: wait
name: Esperar disponibilidad del repo
action: catalog:wait
input:
repoUrl: ${{ parameters.repoUrl }}
timeout: 60
# 3. Push skeleton con código
- id: push-skeleton
name: Copiar skeleton al repo
action: fetch:template
input:
url: ./skeleton
targetPath: ./
values:
name: ${{ parameters.name }}
language: ${{ parameters.language }}
description: ${{ parameters.description }}
owner: ${{ parameters.owner }}
tier: ${{ parameters.tier }}
region: ${{ parameters.region }}
# 4. Crear catalog entity
- id: register-catalog
name: Registrar en Backstage Catalog
action: catalog:register
input:
repoContentsUrl: ${{ steps.create-repo.output.repoContentsUrl }}
catalogInfoPath: /catalog-info.yaml
# 5. Crear namespace de Kubernetes (solo tier 1-2)
- id: create-namespace
name: Crear namespace de Kubernetes
if: ${{ parameters.tier != 'tier-3' }}
action: kubernetes:apply-yaml
input:
manifest: |
apiVersion: v1
kind: Namespace
metadata:
name: ${{ parameters.name }}
labels:
tier: ${{ parameters.tier }}
team: ${{ parameters.owner }}
managed-by: backstage
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: default
namespace: ${{ parameters.name }}
spec:
hard:
requests.cpu: "${{ parameters.tier == 'tier-1' && '4' || '1' }}"
requests.memory: "${{ parameters.tier == 'tier-1' && '8Gi' || '2Gi' }}"
pods: "10"
# 6. Provisionar secrets
- id: provision-secrets
name: Provisionar secretos en Vault
action: vault:write
input:
path: secret/data/services/${{ parameters.name }}
data:
# Short-lived tokens de CI
ci_token: ${{ secrets.GITHUB_DEPLOY_TOKEN }}
# 7. Notificar al equipo
- id: notify
name: Notificar al equipo en Slack
action: slack:notify
input:
channel: '#platform-deploys'
text: |
:rocket: Nuevo servicio creado
*Nombre*: ${{ parameters.name }}
*Owner*: ${{ parameters.owner }}
*Tier*: ${{ parameters.tier }}
*Catalog*: https://backstage.internal/catalog/default/component/${{ parameters.name }}
output:
links:
- title: Open GitHub Repo
url: ${{ steps.create-repo.output.remoteUrl }}
- title: Open in Backstage
url: https://backstage.internal/catalog/default/component/${{ parameters.name }}
- title: Open Kubernetes Namespace
url: https://k8s.internal/namespaces/${{ parameters.name }}
|
El campo |
Custom fields avanzados
Backstage viene con un set de custom fields para casos comunes:
EntityNamePicker
name:
title: Nombre del componente
type: string
ui:field: EntityNamePicker
# El picker valida automáticamente que el formato sea válido
# (kebab-case, longitud, etc.)
RepoUrlPicker
repoUrl:
title: Repositorio
type: string
ui:field: RepoUrlPicker
ui:options:
allowedHosts:
- github.com
- gitlab.com
# Por defecto, valida que el repo existe y el usuario tiene acceso
OwnerPicker con filtro
owner:
title: Equipo
type: string
ui:field: OwnerPicker
ui:options:
# Solo permite seleccionar Groups (no Users)
catalogFilter:
kind: Group
# Filtra por tipo dentro de Group
spec.type: team
SelectFieldFromApi (campo custom que hace fetch de un endpoint)
# Pedir al usuario que elija un cluster de Kubernetes
cluster:
title: Cluster destino
type: string
ui:field: SelectFieldFromApi
ui:options:
path: /api/clusters
valueSelector: '.name'
labelSelector: '.name + " (" + .region + ")"'
Este campo llama a un endpoint del backend (que tú implementas) y muestra los resultados como dropdown. Útil para clusters, bases de datos, dominios, etc.
Conditional execution: if
El step create-namespace que vimos antes tenía if: ${{ parameters.tier != 'tier-3' }}. Esto es conditional execution: el step solo se ejecuta si la condición se cumple.
El if en un template es como el filtro de una receta: "si el comensal es vegetariano, no le pongas el jamón". El template se adapta al usuario sin necesidad de escribir dos templates.
Casos de uso:
# Ejecutar solo si se eligió Datadog
- id: setup-datadog
if: ${{ parameters.observability == 'datadog' }}
action: datadog:create-dashboard
input: ...
# Ejecutar solo si la región es Europa
- id: gdpr-compliance
if: ${{ parameters.region == 'eu-west-1' }}
action: compliance:gdpr-setup
input: ...
# Ejecutar solo para tier 1
- id: oncall-rotation
if: ${{ parameters.tier == 'tier-1' }}
action: pagerduty:create-rotation
input: ...
|
Las condiciones usan Nunjucks, el mismo motor de templates que usa Backstage. Puedes combinar con |
Manejo de secretos en templates: las 3 reglas
El Scaffolder maneja tres tipos de secretos, y cada uno tiene un riesgo distinto.
Regla 1: los inputs del usuario NO son secretos
El usuario teclea su valor en el form. El Scaffolder lo ve, el backend lo loguea (con redacción), y el template lo usa. No hay forma de mantenerlo secreto del usuario que lo creó.
Regla 2: los secrets en app-config.yaml son visibles para el integrator
# app-config.yaml
scaffolder:
defaultEnvironment:
secrets:
AWS_ACCESS_KEY: ${AWS_ACCESS_KEY} # de process.env
GITHUB_TOKEN: ${GITHUB_TOKEN}
Estos secrets están disponibles para cualquier template que los referencie vía ${{ secrets.* }}. Cualquier usuario con permisos de ejecutar templates puede verlos. Si esto te preocupa (y debería), usa la regla 3.
Regla 3: tokens short-lived emitidos on-demand
El patrón más seguro: el template no tiene un secret. Pide al backend que lo emita en el momento.
// Plugin: scaffolder-backend-module-github-token-exchange
// (mencionado en cap-18)
import { githubAuthApiRef } from '@backstage/plugin-github-backend';
export const githubTokenModule = createBackendModule({
pluginId: 'scaffolder',
moduleId: 'github-token-exchange',
register(env) {
env.registerInit({
deps: { github: githubAuthApiRef },
async init({ github }) {
scaffolder.addAction({
name: 'github:exchange-token',
handler: async (ctx) => {
const { token } = await github.getAccessToken();
return { output('token', token) };
},
});
},
});
},
});
# template.yaml — sin secrets hardcodeados
steps:
- id: deploy
action: deploy:to-cluster
input:
githubToken: ${{ steps['github-token'].output.token }}
# El token dura 1 hora. Después de eso, expira.
- id: github-token
action: github:exchange-token
input: {}
|
Regla de oro: si tu template tiene |
Dry-run: simular antes de ejecutar
Un template mal escrito puede tardar 5 minutos en fallar. El dry-run lo simula sin ejecutar las acciones:
# Dry-run desde la CLI
curl -X POST http://localhost:7007/api/scaffolder/v2/dry-run \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $TOKEN" \
-d '{
"templateRef": "golden-path-service",
"values": {
"name": "test-service",
"owner": "team-payments",
"language": "typescript"
}
}'
|
El dry-run evalúa los steps que no tienen side effects (lectura de inputs, validación, etc.) y omite los que sí los tienen (crear repo, escribir secretos). Es útil para validar el template antes de invertir 5 minutos. |
Auditar la adopción de templates
Un template que nadie ejecuta es un plato que nadie pide. Tiene los ingredientes pero no llega al comensal. Mide la adopción, no solo la disponibilidad.
Backstage emite eventos que puedes agregar a un dashboard:
// Capturar todos los task.completed del scaffolder
const eventStream = events.subscribe({
topic: 'scaffolder.task.completed',
handler: async (event) => {
await metrics.increment('scaffolder.task.completed', {
tags: {
template: event.task.spec.templateInfo.entity.metadata.name,
success: event.task.status === 'completed',
duration_seconds: event.task.completedAt - event.task.createdAt,
},
});
},
});
Métricas que deberías tener:
| Métrica | Qué cuenta |
|---|---|
|
Veces que se pulsó "Ejecutar" |
|
Veces que terminó sin error |
|
Veces que terminó con error |
|
Tiempo de ejecución |
|
Tasks por template específico |
|
Si un template tiene 0 ejecuciones en 30 días, hay dos posibilidades: nadie lo necesita (elimínalo) o es difícil de usar (mejora el form). Las métricas te dicen cuál. |
Marketplace interno de templates
Para que un template sea descubrible, no basta con publicarlo en el catalog. Necesitas un marketplace interno:
Filtrar por relevancia
# catalog-info.yaml de un template
apiVersion: backstage.io/v1alpha1
kind: Template
metadata:
name: golden-path-service
title: Crear un servicio (recomendado)
description: |
Golden path oficial. Usa este template para el 90% de los casos.
tags:
- recommended
- backend
- production
annotations:
backstage.io/techdocs-ref: dir:.
# Categoría para el marketplace
marketplace.backstage.io/category: 'service'
marketplace.backstage.io/tier: 'standard'
marketplace.backstage.io/estimated-time: '5 min'
# SLA del equipo que mantiene el template
marketplace.backstage.io/maintained-by: 'platform-team'
marketplace.backstage.io/sla: 'best-effort'
Página de marketplace
Puedes crear una página personalizada que liste templates por categoría:
// packages/app/src/components/MarketplacePage.tsx
import { useTemplateList } from '@backstage/plugin-scaffolder-react';
export const MarketplacePage = () => {
const { templates, loading, error } = useTemplateList();
if (loading) return <Progress />;
if (error) return <ErrorPanel error={error} />;
const grouped = groupBy(templates, (t) =>
t.metadata.annotations?.['marketplace.backstage.io/category'] ?? 'other',
);
return (
<>
{Object.entries(grouped).map(([category, items]) => (
<section key={category}>
<h2>{category}</h2>
<TemplateGrid templates={items} />
</section>
))}
</>
);
};
Testear templates con CI
Un template sin tests es código sin tests. Si cambias el template.yaml y rompes algo, te enterarás cuando un developer pierda 5 minutos.
# .github/workflows/test-template.yml
name: Test templates
on:
pull_request:
paths:
- 'templates/**'
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Backstage CLI
run: npm install -g @backstage/cli
- name: Lint template
run: |
for f in templates/**/template.yaml; do
backstage-cli repo lint --filename "$f"
done
- name: Validate YAML
run: |
for f in templates/**/template.yaml; do
yq '.' "$f" > /dev/null
done
- name: Dry-run test
run: |
# Verificar que el dry-run no falla
backstage-cli scaffolder dry-run \
--template ./templates/golden-path-service/template.yaml \
--values '{"name": "test-svc", "owner": "team-x", "language": "typescript"}'
|
Si un template no se puede testear con dry-run automático, probablemente no está listo para producción. Un template complejo debería poder ejecutarse en CI contra un entorno de staging. |
Resumen
-
Un golden path es un template que hace TODO: repo, CI, catalog, namespace, secrets, monitoring.
-
Custom fields (
EntityNamePicker,RepoUrlPicker,OwnerPicker,SelectFieldFromApi) mejoran la UX del form. -
ifpermite conditional execution: el step solo corre si la condición se cumple. -
Secrets: tres reglas, una sola conclusión: tokens short-lived, exchange on-demand.
-
Mide la adopción:
scaffolder.task.completedpor template, con duración p50/p95. -
Un marketplace interno necesita
catalog-info.yamlanotado y una página personalizada. -
Testea templates en CI con lint + dry-run.
Glosario del capítulo
- Golden path
-
Camino óptimo recomendado para una tarea común (crear servicio, deployar, etc.).
- Template
-
Manifiesto YAML que define un form + steps para provisionar algo.
- Skeleton
-
Directorio con código base que se copia al repo del usuario.
- Custom field
-
Componente UI especializado para un input (EntityNamePicker, OwnerPicker, etc.).
- Conditional execution
-
Step que solo se ejecuta si una condición Nunjucks es verdadera.
- Dry-run
-
Simulación de un template que evalúa inputs pero no ejecuta side effects.
- Short-lived token
-
Token con expiración corta (minutos a horas) emitido en tiempo de ejecución.
- Marketplace
-
Vista agregada de templates disponibles, filtrados por categoría/tags.
Próximo capítulo
Apéndice A — Git desde cero — si tu equipo nunca ha tocado Git, empieza aquí. Cubrimos desde git init hasta rebase, con analogías de cocina.
Parte V: Parte 5 — Operaciones, IaC y plataforma como producto
Docker Compose, kind, OpenTofu, Crossplane, monitorización, upgrades, multi-tenant y cómo vender la cocina dentro de la organización.