Creating a Golden Path Template for Microservices
This how-to walks through authoring a complete, version-pinned Backstage software template that scaffolds a production-ready HTTP microservice: source skeleton, CI workflow, container build, and catalog registration. It is the concrete implementation behind the Golden Paths & Paved Roads sub-section of Developer Experience & Self-Service Platforms, and it reuses the action vocabulary documented in Scaffolder Template Design.
Prerequisites
Authoring a golden-path template needs four things: the scaffolder with its GitHub module, the CLI for dry-runs, a scoped ${GITHUB_TOKEN}, and a template repository referenced from catalog.locations by an explicit tag.
@backstage/plugin-scaffolder-backend@^1.22.0and@backstage/plugin-scaffolder-backend-module-github@^0.5.0installed and registered in the backend.@backstage/cli@^0.27.0available for validation and dry-runs.- A GitHub organization and a token in
${GITHUB_TOKEN}withrepoandworkflowscopes, injected viaapp-config.yaml. - A
${CONTAINER_REGISTRY}host value and at least oneGroupentity in the catalog to serve as an owner. - A Git repository to hold templates (for example
golden-paths), referenced fromcatalog.locationswith an explicit?ref=tag.
Exact Configuration
The template has two parts that map cleanly to the three actions: a Template definition drives the flow, and a skeleton/ directory holds the files that fetch:template renders, publish:github commits, and catalog:register indexes.
1. Lay out the template directory
golden-paths/templates/microservice/
microservice.yaml # the Template definition
skeleton/ # files rendered into the new repo
catalog-info.yaml
Dockerfile
.github/workflows/ci.yaml
cmd/server/main.go
2. Author the Template definition
# templates/microservice/microservice.yaml
# Requires scaffolder.backstage.io/v1beta3 (Backstage >= 1.22.0)
apiVersion: scaffolder.backstage.io/v1beta3
kind: Template
metadata:
name: golden-path-microservice
title: Golden Path — HTTP Microservice
description: Provisions a Go HTTP service with CI, container build, and catalog entry
tags: [golden-path, microservice]
spec:
owner: group:platform-engineering
type: service
parameters:
- title: Service identity
required: [name, owner]
properties:
name:
title: Service name
type: string
pattern: "^[a-z][a-z0-9-]{2,38}$"
ui:autofocus: true
owner:
title: Owning team
type: string
ui:field: OwnerPicker
ui:options:
catalogFilter: { kind: Group }
steps:
- id: render
name: Render skeleton
action: fetch:template
input:
url: ./skeleton
values:
name: ${{ parameters.name }}
owner: ${{ parameters.owner }}
registry: ${CONTAINER_REGISTRY}
- id: publish
name: Create repository
action: publish:github
input:
repoUrl: github.com?owner=${GITHUB_ORG}&repo=${{ parameters.name }}
defaultBranch: main
protectDefaultBranch: true
topics: [golden-path, microservice]
- id: register
name: Register component
action: catalog:register
input:
repoContentsUrl: ${{ steps.publish.output.repoContentsUrl }}
catalogInfoPath: /catalog-info.yaml
output:
links:
- title: Open repository
url: ${{ steps.publish.output.remoteUrl }}
- title: View in catalog
icon: catalog
entityRef: ${{ steps.register.output.entityRef }}
3. Author the skeleton’s catalog entry
Templated fields use the {{ }} Nunjucks syntax so they resolve at render time.
# skeleton/catalog-info.yaml
# Requires backstage.io/v1alpha1
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: ${{ values.name }}
annotations:
backstage.io/techdocs-ref: dir:.
spec:
type: service
lifecycle: experimental
owner: ${{ values.owner }}
4. Provide opinionated CI
# skeleton/.github/workflows/ci.yaml
# Requires GitHub Actions; image pushed to ${CONTAINER_REGISTRY}
name: ci
on: { push: { branches: [main] }, pull_request: {} }
jobs:
build:
runs-on: ubuntu-latest
permissions: { contents: read, id-token: write }
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with: { go-version: "1.22" }
- run: go test ./...
- name: Build image
run: docker build -t ${{ '${{' }} secrets.REGISTRY {{ '}}' }}/${{ values.name }}:${{ '${{' }} github.sha {{ '}}' }} .
5. Register the template location
# app-config.yaml
# Requires Backstage >= 1.22.0
catalog:
locations:
- type: url
target: https://github.com/${GITHUB_ORG}/golden-paths/blob/v2.4.0/templates/microservice/microservice.yaml
rules:
- allow: [Template]
Validate in four passes: schema-validate the template, lint the skeleton YAML, confirm a dry-run leaves no unresolved placeholder, and verify the generated component resolves a group owner.
# Requires @backstage/cli >= 0.27.0
set -euo pipefail
# 1. Schema-validate the template
npx @backstage/cli catalog validate --path ./templates/microservice/microservice.yaml
# expected: "Validated 1 entity ... 0 errors"
# 2. Lint skeleton YAML
yamllint ./templates/microservice/skeleton
# expected: no errors
# 3. Confirm no unresolved placeholders survive a dry-run render
grep -R '\${{' ./dry-run-output && echo "FAIL" || echo "clean render"
# expected: "clean render"
# 4. Verify the generated component resolves an owner
yq '.spec.owner' ./dry-run-output/catalog-info.yaml | grep -qE '^group:' && echo "owner ok"
# expected: "owner ok"
The template pitfalls split into rendering issues (literal placeholders, mangled Actions expressions) and provider issues (403s, missing owner, stale cache). The diagram sorts them so the fix targets the skeleton or the provider config.
| Symptom | Root Cause | Resolution |
|---|---|---|
${{ values.name }} appears literally in the created repo |
Skeleton file not processed by fetch:template |
Ensure the file lives under the url directory passed to fetch:template, not copied via fetch:plain |
publish:github fails with 403 |
${GITHUB_TOKEN} lacks repo/workflow scope or is not org-authorized |
Reissue a fine-grained token with both scopes and authorize it for the org |
| Catalog shows the component with no owner | OwnerPicker returned a name, not an entity ref |
Reference ${{ parameters.owner }} directly; the picker already yields group:default/... |
| GitHub Actions expression mangled by the template | Nunjucks consumes ${{ }} meant for Actions |
Escape Actions expressions with ${{ '${{' }} ... {{ '}}' }} as shown in the CI skeleton |
| New template revision not picked up | Backend cached the old ?ref= |
Bump the tag in catalog.locations and restart the backend |
Frequently Asked Questions
Where should the templated CI expressions and the scaffolder expressions be disambiguated?
Both Backstage’s Nunjucks and GitHub Actions use ${{ }}. Inside a skeleton file the scaffolder evaluates first, so any expression meant for Actions must be escaped (${{ '${{' }} ... {{ '}}' }}). Keep the escaping confined to CI workflow files; everything else in the skeleton is yours to template freely.
How do I add a second language without forking the template?
Add a language enum parameter and select the skeleton directory with fetch:template pointing at ./skeleton/{{ parameters.language }}. This keeps one maintained golden path with per-language sub-skeletons rather than duplicating the entire template.
Related
- Golden Paths & Paved Roads — parent sub-section
- Developer Experience & Self-Service Platforms — section overview
- Enforcing Paved-Road Policies with Software Checks — gating the service this template creates
- Automating Repository Scaffolding with Backstage Software Templates — the repository-creation action in depth