DevTools Hub

Search tools

Search for a developer tool

Why Kubernetes Services Don't Route Traffic

Part of the Kubernetes Toolkit

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 this

Nothing 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 bundle

That 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.

Related tools