ITSkillsCenter
Développement Web

Scaffolder Backstage : créer un template de microservice Node.js

14 min de lecture

Article principal : Plateforme de développeur interne avec Backstage : architecture pour startup
Ce tutoriel s’inscrit dans la série Platform Engineering et Backstage. Pour la vue d’ensemble du sujet, lire d’abord le guide principal.

Ce que ce tutoriel construit concrètement

Le Scaffolder est probablement la fonctionnalité Backstage qui produit la valeur la plus tangible et la plus rapide. Le développeur ouvre le portail, clique sur Create, choisit Node.js Microservice, remplit un formulaire de quatre champs, et trois minutes plus tard un nouveau dépôt GitHub existe avec le squelette d’une API Fastify, un pipeline GitHub Actions qui compile et publie l’image Docker, un fichier catalog-info.yaml qui enregistre le service dans le portail, et un README qui explique comment continuer. Tout cela est généré par un seul fichier YAML : le template Scaffolder. Ce tutoriel construit un tel template de bout en bout.

L’objectif est précis. À la fin, vous saurez écrire un template au format scaffolder.backstage.io/v1beta3, organiser un dossier de fichiers modèles avec des placeholders Nunjucks, enchaîner les actions fetch:template, publish:github et catalog:register, et déclarer le template dans le catalogue pour qu’il apparaisse dans la page Create. Le tutoriel suppose une instance Backstage v1.50.0 qui sait s’authentifier sur GitHub — si ce n’est pas le cas, suivre d’abord les tutoriels d’installation et de catalogue.

Étape 1 — Comprendre l’anatomie d’un template

Un template Scaffolder est une entité du catalogue de kind Template qui décrit deux choses : un formulaire utilisateur (les parameters) et une suite d’étapes à exécuter (les steps). La spec officielle utilise désormais l’apiVersion scaffolder.backstage.io/v1beta3. Les anciennes versions (v1beta2 et antérieures) restent acceptées pour rétro-compatibilité mais on rédige les nouveaux templates en v1beta3 systématiquement.

La structure de haut niveau est identique pour tous les templates. La metadata donne nom, titre et description visibles dans le portail. Le spec.owner attribue la propriété (souvent platform-team). Le spec.type classifie le template (service, website, library) ce qui permet de filtrer par catégorie dans la page Create. La spec.parameters liste un ou plusieurs steps de formulaire, chacun avec ses champs JSON Schema. La spec.steps énumère les actions à exécuter dans l’ordre. La spec.output définit ce que voit l’utilisateur à la fin — typiquement un lien vers le repo créé et un lien vers la fiche catalogue de la nouvelle entité.

Étape 2 — Préparer le squelette du dépôt template

On démarre par créer un nouveau dépôt Git qui hébergera le template, par exemple example-org/scaffolder-templates. Ce dépôt va contenir le YAML du template à la racine et un sous-dossier template/ avec les fichiers modèles qui seront copiés dans chaque nouveau service. Cette séparation est importante : Backstage ne touche pas aux fichiers du dépôt template lui-même, il ne fait que les copier en appliquant les substitutions.

mkdir -p scaffolder-templates/nodejs-service/template
cd scaffolder-templates/nodejs-service
touch template.yaml
mkdir -p template/.github/workflows
touch template/package.json template/README.md template/catalog-info.yaml \
      template/.github/workflows/ci.yml template/Dockerfile

Cette arborescence est le pattern recommandé. Le dossier template/ contient le squelette du futur projet : un package.json minimal, un README.md templaté, un catalog-info.yaml qui enregistrera le service dans Backstage, un workflow GitHub Actions et un Dockerfile. Les fichiers seront passés à Nunjucks au moment de la génération, ce qui permet d’y insérer des valeurs venues du formulaire.

Étape 3 — Écrire les fichiers modèles avec placeholders Nunjucks

Les fichiers du dossier template/ peuvent contenir des placeholders au format Nunjucks {{ values.x }} qui seront remplacés par les valeurs du formulaire. Pour un microservice Node.js, le package.json minimal ressemble à ceci. Notez que les noms de paquets sont en kebab-case et que la valeur name est exactement celle saisie par l’utilisateur dans le formulaire.

{
  "name": "{{ values.name }}",
  "version": "0.1.0",
  "description": "{{ values.description }}",
  "main": "src/index.js",
  "scripts": {
    "start": "node src/index.js",
    "dev": "node --watch src/index.js",
    "test": "node --test"
  },
  "dependencies": {
    "fastify": "^5.8.0"
  }
}

La plage ^5.8.0 autorise les patches et minors compatibles dans la branche Fastify 5 — la pratique recommandée est d’épingler une plage caret stable dans les golden paths et de bumper consciemment la base lors des révisions de template. Cette discipline garde les nouveaux services alignés sur la même base testée par l’équipe plateforme. Pour le catalog-info.yaml du futur service, on injecte le nom et le propriétaire saisis par l’utilisateur et on déclare le repo en annotation pour les plugins GitHub.

apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
  name: {{ values.name }}
  description: {{ values.description }}
  annotations:
    github.com/project-slug: {{ values.destination.owner }}/{{ values.destination.repo }}
    backstage.io/techdocs-ref: dir:.
spec:
  type: service
  lifecycle: experimental
  owner: {{ values.owner }}
  system: {{ values.system }}

Le {{ values.destination.owner }}/{{ values.destination.repo }} est rempli à partir du champ RepoUrlPicker du formulaire qui décompose une URL de dépôt en host, owner et repo. Cette astuce rend le catalog-info.yaml autoporteur sans qu’on ait à demander à l’utilisateur le slug GitHub à part.

Étape 4 — Définir les parameters du formulaire

Les parameters utilisent JSON Schema enrichi de directives ui: qui pilotent l’affichage. Un bon formulaire reste court — quatre à cinq champs sur une page, idéalement deux pages maximum si on veut séparer logiquement les sections. On commence par le YAML du template avec ses parameters.

apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
  name: nodejs-service
  title: Microservice Node.js (Fastify)
  description: Genere un microservice Node.js avec CI, Dockerfile et catalog-info
  tags:
    - nodejs
    - service
spec:
  owner: platform-team
  type: service
  parameters:
    - title: Identite du service
      required:
        - name
        - description
        - owner
      properties:
        name:
          title: Nom du service
          type: string
          description: kebab-case, lowercase, sans espaces
          pattern: '^[a-z0-9-]+$'
          ui:autofocus: true
        description:
          title: Description
          type: string
        owner:
          title: Equipe proprietaire
          type: string
          ui:field: OwnerPicker
          ui:options:
            catalogFilter:
              kind: Group
        system:
          title: Systeme metier
          type: string
          ui:field: EntityPicker
          ui:options:
            catalogFilter:
              kind: System
    - title: Destination GitHub
      required:
        - destination
      properties:
        destination:
          title: URL du depot a creer
          type: string
          ui:field: RepoUrlPicker
          ui:options:
            allowedHosts:
              - github.com
            requestUserCredentials:
              secretsKey: USER_OAUTH_TOKEN
              additionalScopes:
                github:
                  - repo
                  - workflow

Plusieurs détails comptent. Le pattern sur le nom rejette les saisies invalides côté formulaire et évite des erreurs en aval. Le champ OwnerPicker propose dynamiquement les Groups du catalogue : l’utilisateur ne saisit pas un nom à la main, il choisit dans une liste. L’EntityPicker avec catalogFilter: {kind: System} propose les Systems existants. Le RepoUrlPicker avec requestUserCredentials demande à l’utilisateur d’autoriser l’app pour pousser sur GitHub avec les scopes repo et workflow nécessaires à la création du dépôt et du workflow CI.

Étape 5 — Enchaîner les steps de génération

Les steps s’exécutent en séquence dans l’environnement isolé du Scaffolder. Pour un microservice Node.js standard, trois étapes suffisent : récupérer et templater les fichiers, créer le dépôt GitHub avec ces fichiers, enregistrer la nouvelle entité dans le catalogue. Chaque step a un id, un name affiché à l’utilisateur, une action de la liste des actions disponibles, et un input qui passe des paramètres à l’action.

  steps:
    - id: fetch
      name: Recuperer le squelette
      action: fetch:template
      input:
        url: ./template
        values:
          name: ${{ parameters.name }}
          description: ${{ parameters.description }}
          owner: ${{ parameters.owner }}
          system: ${{ parameters.system }}
          destination: ${{ parameters.destination | parseRepoUrl }}

    - id: publish
      name: Creer le depot GitHub
      action: publish:github
      input:
        description: ${{ parameters.description }}
        repoUrl: ${{ parameters.destination }}
        defaultBranch: main
        repoVisibility: private
        token: ${{ secrets.USER_OAUTH_TOKEN }}

    - id: register
      name: Enregistrer dans le catalogue
      action: catalog:register
      input:
        repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
        catalogInfoPath: /catalog-info.yaml

  output:
    links:
      - title: Depot du service
        url: ${{ steps.publish.output.remoteUrl }}
      - title: Fiche catalogue
        icon: catalog
        entityRef: ${{ steps.register.output.entityRef }}

L’action fetch:template récupère le contenu du dossier template/ et applique Nunjucks avec les values fournies. Le filtre parseRepoUrl est un filtre custom courant — fourni par certaines actions communautaires ou ajouté via une action custom — qui décompose l’URL GitHub en un objet {host, owner, repo} ; sans ce filtre, on accède directement à l’URL brute par ${{ parameters.destination }} et on la passe en l’état aux actions publish:github qui savent la parser nativement. L’action publish:github crée le dépôt avec les fichiers générés en utilisant le token OAuth de l’utilisateur. L’action catalog:register ajoute manuellement la nouvelle entité dans le catalogue sans attendre le prochain cycle de discovery, ce qui rend le service immédiatement visible dans le portail. La section output définit les deux liens cliquables qui apparaissent à l’utilisateur à la fin de la génération.

Étape 6 — Déclarer le template dans le catalogue

Pour que le template apparaisse dans la page Create du portail, il faut l’enregistrer comme entité du catalogue. Le moyen le plus propre est d’ajouter une Location dans le app-config.yaml qui pointe vers le fichier template.yaml du dépôt scaffolder-templates. À chaque rafraîchissement, Backstage va lire ce fichier et exposer le template.

catalog:
  rules:
    - allow: [Component, System, API, Resource, Location, Template, Group, User, Domain]
  locations:
    - type: url
      target: https://github.com/example-org/scaffolder-templates/blob/main/nodejs-service/template.yaml
      rules:
        - allow: [Template]

La règle allow ouvre explicitement le kind Template qui n’est pas dans la liste par défaut. Sans cette règle, le chargement échoue avec un message kind not allowed. Au redémarrage de yarn dev, le template apparaît dans Backstage : ouvrir Create, choisir Microservice Node.js (Fastify), remplir le formulaire, suivre le déroulement des steps en temps réel, et cliquer sur le lien du dépôt généré pour vérifier le résultat.

Étape 7 — Tester le template avec une génération réelle

Le test grandeur nature reste l’étape la plus instructive. On clique sur Create, on choisit le template, on remplit : name: order-api, description: Service de gestion des commandes, owner: platform-team, system: payment-system, destination: github.com/example-org/order-api. On valide. Backstage affiche en direct l’exécution des trois steps. Si tout va bien, on voit successivement « Recuperer le squelette ✓ » puis « Creer le depot GitHub ✓ » puis « Enregistrer dans le catalogue ✓ ».

On clique alors sur le lien du dépôt. Sur GitHub, le repo example-org/order-api existe avec les fichiers attendus : package.json au nom correct, README.md personnalisé, catalog-info.yaml rempli, workflow GitHub Actions opérationnel. On retourne dans Backstage et on cherche order-api dans le catalogue : la fiche existe avec le bon owner et le bon système, et le lien GitHub fonctionne. La boucle est bouclée.

Si l’étape publish:github échoue avec un message d’autorisation, c’est généralement que le token OAuth manque les scopes repo ou workflow. Il faut révoquer l’autorisation existante côté GitHub (Settings → Applications → Authorized OAuth Apps) et relancer la génération pour redemander les bons scopes. Si l’étape catalog:register échoue avec 404 Not Found, c’est que le catalog-info.yaml n’a pas été correctement écrit dans le dépôt — vérifier le rendu du fichier directement dans le repo.

Étape 8 — Ajouter une action custom pour les besoins spécifiques

Les actions de base couvrent les cas standards. Pour des besoins plus avancés — provisionner une base de données, créer un secret dans Vault, ouvrir un canal Slack, déclencher un job Jenkins — on écrit une action custom. La documentation officielle détaille la procédure ; en résumé, on crée une action TypeScript qui implémente l’interface TemplateAction et on l’enregistre côté backend.

// packages/backend/src/scaffolder/createSlackChannel.ts
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';

export const createSlackChannelAction = createTemplateAction<{
  channelName: string;
  topic: string;
}>({
  id: 'company:slack:create-channel',
  schema: {
    input: {
      type: 'object',
      required: ['channelName'],
      properties: {
        channelName: { type: 'string' },
        topic: { type: 'string' },
      },
    },
  },
  async handler(ctx) {
    const { channelName, topic } = ctx.input;
    // appel API Slack ici, ex: web.conversations.create({ name: channelName })
    ctx.logger.info(`Canal #${channelName} cree`);
    ctx.output('channelId', 'C12345678');
  },
});

L’action est ensuite enregistrée dans le backend en l’ajoutant aux actions du Scaffolder via le système de modules. Une fois enregistrée, on peut l’utiliser dans n’importe quel template via action: company:slack:create-channel. La convention de nommage est {organisation}:{domaine}:{verbe}, ce qui évite les collisions avec les actions natives de Backstage. Il faut prévoir une gestion d’erreur propre dans le handler — un step qui jette une exception interrompt le déroulement complet du template.

Erreurs fréquentes

Erreur Cause Solution
Template not allowed in catalog Règle catalog.rules absente Ajouter allow: [Template] dans la location
publish:github 401 Unauthorized Scopes OAuth insuffisants Révoquer l’autorisation, redemander avec repo + workflow
catalog:register 404 Not Found catalog-info.yaml absent du repo Vérifier que le fichier est bien dans template/
Variable {{ values.x }} non remplacée Mismatch entre parameters et values du fetch S’assurer de passer toutes les valeurs au step fetch:template
repoUrl invalid format Mauvais format d’URL dans destination Le RepoUrlPicker génère github.com?owner=x&repo=y, ne pas le réécrire
OwnerPicker liste vide Aucun Group dans le catalogue Charger d’abord les Groups (cf. tutoriel catalogue)
Template caché dans Create Tag manquant ou type non recherché Vérifier les filtres actifs sur la page Create

Tutoriels associés

Pour aller plus loin

Retour au guide principal : Plateforme de développeur interne avec Backstage : architecture pour startup.

FAQ

Quelle différence entre v1beta2 et v1beta3 ?
v1beta3 unifie la syntaxe des expressions Nunjucks (${{ ... }}) et améliore le typage. v1beta2 reste fonctionnel mais est en mode maintenance — les nouveaux templates utilisent v1beta3 par défaut. Pour migrer un template existant, l’opération est mécanique et la doc fournit un guide pas-à-pas.

Peut-on créer un dépôt GitLab ou Bitbucket au lieu de GitHub ?
Oui, les actions publish:gitlab, publish:bitbucket, publish:azure existent toutes. Le RepoUrlPicker accepte les hosts correspondants quand on les ajoute dans allowedHosts. Une organisation multi-VCS configure plusieurs intégrations en parallèle dans integrations du app-config.yaml.

Comment versionner les templates eux-mêmes ?
Le pattern recommandé est un dépôt dédié (par exemple scaffolder-templates) avec une branche main stable et des branches de feature pour les évolutions. Chaque modification passe par une pull request revue par l’équipe plateforme. Les bumps de versions de dépendances sont annoncés à l’avance pour permettre aux équipes produit de planifier l’adoption.

Combien de templates faut-il maintenir ?
Trop peu, on n’absorbe pas la diversité des besoins ; trop, on disperse la maintenance. La règle empirique est entre quatre et huit templates pour une startup de cinquante développeurs : un par pattern récurrent (microservice HTTP, worker async, frontend, fonction serverless, job batch, librairie partagée). Au-delà, c’est qu’on a besoin de paramétrer plus finement les templates existants plutôt que d’en créer un nouveau.

Le scaffolder peut-il modifier un service existant ?
Oui, via les actions fs:rename, fs:delete et la possibilité de pousser une pull request avec publish:github:pull-request. Cette dernière action est utile pour les templates de migration — par exemple « ajouter le monitoring Prometheus à un service existant » qui ouvre une PR avec les fichiers de configuration nécessaires.

Peut-on rendre certains champs conditionnels ?
JSON Schema supporte oneOf, anyOf et dependencies qui permettent des formulaires conditionnels. Par exemple, afficher un champ cluster Kubernetes cible uniquement si type de déploiement vaut kubernetes. La syntaxe est plus verbeuse mais fonctionne nativement dans le formulaire généré.

Comment déboguer un step qui échoue mystérieusement ?
Le Scaffolder enregistre tous les logs de chaque exécution dans la base de données. L’onglet Activity de la page Create affiche l’historique avec les logs détaillés. En local, yarn dev écrit aussi les logs sur stdout. Pour les actions custom, utiliser ctx.logger.info, ctx.logger.warn et ctx.logger.error pour tracer ce qui se passe.

Où héberger votre projet web ?

Hostinger propose des plans dimensionnés pour les freelances et PME. Lien d’affiliation — pas de surcoût pour vous.

Comparer les plans →

Lien d affiliation. Si vous achetez via ce lien, le blog reçoit une petite commission sans surcoût pour vous.

Sponsoriser ce contenu

Cet emplacement est à vous

Position premium en fin d'article — c'est l'instant où les lecteurs sont le plus engagés. Réservez cet espace pour votre marque, votre formation ou votre offre.

Recevoir nos tarifs
Publicité