Ingesting Kubernetes Resources into the Catalog
A service in the catalog that says nothing about where it actually runs leaves developers guessing which cluster, namespace, and workload back it. Ingesting Kubernetes resources ties each catalog Component to its live deployments, so the entity page shows real pods, not just intent. This how-to wires the Kubernetes plugin and an ingestion pattern that links workloads to entities by label. It extends Catalog Integration Patterns within Plugin Ecosystem & Custom Extensions.
Prerequisites
- Backstage 1.20+ with a populated catalog of Components.
- One or more Kubernetes clusters reachable from the backend, with a read-only service account.
- A labeling convention linking workloads to entities — typically
backstage.io/kubernetes-id. - The
@backstage/plugin-kubernetes(frontend) and its backend installed.
Exact Configuration
-
Grant the backend read-only cluster access via a scoped service account.
# k8s-rbac.yaml — read-only, cluster-scoped for discovery apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: { name: backstage-read } rules: - apiGroups: ['', 'apps'] resources: ['pods', 'services', 'deployments', 'replicasets'] verbs: ['get', 'list', 'watch'] -
Register clusters in Backstage config.
# app-config.yaml # Requires @backstage/plugin-kubernetes-backend >= 0.18 kubernetes: serviceLocatorMethod: { type: multiTenant } clusterLocatorMethods: - type: config clusters: - name: prod url: ${K8S_PROD_URL} authProvider: serviceAccount serviceAccountToken: ${K8S_PROD_TOKEN} -
Label workloads to the entity so the plugin knows which pods belong where.
# deployment.yaml metadata: labels: backstage.io/kubernetes-id: payments-api
Validation
# Requires kubectl, curl, jq
# 1. The service account can list but not mutate
kubectl auth can-i list pods --as=system:serviceaccount:default:backstage-read
kubectl auth can-i delete pods --as=system:serviceaccount:default:backstage-read
# Expected: yes, then no
# 2. Workloads carry the join label
kubectl get deploy payments-api -o jsonpath='{.metadata.labels.backstage\.io/kubernetes-id}'
# Expected: payments-api
# 3. The entity resolves its workloads through the portal
curl -s "${PORTAL_URL}/api/kubernetes/resources/workloads/query" \
-d '{"entityRef":"component:default/payments-api"}' | jq '.items | length'
# Expected: > 0
Edge Cases & Troubleshooting
| Symptom | Root Cause | Resolution |
|---|---|---|
| No workloads shown | Label missing or mismatched | Add backstage.io/kubernetes-id matching the entity name |
| Wrong pods on an entity | Two services share a label value | Use unique ids; namespace-scope the selector |
| Cluster unreachable | Token expired or network policy | Rotate the SA token; allow backend egress to the API |
| Everything shown, slow | No namespace scoping, cluster-wide scan | Scope discovery to relevant namespaces |
| Portal can mutate cluster | ClusterRole too broad | Restrict to get/list/watch only |
Frequently Asked Questions
Should the portal read from clusters directly or via a proxy?
For a handful of clusters, direct read with scoped service accounts is simplest. As the fleet grows, a proxy or aggregation layer centralizes credentials and rate-limiting so the portal holds one endpoint instead of dozens of tokens. Either way the access stays read-only — the decision is about credential management, not capability.
Does this replace GitOps or a deployment tool?
No. The portal reads and displays cluster state; it does not deploy. Your GitOps or CD pipeline remains the way workloads change. Ingestion just surfaces the result — which pods are running, and whether they are healthy — next to the service, so a developer sees reality without leaving the entity page.