, ,

Ingress to Routes, Load Balancing and DNS Cutover (TKGI to OpenShift Series, Part 20)

Moving wave1-web traffic off NSX-T and onto OpenShift Routes, with a weighted load balancer cutover, a TTL schedule that survives stale resolvers, and the 504 that only appears after you switch. Part 20 of the TKGI to OpenShift Series.

TKGI to OpenShift Series · Part 20 of 26
Key takeaways:
1. Your headline command is oc create route edge web –service=web –hostname=shop.example.com –cert=tls.crt –key=tls.key –port=8080.
2. Author Routes yourself. If you let the ingress to route controller do it, OpenShift names the generated Route after your Ingress plus a random suffix, and a name nobody can predict is a name nobody can pin a certificate rotation or a monitoring check to.
3. A Route carries one primary backend plus up to three alternates, with weights between 0 and 256. That is your in cluster blue green. It cannot send a single request to a Service that still lives on TKGI, so cross platform weight shifting happens at the external load balancer.
4. DNS is an announcement, not a traffic control. Lower the TTL 48 hours ahead, shift weight at the load balancer, and change the record only once the TKGI pool has gone quiet. On my run 1.9 percent of requests still reached the old pool 41 minutes after the record changed.
5. Check the route timeout before you cut, not after. Requests that ran 46 seconds behind the NSX-T virtual server started returning 504 at exactly 30 seconds on OpenShift.

A DNS change is not a cutover. It is an announcement, and every resolver between your users and your zone file is free to ignore it for as long as it likes. That is the blunt version, and it is why this Part spends most of its length inside a load balancer configuration and only a few paragraphs on a zone edit.

Parts 18 and 19 got wave1-web and wave2-data running on OpenShift, verified by row counts rather than pod status. Nothing outside the cluster is talking to them yet. Every real user request still lands on an NSX-T virtual server that NCP, the NSX Container Plugin that turned TKGI Ingress objects into load balancer configuration, built months ago and nobody has touched since. This Part moves that traffic. Two objects change shape on the way: a Kubernetes Ingress becomes an OpenShift Route, which is Red Hat’s own external access object and predates Ingress by several years, and an NSX-T pool member becomes an OpenShift ingress router endpoint.

Who this is for: you have both waves restored and healthy on OCP 4.19, the default ingress controller is serving *.apps.ocp1.example.com, and your production hostnames still point at NSX-T. You control the external load balancer VIP in front of both platforms and you can edit the authoritative DNS zone, or you can get someone to do it inside a change window. Three hostnames move here: shop.example.com, admin.example.com and reports.example.com.

Preflight, Route Inventory and DNS Facts to Collect

Preflight for a cutover is not a checklist of cluster health. Your cluster is fine. Preflight here is a list of facts about things you do not own: resolver behaviour, certificate expiry, load balancer idle timeouts, and whichever team wrote the firewall rule that lets the NSX-T VIP reach your application and has never heard of your new one.

Step 1. Inventory every Ingress and Service on TKGI

On the OpenShift side I use oc rather than kubectl, because it understands Routes, Projects and Security Context Constraints natively and this Part is mostly Routes. On TKGI there is no oc, so the source side stays on kubectl.

# Versions this runbook was written and tested against oc version –client # 4.19.9 kubectl version –client # TKGI 1.18 cluster, 1.29.x # OADP 1.5.1 and Velero 1.16 carried the workloads in Parts 18 and 19 # Every Ingress in the migration waves, with its hostname and TLS secret kubectl get ingress –all-namespaces -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,HOST:.spec.rules[*].host,TLS:.spec.tls[*].secretName NS NAME HOST TLS wave1-web web shop.example.com shop-tls wave1-web web-admin admin.example.com admin-tls wave2-data reports reports.example.com reports-tls # Anything exposed outside HTTP will not become a Route at all kubectl get svc –all-namespaces –field-selector spec.type=LoadBalancer -o custom-columns=NS:.metadata.namespace,NAME:.metadata.name,PORTS:.spec.ports[*].port NS NAME PORTS wave2-data pg-direct 5432

That last row is the one people miss. pg-direct is a raw PostgreSQL port that NCP published on an NSX-T virtual server. OpenShift’s default ingress controller carries ports 80 and 443 and nothing else, so no Route exists that can replace it. It needs either a Service of type LoadBalancer backed by the MetalLB Operator, or a decision that database clients move inside the cluster and stop crossing the network at all. Find these on day one, because each is a separate conversation with a separate team.

Step 2. Record DNS facts, not a DNS diagram

Ask for the authoritative TTL and then measure it yourself, because the two disagree more often than anyone expects. A record that a wiki says has a 300 second TTL frequently turns out to be 3600 in the zone, and a corporate forwarder can hold it far longer than either number.

# What the authoritative server says dig @ns1.example.com +noall +answer shop.example.com shop.example.com. 3600 IN A 10.40.12.20 # What a client on the office network actually gets dig +noall +answer shop.example.com shop.example.com. 2417 IN A 10.40.12.20 # After lowering the zone TTL, confirm the change propagated dig @ns1.example.com +noall +answer shop.example.com shop.example.com. 60 IN A 10.40.12.20

Lower the TTL to 60 seconds at least 48 hours before the window. Not 24. A resolver that picked up the record at 3600 seconds one minute before your change still holds the old value for another 59 minutes, and any cache with a longer minimum TTL configured locally holds it for longer still. Forty eight hours costs you nothing except a slightly chattier zone and it removes an entire category of cutover night surprise.

Where most guides get this wrong: almost every migration writeup treats the DNS flip as the cutover, with a rollback plan that reads change the record back. That plan does not work, because you cannot un-cache an answer. Rolling a record back is exactly as slow as rolling it forward, and it is slow at the worst possible moment. Treat DNS as the last cosmetic step and do the actual traffic shift somewhere you can reverse in one second.

Ingress Objects, Generated Routes and TLS Termination

OpenShift will accept your Ingress objects. When you create one, a controller writes a managed Route to satisfy it, and deleting the Ingress deletes the Route. That sounds like a free migration and it is a trap for three reasons. Generated Routes are named after the Ingress plus a random suffix, so web becomes something like web-gnztq and your automation cannot address it. Ingress has no field for re-encryption or passthrough, so anything beyond plain edge termination arrives through an annotation you have to remember. Worst of all, when the controller declines to convert an Ingress it says nothing at all: no event, no status condition, no log line you will find without asking Red Hat support what happened.

Verdict: write Routes. Keep Ingress objects only for third party charts that hardcode them and cannot be patched, and even then pin the hostname explicitly. Restoring TKGI Ingress manifests with OADP and letting them auto convert is the option to avoid, because a restored Ingress claims your live production hostname the instant it lands.

Step 3. Create the Route on a test hostname first

Never point the first Route at the production hostname. Build it on a name only you know, prove it serves, then add the real name as a second Route once the load balancer is ready. Certificates come from files that live in a mode 0600 directory and never enter git. If you script this, read the key path from an environment variable such as $TLS_KEY_PATH rather than embedding it.

oc project wave1-web # Prove it on a cluster hostname nobody is using oc create route edge web-test –service=web –port=8080 route.route.openshift.io/web-test created oc get route web-test NAME HOST/PORT PATH SERVICES PORT TERMINATION WILDCARD web-test web-test-wave1-web.apps.ocp1.example.com web 8080 edge None # Now the real hostname, with the certificate you already own oc create route edge web –service=web –hostname=shop.example.com –cert="$TLS_CERT_PATH" –key="$TLS_KEY_PATH" –port=8080 Error from server: routes.route.openshift.io "web" is forbidden: spec.host: Invalid value: "shop.example.com": route claims host that is already claimed # Something else already holds it. Find it. oc get routes –all-namespaces -o jsonpath='{range .items[*]}{.spec.host}{"t"}{.metadata.namespace}/{.metadata.name}{"n"}{end}’ | grep shop shop.example.com wave1-web/web-h4x2p

That is a real failure from my own run and it is worth understanding rather than deleting your way past. web-h4x2p is a generated Route. An Ingress manifest came across in the OADP restore back in Part 18, OpenShift converted it silently, and it has been quietly claiming shop.example.com ever since. Nothing broke because DNS still pointed at NSX-T, so nobody noticed. Delete the Ingress, not the Route, or the controller simply rebuilds it.

Below is the mapping I now keep open during every one of these conversions. It is the artifact worth saving out of this Part, because roughly a third of the rows have no clean equivalent and you want to discover that at design time rather than at 02:00.

TKGI side, Ingress or NCPOpenShift equivalentWhat to watch
Ingress rule host plus pathRoute spec.host and spec.pathOne Route per host and path pair, so a three path Ingress becomes three Routes
TLS secret referenced in IngressRoute spec.tls.certificate and keyConverted Ingress defaults to edge termination when no annotation is set
Backend TLS re-encryptiontermination: reencrypt plus destinationCACertificateIngress has no field for it, only the route.openshift.io/termination annotation
TLS passthrough to the podtermination: passthroughFrom an Ingress, set path to empty and pathType to ImplementationSpecific
NCP ingress class annotationNothing, delete itHarmless but misleading, and it hides which controller is really serving
NSX-T load balancer algorithmhaproxy.router.openshift.io/balanceOptions are random, source, roundrobin, leastconn; default is random, and source for passthrough
NSX-T persistence profilerouter.openshift.io/cookie_namePassthrough Routes cannot set cookies, they hash the source IP instead
Source IP allow list on the virtual serverhaproxy.router.openshift.io/ip_allowlistSpace separated only, and 61 entries visible in the generated config
Wildcard hostwildcardPolicy: SubdomainOnly Subdomain or None exist, there is no arbitrary wildcard
Traffic split between two app versionsalternateBackends, up to three extraWeights 0 to 256, in cluster Services only, all zero returns 503
Non HTTP virtual server, for example 5432Service type LoadBalancer with MetalLBNo Route can do this; default ingress controller is 80 and 443 only
Ingress and NCP features mapped to their OpenShift Route equivalents, with the rows that have no equivalent called out.

Step 4. Match the termination mode and fix the timeout

Termination mode is usually easy: if NSX-T was doing SSL offload, you want edge. If it re-encrypted to the pod, you want reencrypt and you need the backend CA. If the application terminated its own TLS, you want passthrough and you lose cookies and HTTP header rewriting along with it. Timeout is the one that ambushes people, because nothing about it appears in any Ingress manifest you are migrating.

# A report endpoint that took 46 seconds on TKGI and nobody complained curl -sS -o /dev/null -w ‘%{http_code} %{time_total}n’ https://web-test-wave1-web.apps.ocp1.example.com/api/report 504 30.014 # HAProxy cut it at 30 seconds. Raise the timeout on that Route only. oc annotate route web-test –overwrite haproxy.router.openshift.io/timeout=120s route.route.openshift.io/web-test annotated curl -sS -o /dev/null -w ‘%{http_code} %{time_total}n’ https://web-test-wave1-web.apps.ocp1.example.com/api/report 200 46.318

Two rules follow from that 504. Raise the timeout per Route rather than globally on the ingress controller, so one slow reporting endpoint does not hold connections open for every application on the cluster. And keep your external load balancer idle timeout higher than your Route timeout, which Red Hat states explicitly, otherwise the load balancer tears down a connection the router still believes is live and you get intermittent resets that are miserable to trace.

If your estate is heavier on non HTTP virtual servers than on Ingress, that changes the shape of this work considerably, and it is one of the places where landing on VKS instead of OpenShift is a reasonable call; see the TKGI to VKS guide for that path. Everything else here assumes OpenShift. For how the ingress controller and OVN-Kubernetes fit together underneath all of this, Part 13 covers the plumbing and this Part will not repeat it.

Weighted Backends and a Staged Traffic Cutover

A Route can carry up to four backends in total, one primary and three alternates, each with an integer weight between 0 and 256 that defaults to 100. Share of traffic is that backend’s weight divided by the sum of all weights. Set every weight to zero and the Route returns a plain 503, which is a genuinely useful thing to know before you try it as a drain mechanism.

oc set route-backends web web=90 web-next=10 route.route.openshift.io/web backends updated oc set route-backends web NAME KIND TO WEIGHT SHARE routes/web Service web 90 90% Service web-next 10 10% # Weight zero drains without killing established connections oc set route-backends web web=0 web-next=100 route.route.openshift.io/web backends updated

Now the honest part, and it is the reason this section exists. alternateBackends only references Services, and only Services in the same namespace of the same cluster. It cannot address a Service that is still running on TKGI. Every blog post that suggests weighted Routes as your migration cutover mechanism has quietly skipped that sentence. Weighted Routes are excellent for the blue green step you do after arrival, when both versions live on OpenShift. Cross platform shifting has to happen one layer up, at whatever external load balancer can hold both a TKGI pool and an OpenShift pool at the same time.

flowchart LR
  C[Client] --> D[DNS shop.example.com]
  D --> V[External LB virtual server]
  V -->|weight falling| A[Pool A, NSX T virtual server]
  V -->|weight rising| B[Pool B, OpenShift ingress routers]
  A --> T[wave1 web pods on TKGI]
  B --> R[Route web, edge TLS]
  R --> O[wave1 web pods on OpenShift]
Both platforms sit behind one virtual server. Traffic moves by changing pool weights, which is reversible in one second, and DNS never changes until the left branch is idle.

Step 5. Shift pool weight in four stages

Add the OpenShift ingress router nodes as a second pool behind the existing virtual server, health checked on /healthz at port 1936 or on your application path, and start it at weight zero. Confirm the pool is green while carrying nothing. Then move in four stages with a soak between each, watching error rate and p95 latency rather than pod status. My stage boundaries were 10, 25, 50 and 90 percent, with soaks of 45, 55 and 70 minutes.

Share of shop.example.com requests served by OpenShift One cutover day, measured at the external load balancer, OCP 4.19.9 and TKGI 1.18 100% 50% 0% 10% 25% 50% 90% 98.1% 09:00 09:20 10:05 11:00 12:10 13:00 baseline stage 1 stage 2 stage 3 stage 4 DNS moved Final bar stops at 98.1 percent. 1.9 percent of requests were still arriving at the TKGI pool 41 minutes after the record changed.
Weight moves in controlled stages. DNS moves once, at the end, and still leaves a residual tail.

Notice what the last bar admits. Even with a 60 second TTL set two days in advance, and with the record changed cleanly, 1.9 percent of requests were still hitting the TKGI pool 41 minutes later. Those came from two internal forwarders with a minimum cache floor configured locally and from a handful of long lived Java clients that resolved once at startup and never looked again. That residual is the whole argument for keeping the old pool alive rather than switching it off the moment the record changes.

Step 6. Move the DNS record last

By the time you touch the zone, the virtual server should already be sending 90 percent or more to OpenShift and every dashboard should be boring. Changing the A record to the OpenShift ingress VIP is then a cleanup step that removes one hop, not a cutover. Leave the old virtual server configured, with the TKGI pool at low weight, for a full 72 hours. Raise the TTL back to 3600 only after that.

Verification, Rollback and Common Failures

Verification for a cutover is a distribution, not a single request. One successful curl proves that one path worked once. What you need is a couple of hundred requests, a status code histogram, and confirmation that the Route was actually admitted by the ingress controller rather than merely accepted by the API server.

# Admitted is the condition that matters. Accepted by the API is not enough. oc get route web -o jsonpath='{.status.ingress[0].conditions[0].type}={.status.ingress[0].conditions[0].status}{"n"}’ Admitted=True # Status distribution, not a single request for i in $(seq 1 200); do curl -sk -o /dev/null -w ‘%{http_code}n’ https://shop.example.com/healthz done | sort | uniq -c 200 200 # Confirm which platform actually served them, from the LB side # (OpenShift routers add a server header the NSX-T pool does not) curl -skI https://shop.example.com/healthz | grep -i ‘^server’ server: haproxy
Rollback, in order: set the OpenShift pool weight to 0 at the external load balancer and the TKGI pool back to 100. That is a one second change and it recovers everything if you have not yet touched DNS. If the record has already moved, revert it and accept that recovery takes as long as the longest cache in the path, which is why the TKGI pool stays warm for 72 hours. Do not delete the OpenShift Routes during a rollback; leave them serving the test hostname so you can keep debugging.
What you seeCauseFix
route claims host that is already claimedA generated Route from a restored Ingress already holds the hostnameDelete the Ingress, not the Route, otherwise the controller recreates it
Admitted=False with reason HostAlreadyClaimedAnother namespace admitted the same host firstFind it across all namespaces, then decide which project legitimately owns the name
503 Application is not availableRoute admitted but the Service has no ready endpoints, or the port name does not matchCheck oc get endpoints and confirm the Route targetPort matches a named Service port
Every request 503 immediately after a weight changeAll backend weights were set to zeroRestore at least one non zero weight; a Route with all weights zero is defined to return 503
504 Gateway Time-out at a consistent 30 secondsDefault HAProxy route timeout is shorter than the NSX-T virtual server wasAnnotate the Route with haproxy.router.openshift.io/timeout and raise the external LB idle timeout above it
Ingress applied, no Route ever appearsConversion silently declined, usually a missing explicit host or an unsupported pathTypeStop using Ingress for that workload and author the Route directly
Sticky sessions lost after cutoverPassthrough Routes cannot set cookies and fall back to source IP hashingMove to reencrypt if you need real session affinity, or set an explicit cookie name on an edge Route
Old pool still taking traffic hours laterA resolver enforcing a local minimum cache TTL, or clients that resolve once at startupKeep the pool warm and drain by weight; identify the client from the LB source addresses
Failure to cause to fix for the cutover itself. Every row here came off a real change window.

Field Note from a Cutover Undone by a Cached TTL

On an earlier engagement, before I had the discipline described above, we did the obvious thing. Routes were built, tested, signed off. At 22:00 we changed the A record and went to watch dashboards. Twelve minutes later the payments team called: roughly a fifth of checkout attempts were failing with a stale session error. Both platforms were serving. A user who resolved to OpenShift, got a session, then hit a resolver that still answered NSX-T on the next request, landed on a pod that had never heard of them.

We rolled the record back at 22:19. Failures did not stop. They kept arriving for another 96 minutes, because reverting a DNS change is exactly as slow as making one and we had simply doubled the number of caches holding a wrong answer. Total user visible impact was 128 minutes for what the change record described as a two minute DNS update. Nobody had made a mistake in any cluster. We had used DNS as a switch, and DNS is not a switch.

What changed after that: session state moved to a shared store before any cutover involving a stateful web tier, both platforms went behind one virtual server, and the DNS record became the last thing anyone touched rather than the first. Same migration, same applications, and the next window produced 0 failed requests across three hostnames.

Shift Weight First, Move DNS Last

My recommendation for this Part is one sentence: put both platforms behind a single load balancer virtual server, move traffic by pool weight, and treat the DNS edit as cleanup you perform after the old pool has already gone quiet. Everything else here, the Route authoring, the termination modes, the timeout annotation, exists to make that shift safe. Skip the shared virtual server and you are left with DNS as your only lever, which means your rollback plan is a cache expiry you do not control.

A clean result looks like this. Every production hostname resolves to the OpenShift ingress VIP. Every Route reports Admitted=True. No Ingress objects remain in the migrated namespaces except ones you deliberately kept. Route timeouts are set per application and sit below the external load balancer idle timeout. Any non HTTP virtual server has an explicit answer, whether that is MetalLB or an in cluster client. Old NSX-T virtual servers still exist but carry zero weight, and there is a dated ticket to remove them.

On Monday, run the Ingress inventory command from Step 1 against your own production TKGI cluster and count how many rows come back with a Service type of LoadBalancer rather than an Ingress. That number is how much of your external access has no Route equivalent at all, and it is the number that decides whether this Part takes you a week or a quarter. Part 21 picks up the secrets, config and pipelines that need to follow the traffic. Full map in the TKGI to OpenShift guide.

TKGI to OpenShift Series · Part 20 of 26
« Previous: Part 19  |  Guide  |  Next: Part 21 »

References

About The Author


Discover more from Journal of Intelligent Infrastructure

Subscribe to get the latest posts sent to your email.

Leave a Reply

Your email address will not be published. Required fields are marked *

Architect’s Toolkit

About the Author

Dr. Pranay Jha is a Cloud and AI Consultant with 18+ years of experience in hybrid cloud, virtualization, and enterprise infrastructure transformation. He specializes in VMware technologies, multi-cloud strategy, and Generative AI solutions. He holds a PhD in Computer Applications with research focused on Cloud and AI, has published multiple research papers, and has been a VMware vExpert since 2016 and a VMUG Community Leader.

Discover more from Journal of Intelligent Infrastructure

Subscribe now to keep reading and get access to the full archive.

Continue reading