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.
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.
Part 2
Your project after this mission · 5 files change
- 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
The template: parameters
The form a developer sees.
OwnerPickerlists 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
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
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.8sRepository: https://github.com/shoplite/wishlistGitOps PR: https://github.com/shoplite/shoplite-gitops/pull/87 - 4
Merge the PR and watch it go live
The ApplicationSet from the GitOps course (Mission 2.3) discovers
apps/wishlistand 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 --squashargocd 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.
Break #2
Invalid name slips through
Remove the pattern from the name parameter and create a service called Wish_List.
Part 5
Interview questions from this mission
What would you put in a golden-path template for a new microservice?
How do you keep services created from templates up to date?