A Service, a Deployment, a ConfigMap, an Ingress — four manifests, each individually valid YAML, each individually accepted by kubectl apply. And yet a request to the Service times out, or a pod crash-loops reading an environment variable that was never set. Nothing in any single manifest is wrong. What's wrong is how they reference each other — and Kubernetes validates every manifest's shape without ever checking whether those references actually resolve to anything.
A Service doesn't look pods up by name
There's no field anywhere that says "route to Deployment payments." A Service finds its pods by matching its spec.selector against pod labels — and that match is a subset check, not an equality check: every key in the selector has to be present and equal on the pod, but the pod is free to have other labels the selector never mentions.
pod labels: app=payments, tier=backend
selector: app=payments
every selector key is present and equal on the pod — the pod's extra tier label doesn't matter
✓ matches
selector: app=payments, tier=frontend
the pod is tier=backend, not tier=frontend — one mismatched key fails the whole selector
✗ no match → 0 endpoints
a selector is a subset check, not an equality check — the pod can have labels the selector never mentions
That asymmetry is also where Services silently break. Add one more key to a selector — say, a tier: frontend meant to scope a separate admin-facing Service — and if no pod actually carries that exact label, the Service matches nothing. Not an error: the Service object exists, accepts the apply, and simply never gets a single endpoint. The first sign is usually a connection that hangs or gets refused, traced back through a perfectly valid-looking Service with nothing behind it.
Matching pods isn't the end of it — the port has to match too
A Service's targetPort can be a number or a name. A number is simple — it's just the container port to forward to. A name has to match a name: given to one of the matched pod's containerPort entries, and this is where a typo hides completely: the selector matches fine, the Service gets real endpoints, and traffic still goes nowhere because the named port it's looking for doesn't exist.
# container actually exposes:
ports:
- name: http
containerPort: 8080
# Service looks for a port named "websecure":
spec:
selector:
app: payments # matches fine
ports:
- port: 443
targetPort: websecure # no container port is named thisNothing about this raises an error at apply time, or even at the Service's own status — a selector that matches zero pods and a named port that matches zero container ports both fail exactly the same way: silently. The only way to catch either one ahead of time is to actually compute the match yourself, which is the one thing a schema validator structurally can't do — it checks that a field is a valid string, not that the string refers to something that exists.
ConfigMap and Secret references have the same blind spot
A container can pull environment variables from a ConfigMap or Secret two different ways — envFrom for a whole object's keys at once, or a volumes entry mounting one as files. Both are just a name, written as plain text, with nothing in the manifest itself confirming that name belongs to anything:
containers:
- name: payments
envFrom:
- configMapRef:
name: payments-config # exists in this bundle → resolved
volumeMounts: []
volumes:
- name: tls
secret:
secretName: payments-tls # not in this bundleThat second reference isn't necessarily wrong — plenty of Secrets are created directly in the cluster (by a cert-manager, an external-secrets operator, or just kubectl create secret) and never appear in the YAML you're looking at at all. The honest answer a tool can give here isn't "broken," it's "not in this bundle — check whether it's meant to already exist in the cluster." Confusing those two cases is exactly how a missing Secret turns into a pod stuck in CreateContainerConfigError days after everyone stopped looking at the manifest.
Ingress adds one more hop to the same chain
An Ingress rule routes a host and path to a Service by name and port — which only works if that Service exists, and only sends real traffic if that Service's own selector resolves to pods. One dangling reference anywhere in the chain and the symptom shows up at the very end of it, nowhere near its actual cause:
rules:
- host: api.example.com
http:
paths:
- path: /payments
backend:
service: { name: payments-svc, port: { number: 80 } } # exists → resolves
- path: /legacy
backend:
service: { name: payments-legacy-svc, port: { number: 80 } } # doesn't exist/payments works. /legacy returns a 503 from the ingress controller itself, with every individual manifest in the bundle still perfectly valid — the break is in the reference between them, which is a different category of bug from anything a YAML or schema validator checks.
Try it yourself
Kubernetes Manifest Explorer parses a full bundle and resolves every one of these references for real — which Services select which pods (and which selectors match nothing), which named ports don't exist, which ConfigMap/Secret references are satisfied inside the bundle versus pointing outside it, and which Ingress rules land on a real Service. Kubernetes YAML Validator catches the structural mistakes within a single manifest first. Both run entirely in your browser.