Services, Ingress & Networking

Pods are ephemeral—their IPs change on every restart. Services provide stable virtual IPs and DNS names; Ingress and Gateway API expose HTTP routes to the outside world; the CNI plugin wires pod-to-pod traffic on every node; NetworkPolicy (when your CNI supports it) replaces the default allow-all mesh with explicit whitelists. OpenShift layers Routes, OVN-Kubernetes, and egress controls on the same primitives.

developer devops architect ClusterIP Ingress Gateway API Routes
CLI

End-to-end traffic flow

External HTTP traffic enters through an edge controller (Ingress or OpenShift Route), lands on a Service virtual IP, and is load-balanced to ready pod endpoints. East-west pod traffic bypasses Services when apps call each other by DNS name.

flowchart LR
  CLIENT[Client / Browser]
  EDGE[Ingress or Route\n+ controller]
  SVC[Service ClusterIP\nkube-proxy / eBPF]
  EP[EndpointSlices\nready pod IPs]
  POD[Pod containers]

  CLIENT -->|HTTPS host header| EDGE
  EDGE -->|ClusterIP:port| SVC
  SVC --> EP
  EP --> POD

  POD2[Other pod] -.->|CoreDNS short name| SVC

The Service is a stable front door: selectors match pod labels; the Endpoints or EndpointSlice controller maintains the backend list; kube-proxy (iptables/IPVS) or a CNI replacement like Cilium eBPF programs rules on each node so traffic to the ClusterIP is DNAT'd to a healthy pod IP.

🔬 Under the Hood

ClusterIP is virtual—it does not exist on any interface. kube-proxy watches Service and EndpointSlice objects and installs NAT rules so packets destined for 10.96.0.15:80 reach a real pod IP on the node or across the overlay network.

Services

A Service groups pods behind one DNS name and one stable IP (or hostname for ExternalName). The type field controls how that front door is reachable from inside and outside the cluster.

Type Reachability Typical use
ClusterIP (default) Virtual IP reachable only inside the cluster Internal microservice APIs, databases behind app tier
NodePort ClusterIP + static high port (30000–32767) on every node Dev clusters, bare-metal without cloud LB; often paired with external LB
LoadBalancer ClusterIP + cloud LB provisioning external IP/hostname Public TCP/UDP services on EKS/GKE/AKS; L4 exposure
ExternalName CNAME DNS record to external hostname—no proxy, no endpoints Point in-cluster DNS at SaaS API or legacy DB outside cluster
Headless (clusterIP: None) No virtual IP; DNS returns individual pod A records StatefulSet stable identity, client-side LB, service mesh control plane

ClusterIP — default internal load balancer

Kubernetes allocates a ClusterIP from the Service CIDR (configured on the API server, often 10.96.0.0/12). Traffic to service-name.namespace.svc.cluster.local:port hits any ready endpoint selected by spec.selector. Session affinity (sessionAffinity: ClientIP) pins clients to one pod when needed.

NodePort — publish on every node

Extends ClusterIP by opening the same port on all nodes. Clients use <node-ip>:<nodePort>. Firewalls must allow the range; in production you usually front NodePort with an external load balancer or Ingress rather than exposing node IPs directly.

LoadBalancer — cloud integration

The cloud controller manager watches Services of type LoadBalancer and provisions an ELB/NLB, Azure LB, or GCP forwarding rule. spec.loadBalancerIP (deprecated) and annotations vary by provider. On bare metal, MetalLB or kube-vip can satisfy the same API.

ExternalName — DNS alias only

Returns a CNAME for externalName—no kube-proxy rules, no endpoints. Useful for migrating apps gradually: change the Service definition instead of every pod's config when moving from legacy-db.corp to an in-cluster database.

Headless — per-pod DNS

With clusterIP: None, CoreDNS returns A/AAAA records for each ready pod behind the selector (or for StatefulSet pods: pod-0.service-name). Kafka, Cassandra, and etcd clients often need direct pod addresses for partition-aware routing.

yaml
apiVersion: v1
kind: Service
metadata:
  name: api
  namespace: production
  labels:
    app: api
spec:
  type: ClusterIP          # default; omit for ClusterIP
  selector:
    app: api
  ports:
    - name: http
      port: 80             # Service port (what clients use)
      targetPort: 8080     # container port
      protocol: TCP
  sessionAffinity: None

---
# Headless — DNS per pod
apiVersion: v1
kind: Service
metadata:
  name: kafka-brokers
spec:
  clusterIP: None
  selector:
    app: kafka
  ports:
    - port: 9092

---
# ExternalName — CNAME to outside world
apiVersion: v1
kind: Service
metadata:
  name: payments-saas
spec:
  type: ExternalName
  externalName: api.stripe.com
terminal — services
$ kubectl expose deployment api --port=80 --target-port=8080 --name=api
$ kubectl get svc,endpointslices -l app=api -o wide
$ kubectl run tmp --rm -it --image=busybox -- wget -qO- http://api.production.svc:80/health$ oc expose deployment/api --port=8080
→ Service created; use oc expose route for external HTTP
$ oc get svc,endpointslices -l app=api
$ oc run tmp --rm -it --image=registry.redhat.io/ubi9/ubi-minimal -- curl -s http://api:8080/health
⚠️ Pitfall

targetPort can be a name (http) or number—if the pod's named port does not exist, endpoints stay empty and the Service silently black-holes traffic. Always check kubectl get endpoints or EndpointSlices after deploy.

💡 Pro Tip

Use publishNotReadyAddresses: true on headless Services when pods must be discoverable before passing readiness (e.g. Cassandra bootstrap). Default Services only route to ready pods.

Service discovery

Kubernetes does not use a separate service registry—DNS is the discovery mechanism. CoreDNS (default since 1.13) answers queries for Services and Pods from every pod's /etc/resolv.conf.

CoreDNS

CoreDNS runs as a Deployment (typically in kube-system) and is exposed via a kube-dns Service at 10.96.0.10 (cluster-specific). Each node's kubelet configures pod DNS to forward cluster.local queries to CoreDNS. The Corefile ConfigMap defines plugins: kubernetes (in-cluster records), forward (upstream for external names), cache, health.

DNS name formats

Query pattern Resolves to Example
Fully qualified (FQDN) Service ClusterIP or pod IPs (headless) api.production.svc.cluster.local
Cross-namespace Service in another namespace postgres.data.svc.cluster.local
Short name (same namespace) Service in pod's namespace api or api.production
Pod DNS (hostname + subdomain) Pod IP when hostname/subdomain set web-0.nginx.default.svc.cluster.local
Pod IP reverse (optional) <pod-ip-dashed>.<ns>.pod.cluster.local 10-244-1-5.default.pod.cluster.local

Short names and search domains

Kubelet injects dnsPolicy: ClusterFirst by default. Pod resolv.conf includes search paths: namespace.svc.cluster.local, svc.cluster.local, cluster.local. So curl http://api from a pod in production tries api.production.svc.cluster.local automatically.

Pod DNS policies

dnsPolicyBehavior
ClusterFirstCluster DNS first; upstream for external names (default)
ClusterFirstWithHostNetSame as ClusterFirst for pods using hostNetwork
DefaultInherit node resolv.conf—no cluster DNS
NoneUse only dnsConfig you specify

Legacy environment variables

Older discovery mechanism: for each Service active when a pod starts, kubelet injects env vars like API_SERVICE_HOST and API_SERVICE_PORT. Services created after the pod starts are invisible to this mechanism—DNS is always preferred. Enable/disable via the enableServiceLinks field on the Pod spec (default true).

bash
# Inspect DNS from a debug pod
kubectl run dnsutils --rm -it --image=tutum/dnsutils -- nslookup api.production.svc.cluster.local
kubectl exec -it deploy/api -- cat /etc/resolv.conf

# CoreDNS health and config
kubectl -n kube-system get configmap coredns -o yaml
kubectl -n kube-system logs -l k8s-app=kube-dns --tail=50
⚙️ Config Note

Custom upstream DNS: edit CoreDNS forward plugin or set dnsConfig.nameservers on pods. OpenShift 4.x uses CoreDNS with cluster-domain cluster.local by default; OVN-Kubernetes handles network plumbing separately.

🎯 Interview Tip

Be able to explain why curl api works inside a pod but not from your laptop (no search domain / no route to ClusterIP). Know the difference between Service DNS (ClusterIP) and headless DNS (pod A records).

EndpointSlices

EndpointSlices replaced the monolithic Endpoints object for scalability. Each slice holds up to 100 endpoints (configurable); large Services shard across many slices so watches stay small and controllers scale to thousands of backends.

EndpointSlices vs Endpoints

Aspect Endpoints (legacy) EndpointSlice (current)
API v1/Endpoints — one object per Service discovery.k8s.io/v1/EndpointSlice — many per Service
Size limit etcd object size risk at ~1000+ backends Sharded; default max 100 addresses per slice
Dual-stack Awkward IPv4/IPv6 modeling Native addressType IPv4/IPv6/FQDN
Topology hints Limited hints for zone-aware routing (feature-gated)
Status Still created for compatibility Primary source; Endpoints mirrored automatically

EndpointSlice controller

The built-in endpointslice-controller watches Pods and Services. When pod labels match a Service selector and the pod is ready (unless publishNotReadyAddresses), it adds endpoints entries with pod IP, node name, target reference, and optional ready/serving conditions. Manual EndpointSlices are allowed for external backends (e.g. traffic to VMs outside the cluster).

kube-proxy watches

kube-proxy mode (iptables, IPVS, or nftables) watches EndpointSlices labeled with kubernetes.io/service-name. On change, it reprograms NAT rules: each Service port maps to a probabilistic or round-robin backend set. IPVS supports more sophisticated LB algorithms; Cilium/Calico eBPF can bypass kube-proxy entirely and program datapath from EndpointSlice directly.

terminal — endpointslices
$ kubectl get endpointslices -A -l kubernetes.io/service-name=api
$ kubectl describe endpointslices -n production -l kubernetes.io/service-name=api
$ kubectl get endpoints api -n production -o yaml  # legacy mirror$ oc get endpointslices -n production -l kubernetes.io/service-name=api
$ oc describe endpointslices
⚖️ Trade-off

Disabling EndpointSlice mirroring to Endpoints (EndpointSliceMirroring feature) breaks legacy controllers that only watch Endpoints. Most modern ingress controllers and service meshes read EndpointSlices directly.

Ingress

An Ingress resource is declarative routing config—it does nothing until an Ingress controller (nginx, Traefik, AWS ALB, GCE, etc.) watches it and programs the data plane. Ingress is HTTP/HTTPS only (with some controller extensions).

Requires a controller

Installing the Ingress API (networking.k8s.io/v1) does not install a controller. You must deploy one—ingress-nginx, HAProxy Ingress, Istio Gateway, or a cloud-specific operator. The controller reads Ingress objects, Services, EndpointSlices, and Secrets, then updates load balancer config or node-level proxies.

Rules, paths, and backends

Each spec.rules entry matches a host (optional) and http.paths. Path types: Prefix (default longest-prefix match), Exact, ImplementationSpecific (controller-defined). Backend is a Service name + port number—not a pod IP.

TLS

spec.tls lists hostnames and a Secret containing tls.crt + tls.key. cert-manager commonly automates this via Certificate CRDs and ACME. Default ingress-nginx terminates TLS at the controller pod.

Annotations

The Ingress spec is intentionally minimal; controllers extend behavior via annotations—e.g. nginx.ingress.kubernetes.io/ssl-redirect, nginx.ingress.kubernetes.io/proxy-body-size, cert-manager.io/cluster-issuer. Annotation sprawl is a major motivator for Gateway API.

IngressClass

spec.ingressClassName binds an Ingress to a controller implementation (e.g. nginx, alb). The IngressClass resource names the controller (spec.controller) and optional parameters. Legacy annotation kubernetes.io/ingress.class is deprecated.

HTTP/HTTPS limits

  • L7 only — no native TCP/UDP (use Gateway API TCPRoute or Service type LoadBalancer)
  • No mutual TLS client auth in the standard spec (controller annotations or Gateway API)
  • Single resource per host/path merge — conflicting Ingresses across namespaces require controller-specific precedence
  • No built-in traffic splitting — canary via annotations (nginx) or migrate to Gateway API weighted backends
yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-ingress
  namespace: production
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.example.com
      secretName: api-tls
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /v1
            pathType: Prefix
            backend:
              service:
                name: api
                port:
                  number: 80
          - path: /metrics
            pathType: Prefix
            backend:
              service:
                name: api
                port:
                  number: 9090
terminal — ingress
$ kubectl get ingressclass
$ kubectl describe ingress api-ingress -n production
$ kubectl get ingress -A -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,CLASS:.spec.ingressClassName,HOSTS:.spec.rules[*].host,ADDR:.status.loadBalancer.ingress[*].ip$ oc get ingress -n production
→ On OCP, prefer Route for edge HTTP; Ingress supported with optional controller
$ oc describe ingress api-ingress
⚠️ Pitfall

Applying an Ingress without a matching IngressClass or running controller leaves status.loadBalancer empty forever. Always verify controller pods are healthy and ingressClassName matches your deployment.

📦 Real World

Most EKS/GKE teams use cloud-specific Ingress controllers (ALB, GCE) that provision real L7 load balancers with WAF integration. Self-managed clusters typically run ingress-nginx as a DaemonSet or behind an external LB.

Gateway API

Gateway API is the successor to Ingress—role-oriented resources, expressive routing, and first-class traffic splitting. GA since Kubernetes 1.28 for core types (GatewayClass, Gateway, HTTPRoute).

Resource model

ResourceOwnerPurpose
GatewayClass Cluster admin / infra team Defines controller implementation (like IngressClass); cluster-scoped
Gateway Platform team Listener bindings (host, port, TLS); allocates shared LB infrastructure
HTTPRoute App team Path/host rules, filters, backend refs with weights—attached to Gateway via parentRefs

RBAC separation

Three personas get distinct permissions: infra creates GatewayClass + Gateway; developers create HTTPRoutes in their namespaces that reference allowed Gateways via parentRefs and policy (ReferenceGrant). No more cluster-wide Ingress annotation wars—route attachment is explicit and policy-gated.

Traffic splitting

HTTPRoute.spec.rules[].backendRefs supports weight for canary/blue-green across Services. Filters support redirects, header modification, URL rewrites, and request mirrors—portable across conformant implementations (Cilium, Envoy Gateway, Contour, Istio).

yaml
apiVersion: gateway.networking.k8s.io/v1
kind: GatewayClass
metadata:
  name: envoy
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller
---
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: public-gw
  namespace: infra
spec:
  gatewayClassName: envoy
  listeners:
    - name: https
      protocol: HTTPS
      port: 443
      hostname: api.example.com
      tls:
        mode: Terminate
        certificateRefs:
          - name: api-tls
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: api-route
  namespace: production
spec:
  parentRefs:
    - name: public-gw
      namespace: infra
  hostnames:
    - api.example.com
  rules:
    - matches:
        - path:
            type: PathPrefix
            value: /v1
      backendRefs:
        - name: api-stable
          port: 80
          weight: 90
        - name: api-canary
          port: 80
          weight: 10
⚖️ Trade-off

Gateway API is cleaner but requires a conformant controller and Kubernetes 1.28+ for GA types. Ingress remains ubiquitous; many teams run both during migration. GRPCRoute and TLSRoute are GA in later releases for L4/L7 non-HTTP.

🎯 Interview Tip

Contrast Ingress (single resource, annotation extensions) with Gateway API (GatewayClass → Gateway → HTTPRoute, RBAC boundaries, native weights). Mention GA in 1.28 and that it's a separate CRD family, not a replacement for Services.

CNI — Container Network Interface

Every pod gets an IP from a pod CIDR. The CNI plugin (invoked by kubelet on pod create) attaches the pod to the network: veth pair, bridge, tunnel, or direct routing. Pod-to-pod traffic may stay on the same node or cross the cluster overlay/underlay.

Pod-to-pod networking

Flow: kubelet calls CNI ADD → plugin creates interface in pod netns → assigns IP from IPAM → programs host routes/tunnels/NAT. East-west traffic between pods on different nodes requires a cluster-wide routable pod CIDR or an overlay encapsulating packets between nodes.

Popular plugins

Plugin Datapath NetworkPolicy Notes
Flannel VXLAN or host-gw overlay No (pair with Calico policy or kube-router) Simple; good for getting started
Calico BGP or VXLAN/IP-in-IP Yes (iptables/eBPF) Enterprise standard; flat L3 underlay option
Cilium eBPF (VXLAN/GENEVE optional) Yes (eBPF) Replaces kube-proxy; Hubble observability; Gateway API
OVN-Kubernetes OVS + OVN logical switches Yes Default on OpenShift 4.12+; integrated egress IP/firewall
Weave Net VXLAN mesh Yes (limited) Encrypted overlay option; less common in new clusters

VXLAN vs BGP

ApproachHow it worksPros / cons
VXLAN overlay Pod packets encapsulated UDP 4789 between nodes; works on any L3 underlay No ToR BGP config; ~50 byte overhead; FDB learning
BGP (Calico) Each node advertises pod CIDR routes to ToR routers via BGP Native routing, no encapsulation; requires network team cooperation
host-gw (Flannel) Direct L2 adjacency—routes on host for remote pod subnets Fast; requires L2 connectivity between nodes
🔬 Under the Hood

CNI is a thin spec: ADD, DEL, CHECK, VERSION JSON over stdin. Multus allows multiple CNIs per pod (sidecar NICs). The choice of CNI determines whether NetworkPolicy and egress controls actually enforce.

⚙️ Config Note

Pod CIDR per node is assigned at node registration (--pod-cidr on kube-controller-manager for cloud setups, or IPAM pools in CNI config). Overlapping pod/service CIDRs with the corporate network is a classic Day-1 outage—plan before install.

NetworkPolicy

By default, every pod can reach every pod—a flat zero-trust nightmare at scale. NetworkPolicy is a whitelist model: once a pod is selected by a policy, only explicitly allowed traffic is permitted. Enforcement requires a CNI that implements NetworkPolicy (Calico, Cilium, OVN-Kubernetes—not vanilla Flannel).

Default allow all

Without any NetworkPolicy, all ingress and egress between pods is allowed (subject to normal routing). NetworkPolicy does not provide a global "default deny"—you create that pattern explicitly with an empty ingress: [] / egress: [] rule set.

Whitelist model

Policies are additive for allow rules: if any policy allows a connection, it is permitted. To restrict a pod, select it with podSelector and list only permitted peers. Combine ingress and egress policies for defense in depth.

Ingress and egress rules

  • ingress — who can connect to selected pods (ports + from peers)
  • egress — where selected pods can connect out (ports + to peers)
  • policyTypes — declare Ingress, Egress, or both; unset defaults to what's defined in rules

Selectors and peers

Peer typeMatches
podSelectorPods in same namespace with matching labels
namespaceSelector + podSelectorPods in namespaces matching labels
ipBlockCIDR outside cluster (except metadata IP exceptions)
Empty from/toAll sources/destinations (within policy direction)

Default deny pattern

Platform teams typically apply namespace-level default deny, then layer explicit allow policies per app:

yaml
# Deny all ingress to every pod in namespace
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-ingress
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Ingress
  ingress: []

---
# Allow frontend → api on port 8080 only
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-allow-frontend
  namespace: production
spec:
  podSelector:
    matchLabels:
      app: api
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app: frontend
      ports:
        - protocol: TCP
          port: 8080
  egress:
    - to:
        - podSelector:
            matchLabels:
              app: postgres
      ports:
        - protocol: TCP
          port: 5432
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
          podSelector:
            matchLabels:
              k8s-app: kube-dns
      ports:
        - protocol: UDP
          port: 53

CNI requirement

NetworkPolicy is API-only until the CNI datapath enforces it. Verify your plugin: kubectl get networkpolicies succeeding does not mean rules are active. Test with nc or curl from labeled pods after applying deny policies.

OpenShift: EgressNetworkPolicy / EgressFirewall

Legacy OpenShift EgressNetworkPolicy (cluster-level CIDR allow/deny) is superseded by EgressFirewall CRD in OVN-Kubernetes—namespace-scoped egress rules with deny/allow lists and DNS name matching. Complements Kubernetes NetworkPolicy for north-south egress control.

terminal — networkpolicy
$ kubectl get networkpolicy -A
$ kubectl describe networkpolicy api-allow-frontend -n production
$ kubectl run test-$RANDOM --rm -it --image=nicolaka/netshoot -- curl -m2 http://api:8080$ oc get networkpolicy -n production
$ oc get egressfirewall -n production
→ EgressFirewall requires OVN-Kubernetes (default OCP 4.12+)
🔒 Security

NetworkPolicy is not a WAF—it filters L3/L4 between pods. Combine with Ingress TLS, service mesh mTLS, and PSA/SCC for complete posture. Remember to allow DNS egress (UDP/TCP 53 to kube-dns) or apps silently fail hostname resolution.

⚠️ Pitfall

Selectors only match pods in the same namespace as the policy unless you use namespaceSelector. A policy in production cannot select pods in staging with podSelector alone.

OpenShift networking

OpenShift Container Platform 4.12+ uses OVN-Kubernetes as the default CNI—replacing OpenShift SDN. Edge HTTP(S) is exposed via first-class Route resources backed by the HAProxy-based ingress controller operator, plus integrated egress IP and EgressFirewall for controlled outbound traffic.

OVN-Kubernetes (default OCP 4.12+)

OVN-Kubernetes uses Open vSwitch and the OVN northbound/southbound databases for logical switching and routing. It implements Kubernetes NetworkPolicy, supports hybrid overlay/underlay, IPv6, and OpenShift-specific features: Egress IP, EgressFirewall, and multicast. The openshift-ovn-kubernetes operator manages rollout.

Routes

A route.openshift.io/v1/Route is OpenShift's native edge object—similar to Ingress but integrated with the platform. Creating a Route triggers the ingress controller (HAProxy pods in openshift-ingress) to expose the Service on the cluster's apps domain.

HAProxy router

The default ingress controller runs HAProxy as a DaemonSet or Deployment behind a cloud LB or bare-metal VIP. It watches Routes, Services, and EndpointSlices, generates HAProxy config, and reloads on change. Shard multiple controllers via IngressController CR for multi-tenant or DMZ/internal separation.

TLS termination types

Route TLS type Behavior Use case
edge (default) Router terminates TLS; plain HTTP to backend Service Public HTTPS apps with cluster-managed certs
passthrough TCP mode—TLS flows end-to-end to pod (SNI routing) App manages its own certs; gRPC with TLS
reencrypt Router terminates client TLS, re-encrypts to backend with new cert Zero-trust internal hop; compliance segmentation
insecure / None HTTP only Dev; redirect to HTTPS via annotation

Wildcard DNS

Default apps domain: <app>-<project>.<apps-domain> (e.g. myapp-production.apps.cluster.example.com). Wildcard DNS (*.apps.cluster.example.com) points to the router LB. Routes can specify custom spec.host when DNS is configured.

Route vs Ingress

Aspect OpenShift Route Kubernetes Ingress
Platform support First-class; oc expose creates automatically Requires separate controller install
TLS modes edge, passthrough, reencrypt built-in TLS termination via Secret; passthrough via annotation
TCP/UDP Supported via Route passthrough / IngressController tuning Not in core Ingress spec
Portability OpenShift / OKD specific API Portable across distributions

Egress IP

Assigns a routable node IP to selected namespaces so outbound traffic SNATs to a known address—required for firewall allow-lists to SaaS partners. The EgressIP CR selects namespaces/pods; OVN-Kubernetes moves the IP between nodes for HA.

Egress Firewall

Namespace-scoped EgressFirewall defines ordered deny/allow rules for egress CIDRs and DNS names. Stricter than NetworkPolicy for "this namespace may only reach these external endpoints."

Install ingress-nginx (or cloud controller), set ingressClassName, manage TLS Secrets or cert-manager, point DNS to LB IP.

oc expose svc/api or oc create route edge api --service=api --hostname=api.apps.cluster.example.com—router and wildcard DNS wired by the platform.

yaml
apiVersion: route.openshift.io/v1
kind: Route
metadata:
  name: api
  namespace: production
  annotations:
    haproxy.router.openshift.io/timeout: 60s
spec:
  host: api.apps.cluster.example.com
  to:
    kind: Service
    name: api
    weight: 100
  port:
    targetPort: http
  tls:
    termination: edge
    insecureEdgeTerminationPolicy: Redirect
  wildcardPolicy: None
terminal — openshift networking
$ # vanilla K8s — no Route API; use Ingress or Gateway API$ oc expose svc/api --hostname=api.apps.cluster.example.com
$ oc get route api -o yaml
$ oc get ingresscontroller -n openshift-ingress-operator
$ oc get egressip,egressfirewall -A
$ oc adm pod-network join-projects production networking-project  # Multus secondary NIC
🔴 OpenShift

OVN-Kubernetes is non-optional on new OCP installs. Migrating from OpenShift SDN requires cluster upgrade paths documented by Red Hat—plan maintenance windows. Routes replace Ingress for most internal teams; Ingress CRDs still work if you deploy a controller.

📦 Real World

Enterprise OCP customers use Egress IP so partner banks whitelist a fixed outbound IP. Combined with EgressFirewall, platform teams enforce "production namespaces cannot reach the internet except APIs X, Y, Z" without per-pod sidecars.

💡 Pro Tip

oc get route -o jsonpath='{.items[*].spec.host}' lists all edge URLs. For reencrypt Routes, the Service must serve TLS—the router validates the backend cert against spec.tls.destinationCACertificate.