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.
Prerequisites for Kubernetes ingestion A populated catalog, reachable clusters, a labeling convention, and the plugin enable live workload links. populated catalog reachable clusters labeling convention k8s plugin Live workloads on the entity
The label is the join key — every workload that carries the entity's kubernetes-id shows up on that entity, and nothing else does.

Exact Configuration

  1. 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']
    
  2. 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}
    
  3. Label workloads to the entity so the plugin knows which pods belong where.

    # deployment.yaml
    metadata:
      labels:
        backstage.io/kubernetes-id: payments-api
    
Workloads link to the entity by label The backend reads clusters read-only, matches the kubernetes-id label, and shows a service's pods on its entity page. clusters read-only match label kubernetes-id pods · deploys for this service entity page live health
Read-only access is deliberate — the portal shows workload health and never mutates the Kubernetes cluster, so a compromised portal cannot deploy.

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
Kubernetes ingestion validation checks The service account is read-only, workloads carry the label, and entities resolve their pods. Read-only SA list yes, delete no Label present on workloads Entity resolves its workloads
The delete-denied check is not optional — a portal service account that can mutate the Kubernetes cluster is a far larger blast radius than read access.

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
Kubernetes ingestion pitfalls by class Missing and wrong workloads are labeling issues; unreachable clusters and over-broad access are connectivity and scope issues. Labeling no workloads wrong pods → unique id label Connectivity & scope unreachable over-broad access → rotate token + scope
A shared label value is the subtle bug — two services claiming the same kubernetes-id show each other's pods, which erodes trust fast.

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.