Command Palette

Search for a command to run...

Mission 1.2 · Stage 1 — The Developer Portal: Backstage and Golden Paths

Golden Paths: Software Templates

Goal: A developer fills in a form in Backstage and gets, in minutes, a new service repo with code, Dockerfile, CI, GitOps config, catalog entry, and dashboards, all following platform standards.

70 min Free 4 steps 2 break-it drills

By the end of this mission

  • Design what a golden path should include
  • Write a Backstage Software Template with parameters, steps, and outputs
  • Scaffold a repo from a skeleton and open a PR to the GitOps repo
  • Keep templates maintainable as standards evolve

Part 1

Understand it first

What goes into a golden path

Everything a new service needs to be production-ready on day one, pre-wired: application skeleton with health endpoints and structured JSON logging (Observability course), a multi-stage Dockerfile running as non-root (Docker course), a CI workflow that tests, scans, builds, signs, and bumps the dev tag (CI/CD and DevSecOps courses), Kustomize base and overlays with requests/limits, PDB, probes, an HTTPRoute (Mission 0.2), and an ApplicationSet-compatible folder in the GitOps repo (GitOps course), a catalog-info.yaml and docs, and default dashboards and SLO alerts. Every policy from Mission 0.4 passes automatically.

How templates work

A Template entity has PARAMETERS (a JSON-Schema form: name, owner, description, Java version) and STEPS executed by the scaffolder backend using ACTIONS: fetch:template (render a skeleton directory with Nunjucks templating), publish:github (create the repo), github:actions:dispatch, publish:github:pull-request (open a PR somewhere else, e.g. the GitOps repo), and catalog:register. OUTPUTS show links to the result. Custom actions (TypeScript) can call anything: create a PagerDuty service, a Grafana folder, a Crossplane claim.

Templates age: plan for it

A template is a one-time copy: when the standard changes (new base image, new CI step), the 50 services created last year don't update themselves. Mitigations: keep templates thin and put shared logic in REUSABLE pieces that update centrally (reusable CI workflows, a shared Helm chart or Kustomize component, base images), scorecards that show drift (Mission 1.3), and automated PRs (Renovate) to bump versions everywhere.

What the template does in 3 minutesdiagram
Rendering diagram…

Part 2

Your project after this mission · 5 files change

shoplite-platform/
  • shoplite-templates/
    • spring-boot-service/
      • gitops/
        • new
      • skeleton/
        • .github/
          • workflows/
            • release.ymlnew
        • new
        • catalog-info.yamlnew
      • template.yamlnew

Part 3

Build it, step by step

  1. 1

    The template: parameters

    The form a developer sees. OwnerPicker lists Groups from the catalog, so every new service has a real owner. Validation (pattern, length) prevents names that would break DNS or Kubernetes.

    shoplite-templates/spring-boot-service/template.yamlwhole fileyaml
    apiVersion: scaffolder.backstage.io/v1beta3
    kind: Template
    metadata:
      name: spring-boot-service
      title: New Spring Boot service (golden path)
      description: Production-ready Java service with CI, GitOps, dashboards and docs.
      tags: [java, recommended]
    spec:
      owner: group:team-platform
      type: service
      parameters:
        - title: About the service
          required: [name, owner, description]
          properties:
            name:
              type: string
              title: Service name
              pattern: '^[a-z][a-z0-9-]{2,30}$'
              ui:help: lowercase, dashes, 3–31 chars (used for repo, DNS and k8s names)
            description: { type: string, title: What does it do? }
            owner:
              type: string
              title: Owning team
              ui:field: OwnerPicker
              ui:options: { catalogFilter: { kind: Group } }
            system:
              type: string
              title: System
              ui:field: EntityPicker
              ui:options: { catalogFilter: { kind: System } }
            exposePublicly: { type: boolean, title: Expose at <name>.shoplite.dev?, default: false }
      steps:
        - id: fetch
          name: Render service skeleton
          action: fetch:template
          input:
            url: ./skeleton
            values:
              name: ${{ parameters.name }}
              owner: ${{ parameters.owner }}
              system: ${{ parameters.system }}
              description: ${{ parameters.description }}
        - id: publish
          name: Create GitHub repo
          action: publish:github
          input:
            repoUrl: github.com?owner=shoplite&repo=${{ parameters.name }}
            defaultBranch: main
            repoVisibility: internal
            protectDefaultBranch: true
            requireCodeOwnerReviews: true
        - id: gitops
          name: Render GitOps config
          action: fetch:template
          input:
            url: ./gitops
            targetPath: ./gitops
            values:
              name: ${{ parameters.name }}
              expose: ${{ parameters.exposePublicly }}
        - id: gitops-pr
          name: Open PR in shoplite-gitops
          action: publish:github:pull-request
          input:
            repoUrl: github.com?owner=shoplite&repo=shoplite-gitops
            branchName: add-${{ parameters.name }}
            title: "Add ${{ parameters.name }} (from golden path)"
            description: Created by Backstage for ${{ parameters.owner }}.
            sourcePath: ./gitops
            targetPath: apps/${{ parameters.name }}
        - id: register
          name: Register in catalog
          action: catalog:register
          input:
            repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
            catalogInfoPath: /catalog-info.yaml
      output:
        links:
          - { title: Repository, url: ${{ steps.publish.output.remoteUrl }} }
          - { title: GitOps PR, url: ${{ steps['gitops-pr'].output.remoteUrl }} }
          - { title: Open in catalog, icon: catalog, entityRef: ${{ steps.register.output.entityRef }} }
  2. 2

    The skeleton: templated files

    Files under skeleton/ are rendered with ${{ values.x }}. The CI workflow CALLS a reusable workflow owned by the platform team, so improving CI later updates every service at once.

    shoplite-templates/spring-boot-service/skeleton/.github/workflows/release.ymlwhole fileyaml
    name: release
    on:
      push: { branches: [main] }
      pull_request: {}
    jobs:
      golden-path:
        uses: shoplite/platform-workflows/.github/workflows/java-service.yml@v4
        with:
          service: ${{ values.name }}
          java-version: "21"
        secrets: inherit
  3. 3

    Register the template and try it

    Add the template repo as a catalog location. It appears under Create. Run it with a test name and watch each step's log.

    shoplite-portal/app-config.yamladd to fileyaml
    catalog:
      locations:
        - type: url
          target: https://github.com/shoplite/shoplite-templates/blob/main/spring-boot-service/template.yaml
          rules: [{ allow: [Template] }]
    terminal
    $ # Backstage UI → Create → 'New Spring Boot service (golden path)' → name: wishlist, owner: team-storefront
    ── expected output ──
    ✔ Render service skeleton 0.4s
    ✔ Create GitHub repo 2.1s
    ✔ Render GitOps config 0.1s
    ✔ Open PR in shoplite-gitops 1.6s
    ✔ Register in catalog 0.8s
    Repository: https://github.com/shoplite/wishlist
    GitOps PR: https://github.com/shoplite/shoplite-gitops/pull/87
  4. 4

    Merge the PR and watch it go live

    The ApplicationSet from the GitOps course (Mission 2.3) discovers apps/wishlist and creates the Applications; Kyverno policies pass because the template complies; the HTTPRoute gets DNS and TLS from Mission 0.2.

    terminal
    $ gh pr merge 87 --repo shoplite/shoplite-gitops --squash
    argocd app wait wishlist-in-cluster --health --timeout 600 && curl -s https://wishlist.dev.shoplite.dev/actuator/health
    ── expected output ──
    {"status":"UP"}

Checkpoint — you should now have

  • ✓Running the template creates a repo, a GitOps PR, and a catalog entry.
  • ✓After merge, the new service is deployed, reachable over HTTPS, and passes every policy.
  • ✓CI in the new repo calls the platform's reusable workflow.

Part 4

Break it on purpose

Make each change, run the command, and read the error before revealing the diagnosis. Recognising these messages on sight is what makes you fast on a real team. Undo the change afterwards.

Break #1

Template drift

Six months later, the platform switches base images and adds an SBOM step. You update the template.

terminal
$ gh search code 'FROM eclipse-temurin:17' --owner shoplite --filename Dockerfile | wc -l
── what you'll see ──
31

Break #2

Invalid name slips through

Remove the pattern from the name parameter and create a service called Wish_List.

terminal
$ argocd app get Wish_List-in-cluster
── what you'll see ──
InvalidSpecError: metadata.name: Invalid value: "Wish_List-in-cluster": a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.'

Part 5

Interview questions from this mission

01

What would you put in a golden-path template for a new microservice?

02

How do you keep services created from templates up to date?

0/3 · 0%