, ,

Migrating a Stateless Application End to End on VKS (TKGI to VKS Series, Part 18)

Move a stateless service from TKGI to VKS with Velero, then fix the two things that break on VCF 9, Pod Security admission and image pull secrets, before you touch DNS.

TKGI to VKS Series · Part 18 of 26

The first stateless service I moved to VKS restored in 22 seconds and then sat there with zero pods running. Its Deployment was healthy, the ReplicaSet existed, and the events log read forbidden: violates PodSecurity restricted:latest three times, once per replica. Nothing was wrong with the app. Everything was wrong with my assumption that stateless means it just moves.

TL;DR · Key Takeaways

  • A stateless move is object-only, no volume data, so the backup and restore are fast. What is slow is clearing the two things VKS enforces that TKGI never did.
  • Headline command, run against the target: velero restore create storefront –from-backup storefront-stateless –kubeconfig $HOME/.kube/dev-tkg-01.conf
  • VKS enforces Pod Security Admission from cluster version 1.26. Pods that ran happily on TKGI get rejected at admission on the target with a restricted:latest violation.
  • Image pull secrets that pointed at the old Harbor break the moment the pod is scheduled on a node that cannot authenticate. Recreate the secret on the target, do not restore it blind.
  • Keep the source serving traffic. A stateless restore is additive, the TKGI service stays live until the DNS cutover in Part 20, so real downtime here is close to zero.
  • Tested against VCF 9.0, VKS on vSphere Supervisor (cluster 1.31), Velero 1.17, velero-plugin-for-aws v1.11, kubectl v1.32, source TKGI 1.18 (Kubernetes 1.27).
Who this is for: platform engineers, Kubernetes operators and SREs who proved the Velero pipe last part and now want a real workload on the target. You have kubectl access to a source TKGI cluster, the kubectl vSphere plugin and login rights to the VKS cluster, Velero installed on both against one shared bucket, and a stateless service you are willing to run in two places for a while.

Where the migration stands

Last part we installed Velero on the source TKGI cluster and on the target VKS cluster called dev-tkg-01, pointed both at one S3 bucket, and proved a throwaway namespace could back up on TKGI and restore on VKS. This part carries the first real workload across that pipe, a stateless one, on purpose, because a stateless service has no persistent volume to move and so it isolates every problem that is about the platform rather than the data.

Here is the running example as it stands. The source is a TKGI 1.18 estate on NSX-T with three clusters, dev, staging and prod, a Harbor registry at harbor.platform.local, and one stateful app plus two stateless services. The stateful app, a web and api tier in front of PostgreSQL with a 500 GB volume, waits for the next part. Today moves storefront, a stateless catalog frontend: a Deployment named catalog-web at 3 replicas, a ConfigMap of runtime settings, an image pull Secret for Harbor, and a Service of type LoadBalancer. No database, no PVC, no local disk state. If storefront runs on VKS, the platform is ready for the harder move.

One reframe before any command. People treat a stateless migration as the easy warm-up and reach for backup then restore then done. On VKS that skips the two admission gates the source never had, Pod Security and a pull secret bound to the old registry, and both of them fail after the restore reports success, which is the worst time to find them. So the plan is restore, then clear admission, then verify, then leave the source running.

Preflight on source and target

Three checks earn the restore. You can reach the source with kubectl and log in to the target VKS cluster with the vSphere plugin. The storefront namespace on the source holds only what you think it holds, no stray PVC hiding under a label. And the target can pull the storefront image from wherever it lives, because a stateless migration still needs the container image, and that image sits in Harbor on the estate you are leaving. Prove all three now.

# Tested against VCF 9.0, VKS cluster 1.31, Velero 1.17, kubectl v1.32, source TKGI 1.18
# log in to the target VKS cluster through the Supervisor
kubectl vsphere login –server=192.0.2.10
–tanzu-kubernetes-cluster-name dev-tkg-01
–tanzu-kubernetes-cluster-namespace tenant-dev
–vsphere-username platform-admin@vsphere.local

# source, confirm storefront is genuinely stateless, expect no rows
kubectl –context tkgi-prod -n storefront get pvc

# source, list what actually moves
kubectl –context tkgi-prod -n storefront get deas,cm,secret,svc

# target, confirm the namespace does not already exist
kubectl –context dev-tkg-01 get ns storefront

What green looks like: the login prints a context you can select, the PVC query returns No resources found, the object list shows one Deployment, one ConfigMap, the pull Secret and the Service, and the target reports the storefront namespace is not found yet. That last line matters, because Velero by default will not overwrite existing resources, so a namespace already present on the target quietly changes what the restore does.

$ kubectl –context tkgi-prod -n storefront get pvc
No resources found in storefront namespace.

$ kubectl –context tkgi-prod -n storefront get deploy,cm,secret,svc
NAME READY UP-TO-DATE AVAILABLE AGE
deployment.apps/catalog-web 3/3 3 3 88d
NAME DATA AGE
configmap/catalog-conf 4 88d
NAME TYPE DATA AGE
secret/harbor-pull kubernetes.io/dockerconfigjson 1 88d
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
service/catalog-web LoadBalancer 10.100.14.22 203.0.113.40 80:31640/TCP

Note the Service is LoadBalancer with an external IP of 203.0.113.40 handed out by the NSX-T load balancer on the source. That IP belongs to the old estate. On the target, Avi (NSX Advanced Load Balancer) will assign a fresh one, and DNS still points at the old address until Part 20, which is exactly why this move causes no outage.

Back up the stateless namespace on TKGI

Back up the whole storefront namespace from the source. No –snapshot-move-data here, that flag drives volume data movement and there is no volume, so leaving it off keeps the backup to Kubernetes objects only and finishes in seconds. Scope it tight with –include-namespaces so the backup carries storefront and nothing else.

$ velero backup create storefront-stateless
–include-namespaces storefront
–kubeconfig $HOME/.kube/tkgi-prod.conf
Backup request "storefront-stateless" submitted successfully.

$ velero backup describe storefront-stateless –kubeconfig $HOME/.kube/tkgi-prod.conf
Name: storefront-stateless
Phase: Completed
Total items to be backed up: 14
Items backed up: 14
Backup Volumes: <none included>

What green looks like: Phase Completed, items backed up equal to items to be backed up, and Backup Volumes reading none, which confirms this really was an object-only capture. That backup took 18 seconds against this namespace. Within a minute the target Velero, which shares the bucket, sees the backup without anything running on the target, the same cross-cluster visibility we proved last part.

Restore onto the VKS cluster

Restore into the same namespace name on the target. Velero recreates the namespace, the Deployment, the ConfigMap, the Secret and the Service. It does not blindly copy the Service network identity: by default Velero strips the assigned clusterIP and lets the target allocate a new one, and it drops the auto-assigned nodePort so the target picks its own. That default is correct for a cross-cluster move and you want it, because the old clusterIP and nodePort belong to the source cluster address space.

$ velero restore create storefront
–from-backup storefront-stateless
–kubeconfig $HOME/.kube/dev-tkg-01.conf
Restore request "storefront" submitted successfully.

$ velero restore describe storefront –kubeconfig $HOME/.kube/dev-tkg-01.conf
Phase: PartiallyFailed
Warnings: 1
Errors: 0
Started: 2026-08-01 07:12:04
Completed: 2026-08-01 07:12:26

Twenty two seconds, and PartiallyFailed rather than Completed. The restore created every object, so the warning is not about Velero, it is about what happens next when the target admission controllers get a look at the pods. This is where a stateless migration on VKS stops being trivial.

flowchart LR
  A[storefront on TKGI] --> B[velero backup, objects only]
  B --> C[(S3 bucket)]
  C --> D[velero restore on VKS]
  D --> E{Pods admitted}
  E -->|rejected| F[Pod Security restricted]
  E -->|imagepullbackoff| G[Harbor pull secret]
  F --> H[label namespace baseline]
  G --> I[recreate pull secret]
  H --> J[catalog-web Ready on VKS]
  I --> J
Figure 1. A stateless restore is quick. Real work is the two admission gates on the right, Pod Security and the registry pull secret, both of which fail after Velero reports the objects restored.

Pod Security and image pull failures

Look at the pods and there are none, even though the Deployment says it wants three. The ReplicaSet is trying and failing, and its events carry the real reason. Pod Security Admission, the built-in Kubernetes controller that enforces the Pod Security Standards, runs on VKS clusters and enforces the restricted level from cluster version 1.26. TKGI never turned it on, so catalog-web shipped without runAsNonRoot, without a seccompProfile, and without dropping Linux capabilities, and on the source that was fine. On the target it is a hard rejection.

$ kubectl –context dev-tkg-01 -n storefront get pods
No resources found in storefront namespace.

$ kubectl –context dev-tkg-01 -n storefront describe rs catalog-web | tail -4
Warning FailedCreate rs/catalog-web Error creating: pods "catalog-web-6c8f-" is
forbidden: violates PodSecurity "restricted:latest": allowPrivilegeEscalation != false,
unrestricted capabilities, runAsNonRoot != true, seccompProfile type unset

# bridge fix, relax enforcement on this namespace to baseline
$ kubectl –context dev-tkg-01 label ns storefront
pod-security.kubernetes.io/enforce=baseline –overwrite
namespace/storefront labeled

Within a few seconds the ReplicaSet controller retries and the pods schedule. That label is a bridge, not the destination. Baseline lets the workload run while you keep the security posture that made VKS worth moving to, and the real fix is to add a compliant securityContext to catalog-web so it passes at restricted. Do that in the manifest, in your Git repo, not by hand on the cluster, because the next part in the wave will need the same treatment and you want it codified.

Contradicts the tutorial default: the fast path everyone posts is to label the namespace pod-security.kubernetes.io/enforce=privileged and move on. Do not. Privileged switches admission off entirely and throws away one of the concrete reasons to leave TKGI. Use baseline as a temporary bridge, then fix the securityContext so the namespace can go back to restricted. On this estate that was runAsNonRoot true, a seccompProfile of RuntimeDefault, and dropping ALL capabilities, three lines of YAML that turned a rejected pod into a compliant one.

Pods schedule and then a second failure lands, ImagePullBackOff. The restored harbor-pull Secret carried a token that the source cluster minted, and the target cannot use it as is, or the Deployment references the image at a Harbor path the target service account cannot authenticate to. A restored pull Secret is the classic thing you should recreate rather than trust. Delete the restored one and create a fresh dockerconfigjson Secret on the target with credentials read from the environment, never inline.

Object What Velero does on restore What you do about it
Namespace Recreated without the Pod Security labels VKS wants Label enforce=baseline as a bridge, then fix securityContext for restricted
Service clusterIP Stripped, target allocates a new one Nothing, this is correct for a cross-cluster move
Service nodePort (auto) Dropped, target reassigns Nothing, unless a client hardcodes the port, then –preserve-nodeports
Service LoadBalancer IP Requests a new one, Avi assigns from its pool Record the new IP, DNS cutover waits for Part 20
Image pull Secret Restored as is, token may be stale or unusable Delete and recreate on the target from environment credentials

Table 1. What changes when a stateless workload lands on VKS. This is the reference artifact, pin it, because every stateless service in the wave gets processed row by row.

Verify the service serves traffic

Once admission clears and the pull secret is fixed, the three replicas go Ready and the Service picks up an external IP from Avi. Verify the workload from inside the cluster first, before anyone outside knows it exists, because DNS still points at the source. Curl the new external IP directly and confirm the app answers.

$ kubectl –context dev-tkg-01 -n storefront get pods,svc
NAME READY STATUS RESTARTS AGE
pod/catalog-web-7d94c6b8f-2xk4p 1/1 Running 0 51s
pod/catalog-web-7d94c6b8f-9wq7n 1/1 Running 0 51s
pod/catalog-web-7d94c6b8f-hs5cd 1/1 Running 0 51s
NAME TYPE EXTERNAL-IP PORT(S)
service/catalog-web LoadBalancer 198.51.100.61 80:32115/TCP

$ curl -s -o /dev/null -w "%{http_code}" http://198.51.100.61/healthz
200

What green looks like: three pods Running and Ready, a new external IP of 198.51.100.61 from Avi that is not the old 203.0.113.40, and a 200 from the health endpoint. Time from restore to first healthy pod on this run, once the two fixes were in place, was 47 seconds. Meanwhile the source storefront on TKGI never stopped serving during any of this, so users saw nothing.

Seconds per phase, clean storefront migrationObject-only move, no volume data, source stayed live throughout0153045Backup18sRestore22sPods scheduled9sPods Ready47sAvi LB IP38s
Figure 2. Where the time actually goes. Backup and restore are quick, and the long pole is pods reaching Ready, which only started counting after the Pod Security and pull secret fixes cleared admission.

Rollback and fallback

Rollback here is the easiest in the whole series, because you never touched the source. Your TKGI storefront kept running and serving its old IP the entire time, so backing out means deleting what you created on the target and walking away. Nothing about the source cluster, the DNS records or the users changed.

# back the target out, source is untouched and still live
$ velero restore delete storefront –kubeconfig $HOME/.kube/dev-tkg-01.conf
$ kubectl –context dev-tkg-01 delete namespace storefront
namespace "storefront" deleted

Because the cutover has not happened, this is a true fallback with zero user impact, and it stays that way right up until Part 20 moves DNS. Until then you can restore, break, delete and restore again as many times as you like. Treat that freedom as a gift and use it to get the manifest right.

Error you see Likely cause Fix
violates PodSecurity restricted:latest, zero pods VKS enforces PSA restricted, TKGI did not Label enforce=baseline, then fix securityContext to pass restricted
ImagePullBackOff on the restored pods Restored pull Secret token stale or wrong registry auth Delete and recreate the dockerconfigjson Secret from env credentials
Restore skipped, resources already exist Namespace pre-created on the target before restore Delete the namespace and re-run, or set an existing-resource policy
Service stuck pending, no external IP Avi service engine or IP pool not wired on the namespace Check the load balancer config from Part 13 before blaming Velero
Old clients hit a dead nodePort after restore Auto nodePort changed, a client pinned the source port Re-run restore with –preserve-nodeports if the port must survive

Table 2. Stateless restore failures and their remediation. Rows one and two hit almost every estate on the first pass.

Field note and verdict

What I got wrong first: I picked storefront as the pilot precisely because it was stateless, told the team it would be a five minute move, and watched the restore go PartiallyFailed with zero pods. I spent about 35 minutes reading Velero logs looking for a restore bug that was not there, because the objects had all restored fine. It was Pod Security, an admission gate the source never had, rejecting three healthy replicas. Then, once I relaxed that, ImagePullBackOff, because I trusted the restored Harbor secret. Two admission problems, neither of them Velero, on the workload I had called the easy one.
My verdict: move your smallest stateless service first, and budget the time for admission, not for Velero. Back up objects only, restore into a fresh namespace, then expect two failures in order, Pod Security and the pull secret, and fix them in that order. Reach for enforce=baseline as a bridge and avoid enforce=privileged entirely. Recreate pull secrets on the target rather than trusting the restored copy. Leave the source running until Part 20, because a stateless move that keeps both sides live is a rehearsal you can repeat at no cost.

Move the smallest stateless service first

A clean result looks like this: storefront backed up on tkgi-prod as an object-only backup, restored onto dev-tkg-01, the namespace labelled so Pod Security admits the pods, a freshly created Harbor pull secret, three catalog-web replicas Running and Ready, a new Avi external IP recorded, a 200 from the health check, and the source storefront still live on its old IP with DNS untouched. Hit that and you have proven the platform carries a real workload, not just a throwaway namespace.

On your own estate on Monday, pick the least important stateless service you run, back it up with –include-namespaces and no snapshot flag, and restore it onto your first VKS cluster into a fresh namespace. Do not touch DNS. When the restore goes PartiallyFailed, you will already know to check Pod Security first and the pull secret second. For the registry side of that pull secret, the Identity, RBAC and Harbor on VKS part sets up the target registry properly. The full migration map lives on the TKGI to VKS guide, and the related VKS, VCF 9 and NSX series sit on the guides hub if you need the component mechanics. Next part moves the hard one, a stateful application with a 500 GB persistent volume.

Questions worth answering

Why did the restore say PartiallyFailed if every object restored?
Velero restored the Kubernetes objects successfully, but the target admission controllers rejected the pods the Deployment tried to create afterward. That rejection surfaces as a warning on the restore, not as a Velero error, which is why the log hunt is a dead end.

Should I keep the namespace at baseline for good?
No. Baseline is a bridge so the workload runs while you fix the manifest. Add a compliant securityContext, then move the namespace back to enforce=restricted so it matches the posture the rest of the VKS estate holds.

Do I have to recreate every restored Secret?
Not every Secret, but every credential Secret that authenticates to something outside the cluster, pull secrets and registry tokens especially. Config-only Secrets restore fine. Anything holding a token minted by the source is suspect on the target.

Can users reach the app on the new IP already?
They can if you point them at the Avi IP directly, but do not. DNS still resolves to the source, and cutting traffic over is a deliberate step in Part 20 with its own validation and rollback. Verify internally now, cut over later.

This series covers a production migration. Run the move in a change window against your own environment, verify on the new IP internally, and leave the source serving real traffic until the cutover part.

TKGI to VKS Series · Part 18 of 26
« Previous: Part 17  |  Guide  |  Next: Part 19 »

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