Software Templates avanzados: el golden path completo

Tiempo de lectura: 26 min · Itinerarios: cocinero jefe, platform engineer, CTO. Lee :cap-12 y :cap-13 antes (cubren los fundamentos).

Objetivos del capítulo

Al terminar este capítulo vas a poder:

  • Construir un golden path completo: GitHub repo + catalog entity + Kubernetes namespace + TechDocs en un click.

  • Usar custom fields avanzados: EntityNamePicker, RepoUrlPicker, OwnerPicker, SelectFieldFromApi.

  • Implementar conditional execution con if en los steps.

  • Manejar secretos en templates sin filtrarlos a logs.

  • Auditar la adopción de templates con métricas reales.

  • Montar un marketplace interno de templates.

  • Testear templates con CI.

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
  1. Repo de código con CI configurada.

  2. Catalog entity registrada en Backstage.

  3. TechDocs scaffolding con MkDocs.

  4. Namespace de Kubernetes con quotas y network policies.

  5. Secrets de CI/CD provisionados (sonarqube token, datadog API key, etc).

  6. On-call rotation si el servicio es crítico.

  7. Alertas y dashboards básicos en Datadog/Grafana.

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 output.links es lo que el usuario ve al finalizar. Si lo omites, el usuario tiene que buscar el repo manualmente. Si lo defines, el Scaffolder le da enlaces directos. Es UX pura, pero ahorra tickets de soporte.

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 &&, ||, !, y llamadas a helpers. Si necesitas lógica compleja, escribe un helper custom.

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 secrets.X hardcodeado en app-config.yaml, considera migrarlo a un exchange pattern. Cada secret que reposa en disco es una oportunidad para un atacante.

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

scaffolder.task.started.total

Veces que se pulsó "Ejecutar"

scaffolder.task.completed.total

Veces que terminó sin error

scaffolder.task.failed.total

Veces que terminó con error

scaffolder.task.duration.p50/p95

Tiempo de ejecución

scaffolder.template.adoption.{name}

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.

  • if permite 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.completed por template, con duración p50/p95.

  • Un marketplace interno necesita catalog-info.yaml anotado 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.