Writing a Software Template and TechDocs
Goal
Write the three layers of a Backstage software template yourself — parameters, steps, and output — and create the mkdocs configuration for TechDocs and a skeleton entity. At the end, you put the workload that template would generate onto a real cluster and check the result.
Why it matters
The value of the scaffolder is not reducing typing but making the verified path the default. If you harden the traps the platform team has stepped on once — needing a particular CRD version, or having to use a particular workaround — into the skeleton and documentation, other people do not step on them again. And the structure itself matters. Because parameters is a JSON Schema, wrong input is blocked at the form stage, and because steps is a combination of reusable actions, you do not have to rewrite the repository creation and catalog registration parts when you build a new template. One more easily missed point — what runs publish:github is not the user's browser but the Backstage backend. Because the token is only on the server, self-service through the portal is safer than handing out tokens to every developer. Backstage is not in this environment, so you write the template as a file and grading reads the files, and only the last step uses a real cluster.
Steps
- Write
/root/cba-template/template.yaml—apiVersion: scaffolder.backstage.io/v1beta3,kind: Template,metadata.name: node-service,metadata.titleany value,metadata.tagswith the first itemnodejs,spec.owner: group:team-platform,spec.type: service. - In the same file, add
spec.parameters— the first item of the array hastitle(any value),requiredas[name, owner],properties.nameastype: stringwithpattern: '^[a-z0-9-]+$'andtitle, andproperties.ownerastype: stringwithtitle. - In the same file, add
spec.steps— exactly 3, in order:id: fetch(actionfetch:template,input.url: ./skeleton, and ininput.values.namea substitution expression for the name parameter),id: publish(actionpublish:github), andid: register(actioncatalog:register, ininput.repoContentsUrla reference to the publish step's output, andinput.catalogInfoPath: /catalog-info.yaml). - In the same file, add
spec.output— in the first item oflinks,titleisRepository,urlis a reference to the publish step'sremoteUrloutput, andentityRefis a reference to the register step'sentityRefoutput. - Write
/root/cba-template/mkdocs.yml—site_nameany value, the first item ofnavisHome: index.md, and the first item ofpluginsistechdocs-core. Then in/root/cba-template/docs/index.md, write one title line that starts with#and a description paragraph. - Write
/root/cba-template/skeleton/catalog-info.yaml—kind: Component,metadata.nameas a template value substitution expression (the string must containvalues.name),metadata.annotationswithbackstage.io/techdocs-ref: dir:.andbackstage.io/kubernetes-id: node-service,spec.type: service,spec.lifecycle: experimental, andspec.owneras a substitution expression for the owner value. - Put the result the template would generate onto the cluster. Create the namespace
cba-scaffold, and inside it create the Deploymentnode-service(labelsbackstage.io/kubernetes-id: node-serviceandapp.kubernetes.io/part-of: cba-platform, imagenode:22-alpine, replicas2, and the samebackstage.io/kubernetes-idin the Pod template labels too), the Servicenode-service(port80, targetPort3000), and the ConfigMapnode-service-techdocs(the value of the keytechdocs-refmust be exactly the same as the annotation value of the skeleton from step 6).
Notes
- A substitution expression is a dollar sign followed by double curly braces. Reference form input as
parameters.<이름>and the result of an earlier step assteps.<id>.output.<필드>(the placeholders are the parameter name, the step id, and the output field name). - Wrap substitution expressions in double quotes. This removes any chance of the YAML parser mistaking the curly braces for a flow mapping.
- The output of
publish:githubincludesremoteUrlandrepoContentsUrl, and the output ofcatalog:registerincludesentityRef. - Common mistake 1: writing the Template's apiVersion as
backstage.io/v1alpha1. For the scaffolder it isscaffolder.backstage.io/v1beta3. - Common mistake 2: writing
parametersas a single object. It is an array to support multiple pages. - Common mistake 3: writing a fixed string in the skeleton's
metadata.name. It is a file the template will generate, so it must be a substitution expression.
Template manifest skeleton
Write /root/cba-template/template.yaml — apiVersion: scaffolder.backstage.io/v1beta3, kind: Template, metadata.name: node-service, metadata.title any value, metadata.tags with the first item nodejs, spec.owner: group:team-platform, spec.type: service.
A Template has a different apiVersion from catalog entities. Use the group dedicated to the scaffolder, and the owner has the same reference format as other entities.
parameters — the user form
In the same file, add spec.parameters — the first item of the array has title (any value), required as [name, owner], properties.name as type: string with pattern: '^[a-z0-9-]+$' and title, and properties.owner as type: string with title.
parameters is an array of pages, and each page is a JSON Schema. Check where the list of required items and the property definitions go.
steps — a combination of actions
In the same file, add spec.steps — exactly 3, in order: id: fetch (action fetch:template, input.url: ./skeleton, and in input.values.name a substitution expression for the name parameter), id: publish (action publish:github), and id: register (action catalog:register, in input.repoContentsUrl a reference to the publish step's output, and input.catalogInfoPath: /catalog-info.yaml).
Each step has id, name, action, and input. Recall which action name corresponds to fetching the skeleton, creating the repository, and registering in the catalog.
output — what to show at the end
In the same file, add spec.output — in the first item of links, title is Repository, url is a reference to the publish step's remoteUrl output, and entityRef is a reference to the register step's entityRef output.
You reference the result of an earlier step through the step id. Think about which step's output gives the repository address and which gives the registered entity reference.
mkdocs configuration and documentation
Write /root/cba-template/mkdocs.yml — site_name any value, the first item of nav is Home: index.md, and the first item of plugins is techdocs-core. Then in /root/cba-template/docs/index.md, write one title line that starts with # and a description paragraph.
TechDocs uses mkdocs. The configuration file needs a site name, a navigation list, and the plugin for TechDocs.
The skeleton's catalog-info.yaml
Write /root/cba-template/skeleton/catalog-info.yaml — kind: Component, metadata.name as a template value substitution expression (the string must contain values.name), metadata.annotations with backstage.io/techdocs-ref: dir:. and backstage.io/kubernetes-id: node-service, spec.type: service, spec.lifecycle: experimental, and spec.owner as a substitution expression for the owner value.
It is a file the template will generate, so the name position takes a substitution expression, not a value. Do not forget the annotation that points to the documentation location.
Apply the generated result to the cluster
Put the result the template would generate onto the cluster. Create the namespace cba-scaffold, and inside it create the Deployment node-service (labels backstage.io/kubernetes-id: node-service and app.kubernetes.io/part-of: cba-platform, image node:22-alpine, replicas 2, and the same backstage.io/kubernetes-id in the Pod template labels too), the Service node-service (port 80, targetPort 3000), and the ConfigMap node-service-techdocs (the value of the key techdocs-ref must be exactly the same as the annotation value of the skeleton from step 6).
You put up the workload the template would generate, yourself. The documentation reference value you wrote in the skeleton and the value you put in the cluster must be the same.