Backstage manifests in Kubernetes
Challenge
For a microservice system I was tasked with building a mechanism to transfer data between environments from diverse storage backends.
As not all services are rolled out in every environment, I had to find a way to understand an individual environment from within.
My idea: a standardized data sheet per service in Kubernetes, deployed together with the service.
Idea
Having worked with Backstage before I decided to use its catalog to provide the needed standardized information and get a developer platform as a positive side effect.
Quoting from the Backstage Software Catalog page
The Backstage Software Catalog is a centralized system that keeps track of ownership and metadata for all the software in your ecosystem (services, websites, libraries, data pipelines, etc). The catalog is built around the concept of metadata YAML files stored together with the code, which are then harvested and visualized in Backstage.
These metadata YAML are very close to Kubernetes manifests and could easily be extended with the needed information.
So simply kubectl apply -f catalog-info.yaml?
Yeah well, no, you need CRDs for Kubernetes to understand, check and accept the data; but the metadata YAML were never intended to be put into k8s and thus no CRD exist.
This is why I wrote1 the small tool alteos-gmbh/backstage-crd-gen
- Fetch Schemas: Downloads JSON schemas from the Backstage GitHub repository2 for the specified version
- Parse Schemas: Parses JSON schemas and resolves
$refreferences - Generate Go Types: Creates Go structs with kubebuilder markers for each entity type
- Generate CRDs: Runs
controller-gento produce OpenAPI v3 CRD YAML files
It’s always in the details
Did I say the YAML are very close to k8s manifests? Somehow, some time back in the mists of Backstage’s creation, it was
decided to add information to top-level metadata3, which is NOT supported by Kubernetes.
Also, a kind:API requires a spec.definition as string and Backstage’s processors support substitutions like $text
in the descriptor format4.
This breaks when applying the raw catalog-info.yaml into Kubernetes.
There is help for pre-processing the manifests by using kustomize, but beware, it has its own sharp edges and pitfalls.
See the excerpt below and fully in alteos-gmbh/backstage-crd-gen/example.
Happy patching! (this works for us. But as usual, YMMV!)
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- catalog-info.yaml
# The below is needed to strip Backstage.io's catalog down to kubernetes manifests
# Note: for the `remove` operation, the target MUST exist (https://datatracker.ietf.org/doc/html/rfc6902#section-4.2)
patches:
# metadata.tags
- patch: |-
- op: add
path: /metadata/tags
value: foo
- op: remove
path: /metadata/tags
target:
group: &group backstage.io
version: &version v1alpha1
# metadata.links
- patch: |-
- op: add
path: /metadata/links
value: foo
- op: remove
path: /metadata/links
target:
group: *group
version: *version
# metadata.description
- patch: |-
- op: add
path: /metadata/description
value: foo
- op: remove
path: /metadata/description
target:
group: *group
version: *version
# metadata.title
- patch: |-
- op: add
path: /metadata/title
value: foo
- op: remove
path: /metadata/title
target:
group: *group
version: *version
# --- kind:API ------------------------------------------------------
# APIs may have `spec.definition` references as $text, $json, $yaml, but CRD defines `spec.definition` as string
# Possible strategies:
# - none needed bc/ you define the API inline
# - have a specific `move` (if all use same reference)
- patch: |-
- op: move
from: /spec/definition/$text
path: /spec/definition
target:
group: *group
version: *version
kind: API
# - as `spec.definition` is required for `kind:API`, replace with a comment
- patch: |-
- op: replace
path: /spec/definition
value: placeholder to satisfy kubernetes CRD
target:
group: *group
version: *version
kind: API
Screenshots
$> cd backstage-crd-gen
$> make
go mod download
go mod tidy
go fmt ./...
golangci-lint run ./...
0 issues.
go test ./... -race
? github.com/alteos-gmbh/backstage-crd-gen [no test files]
? github.com/alteos-gmbh/backstage-crd-gen/cmd [no test files]
? github.com/alteos-gmbh/backstage-crd-gen/internal/versions [no test files]
ok github.com/alteos-gmbh/backstage-crd-gen/pkg/fetch (cached)
ok github.com/alteos-gmbh/backstage-crd-gen/pkg/generate (cached)
ok github.com/alteos-gmbh/backstage-crd-gen/pkg/schema (cached)
go build -ldflags "-X github.com/alteos-gmbh/backstage-crd-gen/internal/versions.Version=399c110 -X github.com/alteos-gmbh/backstage-crd-gen/internal/versions.BuildTime=2026-01-17T12:58:25Z" -o bin/backstage-crd-gen .
$> ./bin/backstage-crd-gen generate
Using controller-gen version: 0.20.0
Resolved latest version to: v1.46.3
Fetching schemas for Backstage v1.46.3...
Fetched 11 schemas
Generating Go types...
Generating CRDs...
CRDs written to: crds-gen/v1.46.3
$> kubectl apply -f crds-gen/v1.46.3
customresourcedefinition.apiextensions.k8s.io/apis.backstage.io created
customresourcedefinition.apiextensions.k8s.io/components.backstage.io created
customresourcedefinition.apiextensions.k8s.io/domains.backstage.io created
customresourcedefinition.apiextensions.k8s.io/groups.backstage.io created
customresourcedefinition.apiextensions.k8s.io/locations.backstage.io created
customresourcedefinition.apiextensions.k8s.io/resources.backstage.io created
customresourcedefinition.apiextensions.k8s.io/systems.backstage.io created
customresourcedefinition.apiextensions.k8s.io/users.backstage.io created
$> kubectl kustomize ./example | kubectl apply -f -
api.backstage.io/example-grpc-api created
component.backstage.io/example-website created
resource.backstage.io/example-db created
system.backstage.io/examples created
$> kubectl get apis.backstage.io
NAME TYPE OWNER LIFECYCLE
example-grpc-api grpc guests experimental
$> kubectl describe resources.backstage.io example-db
Name: example-db
Namespace: default
Labels: alteos.com/env-sync-relevant=true
Annotations: <none>
API Version: backstage.io/v1alpha1
Kind: Resource
Metadata:
Creation Timestamp: 2026-01-17T12:59:27Z
Generation: 1
Resource Version: 534
UID: 3db521fb-ad75-47ea-908f-1c36cc4d204b
Spec:
Alteos:
Consumption: producer
Pii:
Default:
Method: scrub
Items:
Field: bar
Table: foo
Field: adsfsdf
Table: sdafadsf
Secret:
Host: ALTEOS_HOST
Name: ALTEOS_NAME
Password: ALTEOS_PASSWORD
Port: ALTEOS_PORT
Username: ALTEOS_USER
Type: rds
Owner: guests
Type: database
Events: <none>
Links
Footnotes
-
very much assisted by Claude Code ↩
-
Backstage Entity schemas ↩
-
Backstage Common
metadata↩ -
Backstage Substitutions in descriptors ↩