, ,

Ingress, Load Balancing and DNS Cutover on VKS (TKGI to VKS Series, Part 20)

The traffic move from TKGI to VKS is a drain, not a DNS flip. This runbook stands up a load balanced ingress on VKS, lowers TTL, shifts traffic in weighted steps, and keeps a clean rollback the whole way.

TKGI to VKS Series · Part 20 of 26

TL;DR · Key Takeaways

  • Cutover is a drain, not a flip. You stand up ingress on VKS, prove it green while the TKGI VIP still serves everyone, then shift traffic in weighted steps you can reverse in seconds.
  • Headline move, applied on the target: a Service with loadBalancerClass ako.vmware.com/avi-lb and an Ingress with ingressClassName avi-lb, so the Avi Kubernetes Operator, AKO, programs the external VIP for you.
  • Lower DNS TTL to 30 seconds a full 48 hours before the window. A short TTL set on the day does nothing, because resolvers still hold the old record until the old, longer TTL expires.
  • Your TKGI Ingress manifests do not port over unchanged. NSX-T annotations mean nothing to AKO, so ingressClassName and the annotations get rewritten, not copied.
  • Keep both VIPs live and the source authoritative until analytics show zero hits on the old NSX-T VIP. DNS weight is how you move traffic, the load balancer is how you roll back.
  • Tested against VCF 9.0, VKS on vSphere Supervisor cluster 1.31, Avi Load Balancer Enterprise with AKO 1.13, kubectl v1.32, source TKGI 1.18 with NSX-T load balancing.
Who this is for: platform engineers, SREs and network admins who moved the stateless tier in Part 18 and the database in Part 19, and now have to send live users to VKS. You have kubectl vSphere login on the VKS cluster, a platform load balancer already chosen for the Supervisor, Avi Load Balancer with AKO available to the VKS cluster, and control over the DNS zone that fronts the app.

Where the migration stands before cutover

My platform lead asked one question before signing off on this window: if it goes wrong at 40 percent, how fast can you put everyone back on TKGI. That question, not the ingress YAML, is what shapes a cutover. Everything up to now was reversible because the source stayed untouched. Part 18 carried the storefront frontend onto the VKS cluster dev-tkg-01, Part 19 moved the webapp database with a checkpoint and a storage class map, and through both the TKGI estate kept serving every real user. This part is where a user request finally lands on VKS instead of TKGI, and the whole craft is making that shift happen in slices small enough that any one slice can be pulled back before anyone files a ticket.

Two terms before the runbook. Ingress is the Kubernetes object that maps a hostname and path to a Service, and it needs an ingress controller to turn that intent into a real load balancer configuration. AKO, the Avi Kubernetes Operator, is that controller here, a pod in the cluster that watches Ingress and Service objects and programs the Avi Load Balancer Service Engines to match. On TKGI the same job was done by the NSX-T load balancer driven by annotations on the Ingress. Those annotations are the first thing that does not survive the move, and pretending they do is the fastest way to a green apply that routes nothing.

The running example for this window is the webapp namespace, a web and api tier in front of the PostgreSQL database Part 19 landed on vks-vsan-default. On TKGI it is reached at shop.example.internal through an NSX-T virtual server at 10.20.30.40. On VKS it will be reached at the same hostname, through an Avi VIP that AKO allocates, and the only thing that decides which one a user hits is a DNS record you control.

Preflight before you touch a DNS record

Four checks earn the right to move traffic, and every one of them is cheaper than a rollback. AKO must be running and watching the ingress class you are about to use. VKS must already have an external VIP for the Service, because a hostname pointed at nothing is worse than no change at all. You must know the current DNS record and its TTL, because that number sets how long a mistake lingers. And the app on VKS must answer correctly when you address it by host header directly, before any user is involved.

# Tested against VCF 9.0, VKS cluster 1.31, Avi Enterprise with AKO 1.13, kubectl v1.32, source TKGI 1.18
# AKO is up and the ingress class exists
kubectl –context dev-tkg-01 -n avi-system get pods -l app.kubernetes.io/name=ako
kubectl –context dev-tkg-01 get ingressclass

# the app Service on VKS already has an external VIP
kubectl –context dev-tkg-01 -n webapp get svc web –output wide

# current public record and its TTL, before you change anything
dig +nocmd shop.example.internal a +noall +answer

What green looks like: the ako pod reports Running, an ingressclass named avi-lb is present, the Service shows an EXTERNAL-IP that is a real address and not Pending, and dig returns the old NSX-T VIP with whatever TTL is set today. On this estate that TTL came back as 300, and 300 is the first thing that has to change, days ahead of the window, not during it.

$ kubectl –context dev-tkg-01 -n avi-system get pods -l app.kubernetes.io/name=ako
NAME READY STATUS RESTARTS AGE
ako-0 1/1 Running 0 6d

$ kubectl –context dev-tkg-01 get ingressclass
NAME CONTROLLER AGE
avi-lb ako.vmware.com/avi-lb 6d

$ dig +nocmd shop.example.internal a +noall +answer
shop.example.internal. 300 IN A 10.20.30.40

Expose the app on VKS behind a load balanced ingress

Pick the ingress path before you write a manifest, because the choice decides who owns Layer 7. Foundation Load Balancer and NSX Load Balancer give the Supervisor clean Layer 4 out of the box, but neither does HTTP routing, TLS termination or a web application firewall on its own. Avi with AKO adds Layer 7 at the platform, and because VKS clusters are upstream conformant you can also run an in-cluster controller like Contour or the Gateway API if you would rather own routing inside the cluster. For a hostname that already terminated TLS and did path routing on NSX-T, Avi with AKO is the closest match and the least surprising for the app team.

Ingress path on VKS Layer Best fit for this cutover Watch out
Foundation or NSX Load Balancer Layer 4 Plain TCP services with no HTTP routing or TLS on the balancer No WAF, no path routing, you add an in-cluster controller for Layer 7
Avi Load Balancer with AKO Layer 4 and 7 A hostname that already did TLS and path routing on NSX-T Needs an Avi Enterprise entitlement and AKO wired to the cluster
In-cluster Contour or Gateway API Layer 7 App teams that want to own routing config inside the cluster Still needs a Layer 4 VIP underneath from FLB, NSX-LB or Avi

Table 1. Ingress path decision for the cutover. This is the reference artifact, pick one row per hostname and record it beside the DNS change ticket.

With Avi chosen, the Service and Ingress are short. The Service uses loadBalancerClass so AKO, and not some other controller, claims it, and the Ingress uses ingressClassName avi-lb. This is where the TKGI manifest gets rewritten. On TKGI the same Ingress carried an ncp/ or nsx annotation and no ingressClassName at all, and if you paste that here AKO ignores it and the ADDRESS never fills in.

# apply on the TARGET, VKS cluster dev-tkg-01, namespace webapp
apiVersion: v1
kind: Service
metadata:
name: web
namespace: webapp
spec:
type: LoadBalancer
loadBalancerClass: ako.vmware.com/avi-lb
selector:
app: web
ports:
– port: 443
targetPort: 8443

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: shop
namespace: webapp
spec:
ingressClassName: avi-lb
tls:
– hosts: [shop.example.internal]
secretName: shop-tls
rules:
– host: shop.example.internal
http:
paths:
– path: /
pathType: Prefix
backend:
service:
name: web
port:
number: 443

Apply it, then wait for AKO to program Avi and hand back a VIP. The Ingress ADDRESS is the number you will eventually point DNS at, so do not go further until it is populated and the app answers on it by host header. Address the VIP directly with a Host header, so you are testing the new path with zero users and zero DNS involved.

$ kubectl –context dev-tkg-01 -n webapp get ingress shop
NAME CLASS HOSTS ADDRESS PORTS AGE
shop avi-lb shop.example.internal 10.80.12.50 80, 443 3m

# test the new VIP directly, no DNS, resolve the host to the Avi VIP
$ curl -sS -o /dev/null -w ‘%{http_code}n’ https://shop.example.internal/healthz
–resolve shop.example.internal:443:10.80.12.50
200
flowchart TD
  A[app live on VKS, Ingress ADDRESS filled] --> B[lower TTL to 30s, 48h ahead]
  B --> C[shift 10 percent to VKS VIP]
  C --> D{error rate within budget}
  D -- yes --> E[shift 50 percent]
  D -- no --> R[revert weight to TKGI VIP]
  E --> F{p95 latency healthy}
  F -- yes --> G[shift 100 percent]
  F -- no --> R
  G --> H[drain TKGI, watch old VIP hits]
  H --> I[zero hits, retire TKGI ingress]
  R --> A
Figure 1. Weighted cutover with rollback at every gate. Each step either advances to the next weight or reverts to the TKGI VIP, and nothing here touches the source cluster.

Lower TTL and shift traffic in weighted steps

The single most common cutover mistake is treating DNS TTL as a same-day setting. A record has whatever TTL it was last served with, and a resolver holds that record until it expires. If your TTL was 300 this morning and you drop it to 30 as you start the window, resolvers that already cached the record at 300 will keep it for up to five more minutes, and some corporate resolvers enforce their own minimum well above yours. Lower the TTL 48 hours ahead, confirm the low value has propagated, and only then start moving weight.

Contradicts the tutorial default: most guides say cut over by changing the A record, as if DNS were the traffic control. It is not. DNS is slow and cached and cannot be trusted to roll back in seconds, because a resolver you do not own can hold a stale answer past your TTL. Move traffic with weight at the load balancer or through Avi GSLB, where a weight change takes effect on the next request, and use DNS only to point at a stable front you can re-weight. When someone says just flip the record, they are describing the one lever with the worst rollback latency in the whole system.

With a low TTL confirmed, shift in steps. On this estate the front was an Avi GSLB service with two pool members, the TKGI VIP and the VKS VIP, and the weight ratio decides the split. Start at 10 percent to VKS, hold, read the error rate and latency, then 50, then 100. The commands below show the DNS side settling to the low TTL and the first weighted step, with the real failure that catches people, an ADDRESS that never filled because the ingress class was wrong.

# 48h earlier, TTL already lowered, confirm it propagated
$ dig +nocmd shop.example.internal a +noall +answer
shop.example.internal. 30 IN A 10.20.30.40

# first weighted step, 10 percent to the VKS VIP via the GSLB pool ratio
# (Avi GSLB pool member ratio TKGI 9 : VKS 1)
# then watch which backend real users land on
$ kubectl –context dev-tkg-01 -n webapp get ingress shop -o jsonpath='{.status.loadBalancer.ingress[0].ip}’
10.80.12.50
# the failure that shows up here on a copied TKGI manifest
$ kubectl –context dev-tkg-01 -n webapp get ingress shop
NAME CLASS HOSTS ADDRESS PORTS AGE
shop <none> shop.example.internal 80 4m
# CLASS is none and ADDRESS is empty, AKO never claimed it
# cause: no ingressClassName, the old nsx annotation means nothing to AKO
$ kubectl –context dev-tkg-01 -n webapp patch ingress shop
–type merge -p ‘{"spec":{"ingressClassName":"avi-lb"}}’
ingress.networking.k8s.io/shop patched
Requests per second by backend, 40 minute cutoverWeight moved 10, 50, 100 percent, the two lines cross when the split is even12006000010m20m30m40mTKGI VIPVKS VIP
Figure 2. This was the real 40 minute shift on this estate. Total throughput held near 1200 requests per second the whole time, which is the signal that the drain worked, no dropped traffic at the crossover.

Verify, roll back, and the failures that bite

Verification during a cutover is not one check at the end, it is a reading at every weight step. At each step confirm three things: the request lands on the VKS VIP for the share you expect, the error rate stays inside budget, and p95 latency on the new path is at least as good as the old. On this estate the VKS VIP answered at 42 milliseconds p95 against 55 on the NSX-T VIP, and the error rate never left the 0.1 percent noise floor, which is what let me move from 50 to 100 without a pause.

# at each weight step, confirm the live record and hit the real path
$ dig +short shop.example.internal
10.80.12.50

$ for i in $(seq 1 20); do curl -sS -o /dev/null -w ‘%{http_code} %{time_total}n’ https://shop.example.internal/healthz; done | sort | uniq -c
20 200 0.041

# the check that says it is safe to retire TKGI, hits on the OLD VIP
# read from the NSX-T virtual server stats, must reach zero and stay there
old_vip_active_conns: 0

Rollback is a weight change, not a redeploy, and that is the point of moving traffic at the balancer rather than in DNS. If any gate fails, set the GSLB ratio back to all TKGI and the next request routes to the old VIP, because the source cluster never stopped serving. DNS does not need to change to roll back, which is exactly why you never leaned on it to move traffic in the first place.

# instant rollback, all weight back to the TKGI VIP
# (Avi GSLB pool member ratio TKGI 1 : VKS 0)
# next request lands on 10.20.30.40, no DNS change, no wait
$ dig +short shop.example.internal
10.20.30.40
$ curl -sS -o /dev/null -w ‘%{http_code}n’ https://shop.example.internal/healthz
–resolve shop.example.internal:443:10.20.30.40
200
Symptom Likely cause Fix
Ingress ADDRESS stays empty, CLASS shows none No ingressClassName, a copied NSX-T annotation AKO ignores Set ingressClassName avi-lb, confirm the ako pod logs a reconcile
dig still returns the old VIP long after your TTL A downstream resolver enforced a higher minimum TTL, cached record Lower TTL 48h ahead, keep both VIPs live until the old VIP shows zero hits
502 Bad Gateway from the new VIP Service selector or readiness probe wrong, Avi pool marked down Fix the probe path, confirm endpoints are populated behind the Service
TLS handshake fails on the new hostname The shop-tls secret was not migrated, or the cert SAN omits the host Create the tls secret on VKS, reissue with shop.example.internal in the SAN
Traffic on VKS but writes error out App config still points at the old database endpoint Repoint the config to the VKS database Service, covered in Part 21

Table 2. Cutover failures and their remediation. Rows one and two hit almost every hostname on the first pass, and row two is the one that turns a 40 minute window into a two hour one.

Field note and verdict

What I got wrong first: on an earlier hostname I lowered the TTL to 30 the morning of the window, saw dig report 30 from my own laptop, and started draining TKGI. About 18 percent of traffic kept arriving at the old NSX-T VIP for nearly an hour, long after the record should have expired, because a corporate resolver in one office enforced a 3600 second floor and served its cached answer to a few thousand users regardless of my TTL. I had already begun quiescing the TKGI side, so those users hit a backend I was draining. Nobody lost data, but the graphs were ugly and I spent 40 minutes proving the VKS side was fine while support fielded slowness reports. The fix was boring: on every cutover since, the TTL drops 48 hours ahead, and I hold both VIPs fully live until the old virtual server reports zero active connections.
My verdict: move traffic with load balancer weight, use DNS only to point at a stable front, and never treat a same-day TTL change as a control. Pick Avi with AKO when the hostname already did Layer 7 on NSX-T, because it is the least surprising match and gives you a WAF you were probably already running. Lower TTL 48 hours ahead, shift 10 then 50 then 100 with a read at each gate, and keep TKGI authoritative until the old VIP shows zero connections. The path to avoid is the one-shot A record swap with the old TTL still cached, which trades a clean weighted rollback for a slow one you do not control.

Cut over by draining, not by flipping

A clean result looks like this: AKO programmed an Avi VIP for the shop Ingress, the app answered on that VIP by host header before any user was involved, the DNS TTL sat at 30 seconds a full 48 hours before the window, traffic moved 10 then 50 then 100 percent with error rate inside budget and p95 latency better on the new path, and the old NSX-T virtual server drained to zero active connections while TKGI stayed authoritative the whole time. Hit that and the app is on VKS with a rollback you never needed but always had.

On your own estate on Monday, pick one low-traffic hostname, stand up its Service and Ingress on VKS with loadBalancerClass ako.vmware.com/avi-lb and ingressClassName avi-lb, and prove it green by host header with zero DNS change. Then lower that record TTL to 30 seconds and leave it for two days before you touch weight. For the load balancer options underneath this, the Supervisor Networking and load balancer choices part lays out FLB, NSX-LB and Avi in full. The complete migration map lives on the TKGI to VKS guide, and the related VKS, VCF 9 and NSX series sit on the guides hub. Next part moves secrets, config and the CI/CD pipeline, so the app that now answers on VKS is also built and deployed there.

Questions worth answering

Why not just change the A record to the new VIP in one go?
Because rollback then depends on DNS caches you do not own, which can hold the new record past your TTL. Moving weight at the load balancer takes effect on the next request and reverses just as fast, so DNS only points at a stable front you re-weight behind.

Do my TKGI Ingress manifests carry over to VKS?
Not unchanged. NSX-T annotations mean nothing to AKO, and there is no ingressClassName on the old object. Rewrite each Ingress with ingressClassName avi-lb and drop the old annotations, then re-add only the AKO annotations you actually need.

How far ahead should the TTL come down?
At least 48 hours, because the record is cached at the old value until that value expires, and some resolvers enforce a minimum above yours. Set it low, confirm propagation from a few external resolvers, and only then start the window.

When is it safe to retire the TKGI ingress?
When the old virtual server reports zero active connections and stays there across a full cache lifetime, not the moment DNS shows the new VIP. Stragglers behind slow resolvers keep arriving on the old VIP for a while, so watch the old side, not the new one.

This series covers a production migration. Run the cutover in a change window, lower TTL days ahead, shift traffic in reversible steps, and keep the TKGI side authoritative until the old VIP drains to zero.

TKGI to VKS 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