Kubernetes Manifest Visualizer

Paste a bundle of manifests and see the object graph: what points at what, and above all what points at nothing. A broken reference in Kubernetes is accepted by the API server, listed as fine by kubectl, and only shows up later as a pod that will not start or a route that returns 503.

Paste below, or drop a file anywhere on this panel

Or drop a file anywhere on this panel. Nothing is uploaded: the analysis runs in this tab.

The answer appears here

Paste on the left and press Draw the graph. Nothing leaves this tab.

Examples

Real input you can load into the tool above. Each one shows a different thing going wrong, because that is what the tool is for.

A Service selecting nothing

A selector that matches no pod labels, which is a working Service with no endpoints

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  template:
    metadata:
      labels:
        app: api
---
apiVersion: v1
kind: Service
metadata:
  name: api
spec:
  selector:
    app: nope

A matching pair

The same two objects wired correctly, for comparison

apiVersion: apps/v1
kind: Deployment
metadata:
  name: api
spec:
  template:
    metadata:
      labels:
        app: api
---
apiVersion: v1
kind: Service
metadata:
  name: api
spec:
  selector:
    app: api

Common mistakes

These are the ones that fail silently. The config is accepted, nothing raises an error, and the consequence arrives later.

  1. Assuming a Service and a Deployment named the same are connected

    They are joined by labels, not by name. A Service selecting app: api matches nothing if the pod template says app: web, and both objects are valid.

    Instead:Compare the Service selector against the POD TEMPLATE labels, not the Deployment's own labels.

  2. Reading targetPort as the Service port

    port is what clients connect to and targetPort is the container port. Swapping them gives a Service that accepts connections and forwards them nowhere.

    Instead:targetPort must match containerPort, or a named port on the container.

What it catches

Severity here ranks by how long it takes you to find out, not by how broken the thing is. A pod stuck in ContainerCreating is visible in kubectl get within a minute. A Service with no endpoints is not visible anywhere.

The reference that resolves and still returns 503

An Ingress naming a Service that is not in the bundle is bad. An Ingress naming a Service that exists and selects no pods is worse, because now every object reports healthy: the Ingress is admitted, the Service has a ClusterIP and appears in DNS, and the only symptom is a 503 from the controller. That case is reported above the missing one, on purpose.

A missing ConfigMap fails four different ways

Mounted as a volume, an absent ConfigMap leaves the pod in ContainerCreating forever with the reason only in kubectl describe. Read through env, it fails the container at start with CreateContainerConfigError, after the image has already been pulled. Referenced by a key that the ConfigMap does not define, it fails the same way while the object itself looks perfectly correct. Marked optional: true, it does not fail at all. These are four different findings here because they are four different investigations.

volumeClaimTemplates generate claims, they do not reference them

A StatefulSet volumeClaimTemplate creates one PVC per replica, named after the template, the StatefulSet and the ordinal. Reading it as a reference reports every StatefulSet in the world as broken, so it is drawn as generating instead, with the names it will create. A volume using claimName is the opposite case: nothing creates that claim for you.

The Ingress port is the Service port

backend.service.port.name matches the name of a port on the Service, not the containerPort name on the pod, and those are usually different words. backend.service.port.number is the Service's published port, not the targetPort. Both mistakes produce a manifest that reviews cleanly and a path that has no upstream.

Selectors that quietly match nothing

A Service selector matching nothing is the classic silent failure. A PodDisruptionBudget matching nothing reports ALLOWED DISRUPTIONS 0 and lets every drain through. A NetworkPolicy whose podSelector matches nothing is inert, so the traffic it was written to block is still allowed and no packet is dropped to tell you. All three are checked against the pod template labels, which is the only thing any of them ever match.

Not in this input, never does not exist

Anything absent from the paste might exist in the cluster, so a dangling edge is reported as not in this input and nothing here claims otherwise. The same goes for what is not followed at all: ownerReferences, IngressClass, StorageClass, PersistentVolume binding, CSI volumes, webhooks and anything a controller creates at runtime.

Why a broken reference in Kubernetes is silent

Kubernetes has no foreign keys. Every reference between objects is a string in a field, resolved much later by a different component, and nothing in the apply path checks that the thing on the other end exists. So a typo is accepted, stored, and reported as healthy, and the failure surfaces minutes later somewhere that does not mention the field you got wrong.

Nothing validates a name at admission time

The API server validates shape: is this a legal object, are the field types right, does the name match the DNS rules. It does not validate meaning. A ConfigMap name is just a string until the kubelet tries to mount it, an Ingress backend name is just a string until the ingress controller builds its upstream, and a scaleTargetRef is just a string until the HPA controller asks for a scale subresource. Each of those happens in a different process at a different time, which is why each failure looks different.

kubectl apply -f broken.yaml

deployment.apps/api created        <- accepted
service/api created                <- accepted
ingress.networking.k8s.io/web created

kubectl get all
everything Running, everything Ready

the ConfigMap named in the volume does not exist,
and nothing above mentions it

Four absences, four moments, four symptoms

The same missing ConfigMap produces a different investigation depending on how it was referenced. Knowing which one you are in tells you where to look, and the pod phase is the giveaway: Pending means the scheduler never placed it, ContainerCreating means the kubelet cannot finish the mount, and a container that starts and stops means the config was resolved and rejected.

reference                     phase                  where the reason is
volumes[].configMap           ContainerCreating      pod events
env[].valueFrom.*KeyRef       CreateContainerConfig  container status
volumes[].persistentVolume..   Pending                scheduler events
imagePullSecrets              runs, or ImagePull..    kubelet log only
serviceAccountName            no pod is created      replicaset events

only the first four are pods you can describe; a missing
ServiceAccount means there is no pod to look at

A Service selector is not a LabelSelector

A Deployment's own selector, a PodDisruptionBudget's selector and a NetworkPolicy's podSelector are all metav1.LabelSelector: they take matchLabels and matchExpressions. A Service selector is not. It is a flat map of label keys to values, equality only, and adding a matchLabels wrapper to one makes the API server fail to unmarshal it with an error about a Go type rather than about the field.

Service                     Deployment, PDB, NetworkPolicy
selector:                   selector:
  app: api                    matchLabels:
  tier: web                     app: api
                              matchExpressions:
                                - key: tier
                                  operator: In
                                  values: [web]

every key is ANDed in both cases, and both match POD
labels from spec.template.metadata.labels, never the
workload's own metadata.labels

policy/v1 reversed what an empty PDB selector means

In policy/v1beta1 a null selector selected every pod in the namespace and an empty one selected none. policy/v1 swapped them. A PodDisruptionBudget carried over from the old API therefore keeps its YAML, passes every validator, and changes from protecting the namespace to protecting nothing. It still reports as a valid budget, so the discovery usually happens during a node upgrade.

policy/v1beta1        policy/v1
selector: null   ->   all pods       none
selector: {}     ->   none           all pods in namespace

kubectl get pdb api
NAME   MIN AVAILABLE   ALLOWED DISRUPTIONS   AGE
api    1               0                     9d

ALLOWED DISRUPTIONS 0 with no pods selected looks
identical to a budget that is fully consumed

What creates a PVC, and what merely asks for one

A StatefulSet volumeClaimTemplate is the only place in a workload spec that creates storage. The controller creates one PVC per replica, named template-statefulset-ordinal, and deliberately never deletes them when the StatefulSet goes away. A volume with claimName does the opposite: it demands a claim that already exists, and the pod stays Pending until one does.

volumeClaimTemplates:            volumes:
  - metadata:                      - name: data
      name: data                     persistentVolumeClaim:
                                       claimName: api-data

creates data-db-0, data-db-1,    requires api-data to exist
data-db-2 for replicas: 3        already, and stays Pending
and keeps them after delete      if it does not

Orphans are never cleaned up

Kubernetes garbage collects an object only when it carries an ownerReference to something that has been deleted. Nothing you write by hand has one, so a ConfigMap left behind by a rename, or a Secret whose consumer was removed, stays in the namespace indefinitely. That is mostly harmless for a ConfigMap and less so for a Secret, which is still readable by anything with get on secrets in that namespace.

kubectl get configmap -n prod

NAME              DATA   AGE
app-config        4      2d
app-config-v1     4      411d      <- nothing mounts this
app-settings      2      270d      <- nor this

nothing in kubectl get says which are referenced