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
@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
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]
Validation
# 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"
Edge Cases & Troubleshooting
| 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