You know Kubernetes Services. You may know Ingress. Then someone hands you an OpenShift cluster and says “expose the app with a Route.” The YAML looks almost familiar. The field names are not.

I was in that spot. I could debug a broken Ingress by checking the controller, the backend Service, and endpoint readiness. Routes reused the same mental stack — stable Service in the middle, edge object in front — but the tooling and TLS defaults surprised me until I read one Route carefully and traced a request end to end.

This post is for Kubernetes users who want that trace spelled out. I will compare Route, Service, and Ingress without pretending they are interchangeable. I will stay modest about advanced routing (weighted splits, shard routers, Gateway API). Those exist. Most beginners need one hostname, one Service, and a working TLS story first.

Three layers — Pod, Service, Route

The pattern is the same as elsewhere in Kubernetes:

  1. Pods run containers and listen on container ports.
  2. Services provide a stable cluster IP and DNS name over ready Pods.
  3. Routes (OpenShift) or Ingress (portable Kubernetes) publish HTTP/S hostnames that point at a Service.

Nothing in that stack replaces a Deployment. If Pods are not ready, neither a Service nor a Route invents healthy backends.

Minimal working stack:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: web
  labels:
    app: web
spec:
  replicas: 2
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
        - name: web
          image: nginx:1.27
          ports:
            - containerPort: 8080
          readinessProbe:
            httpGet:
              path: /
              port: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: web
  labels:
    app: web
spec:
  selector:
    app: web
  ports:
    - name: http
      port: 80
      targetPort: 8080
---
apiVersion: route.openshift.io/v1
kind: Route
metadata:
  name: web
  labels:
    app: web
spec:
  to:
    kind: Service
    name: web
    weight: 100
  port:
    targetPort: http
  tls:
    termination: edge
    insecureEdgeTerminationPolicy: Redirect

Apply and inspect:

oc apply -f web-stack.yaml
oc get deploy,svc,route -l app=web
oc get endpoints web

If endpoints shows no ready addresses, fix the Deployment before debugging the Route. I still catch myself staring at Route YAML when the Service selector is wrong. The edge object is rarely the first broken layer.

Service vs Route — what each one owns

A ClusterIP Service answers: “Which ready Pods receive traffic for web:80 inside the cluster?”

A Route answers: “Which external hostname sends traffic to Service web on which port, and how is TLS handled at the edge?”

They compose. Neither duplicates the other.

ResponsibilityServiceRoute
Stable in-cluster nameYes (web, web.myproject.svc)No
Load balance across PodsYes (via kube-proxy / dataplane)No — forwards to Service
External DNS hostnameNoYes (spec.host or generated)
HTTP path rules (multiple paths)NoLimited — see Ingress for complex paths
TLS at cluster edgeNoYes (spec.tls)

Inside the cluster, other Pods still call http://web or http://web.myproject.svc.cluster.local. The Route does not replace that. It adds an external front door managed by the OpenShift Router.

Check the Service the way you would on any cluster:

kubectl get svc web -o wide
kubectl get endpoints web
kubectl describe svc web

Check the Route with OpenShift-aware commands:

oc get route web
oc describe route web
oc get route web -o yaml

Sample oc get route output columns you will use often:

oc get route
# NAME   HOST/PORT                                    PATH   SERVICES   PORT   TERMINATION   WILDCARD
# web    web-myproject.apps.cluster.example.com ...          web        http   edge          None

The HOST column is what you curl from outside (with DNS or /etc/hosts pointing correctly). SERVICES and PORT must match your Service name and port name or number.

Route vs Ingress — same job, different objects

Ingress is the portable Kubernetes API (networking.k8s.io/v1). You install an Ingress controller; it watches Ingress objects and programs a proxy.

Route is OpenShift’s route.openshift.io/v1 API. The OpenShift Router watches Routes by default on most clusters.

OpenShift clusters often run both a Router for Routes and an Ingress Controller for Ingress resources. Teams pick one style per app to avoid two edge configs for the same Service.

My practical advice: if the platform docs and colleagues use Routes, learn Routes first. If your GitOps repo standardizes on Ingress for multi-cluster portability, use Ingress — but confirm an IngressClass and controller exist on the OpenShift cluster. An Ingress object without a controller behaves like an Ingress anywhere else: correct YAML, no traffic.

Hostnames — spec.host and generated names

Every Route needs a hostname clients can resolve. Two patterns:

Explicit host — you set spec.host:

spec:
  host: api.myproject.apps.prod.example.com

Generated host — omit spec.host and OpenShift assigns one under the cluster apps domain, often derived from the Route name, project, and cluster DNS suffix:

oc create route edge web --service=web --port=http
oc get route web -o jsonpath='{.spec.host}{"\n"}'

Find the cluster apps domain:

oc get ingresscontroller default -n openshift-ingress-operator -o jsonpath='{.status.domain}{"\n"}'

DNS must point clients at the Router’s external address or load balancer. Inside corporate networks that often means a wildcard *.apps.cluster.example.com already resolves. In lab clusters you may patch /etc/hosts:

# after oc get route web
curl -v https://web-myproject.apps.cluster.example.com/

If the browser resolves the name but connection fails, suspect firewall, wrong wildcard DNS, or Router load balancer not reachable — not necessarily the Pod.

Wildcard Routes (spec.wildcard: Subdomain) exist for advanced cases. I rarely need them on day one.

Edge TLS — termination modes that matter

TLS confused me until I separated where the certificate terminates from what the Pod sees.

OpenShift Route spec.tls.termination common values:

ModeClient to RouterRouter to Service/Pod
edgeHTTPS (Router cert)Usually HTTP
passthroughHTTPS end-to-endHTTPS (Router does not decrypt)
reencryptHTTPS (Router cert)HTTPS (Router uses backend cert)

Edge termination is the default pattern on many internal apps:

spec:
  tls:
    termination: edge
    insecureEdgeTerminationPolicy: Redirect

Clients hit https://host/. The Router terminates TLS. Traffic to the Service is often plain HTTP on port 80. Your Pod readiness probe can stay HTTP on the container port — that is normal, not a misconfiguration.

Passthrough when the app must handle TLS itself (or TLS to the Pod is required):

spec:
  tls:
    termination: passthrough
  port:
    targetPort: 8443

Reencrypt when you want edge TLS and encrypted traffic to the backend — the platform team usually owns the destinationCACertificate details.

Reference a TLS Secret for a custom edge cert (pattern varies by cluster):

oc create secret tls shop-tls --cert=tls.crt --key=tls.key

When curl shows certificate errors, check termination mode, host in cert SANs, and whether you are hitting HTTP on an HTTPS-only Route.

oc get route and quick creation

List and inspect Routes:

oc get route
oc get route -A
oc get route web -o wide
oc get route web -o jsonpath='{.spec.host}{"\n"}'
oc describe route web
kubectl get route -n myproject

Create with oc when prototyping — for GitOps I prefer explicit YAML:

oc expose svc web
oc create route edge web --service=web --port=http --hostname=shop.example.com
oc expose deployment/web --port=8080
kubectl apply -f route.yaml

Debugging from Route to Pod — a calm order

When https://my-host/ fails, I walk this sequence. It mirrors Ingress debugging with different commands at the edge.

Step 1 — Route exists and has a host:

oc get route
oc describe route web

Look for Accepted status, correct Host, Service: web, TargetPort: http (or numeric port), and TLS termination. oc describe route shows admitted conditions and sometimes certificate details.

Step 2 — Service and endpoints:

kubectl get svc web
kubectl get endpoints web
kubectl describe svc web

Zero endpoints means no ready Pods match the selector, or port names do not align. Fix labels, probes, or targetPort first.

Step 3 — Pods ready:

kubectl get pods -l app=web
kubectl describe pod -l app=web
kubectl logs -l app=web --tail=50

Step 4 — In-cluster curl (bypass Route):

kubectl run curl-test --rm -it --restart=Never --image=curlimages/curl -- \
  curl -sv http://web.myproject.svc.cluster.local/

If in-cluster works but external Route fails, suspect DNS to the Router, TLS mode, or corporate proxy — not the Deployment.

Step 5 — From outside, verbose curl:

curl -vk https://web-myproject.apps.cluster.example.com/
curl -v http://web-myproject.apps.cluster.example.com/

Redirect loops sometimes mean insecureEdgeTerminationPolicy: Redirect while you test HTTP only. TLS mismatch sometimes means passthrough Route hitting an HTTP-only Pod port. Router logs live in openshift-ingress — ask platform admins for the current label selector if you need them.

Common symptoms:

SymptomLikely cause
503 / no healthy upstreamService has no endpoints
Wrong appRoute points at wrong Service or project
Certificate errorEdge cert/host mismatch, expired cert
Connection refused externallyDNS or LB not aimed at Router
Works in cluster, not outsideRoute/TLS/DNS layer, not Pod

OpenShift clusters may also run an Ingress Controller for standard Ingress — separate from the Router. Check oc get ingressclass before assuming Ingress YAML will work. Do not expose the same Service with both a Route and an Ingress unless the team wants that on purpose.

Final thought

Routes are not a mystery separate from Kubernetes networking. They are the OpenShift edge declaration: hostname and TLS in front, Service behind, Pods at the bottom.

If you already debug Ingress by checking controller → Ingress rule → Service → endpoints → Pods, reuse that order. Swap the first step to oc get route and oc describe route. Keep kubectl get endpoints in the middle where it always belonged.

Learn one explicit Route YAML, one generated-host Route, and one edge TLS example. Trace a failing URL once from curl to Pod logs. After that, Routes feel like a dialect you already spoke — slightly different vocabulary, same grammar.