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.
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.
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.
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
$ 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
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.
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
| dnsPolicy | Behavior |
|---|---|
| ClusterFirst | Cluster DNS first; upstream for external names (default) |
| ClusterFirstWithHostNet | Same as ClusterFirst for pods using hostNetwork |
| Default | Inherit node resolv.conf—no cluster DNS |
| None | Use 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).
# 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
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.
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.
$ 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
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
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
$ 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
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.
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
| Resource | Owner | Purpose |
|---|---|---|
| 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).
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
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.
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
| Approach | How it works | Pros / 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 |
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.
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 type | Matches |
|---|---|
| podSelector | Pods in same namespace with matching labels |
| namespaceSelector + podSelector | Pods in namespaces matching labels |
| ipBlock | CIDR outside cluster (except metadata IP exceptions) |
| Empty from/to | All sources/destinations (within policy direction) |
Default deny pattern
Platform teams typically apply namespace-level default deny, then layer explicit allow policies per app:
# 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.
$ 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+)
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.
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.
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
$ # 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
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.
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.
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.