Nuno
Nuno

Moving a Legacy Adobe AEM Site to Kubernetes

About twenty end-of-life VMs, single points of failure and a CDN that cached nothing. What moving AEM onto one AKS cluster took, and what I'd change.
Vander Nunes
March 26, 2026
XInstagramYouTubeLinkedIn
A row of separate virtual machines folding into one cluster across three zones

The website ran on about twenty virtual machines, every one of them on an operating system past end of life. Each environment had five or six of them: author, publish, dispatcher, a redirect server. Production was a set of single machines on local disks. And the CDN in front of it all was fetching every request from the origin, because caching was switched off.

In October and November 2025 I moved it to one AKS cluster. Here's what that involved, and what I learned about putting a stateful, opinionated CMS like Adobe Experience Manager into containers.

The refresh that looked cheaper

The first option on the table was the obvious one: rebuild the same VMs on a supported OS and carry on. When I estimated both paths, the effort for a like-for-like refresh and for a move to Kubernetes came out close.

That settled it. A refresh would have fixed the OS and nothing else. Every production role would still be one machine on one disk. Deploys would still be manual runs. For roughly the same effort, the Kubernetes move fixed the reason the platform was fragile, not just the date on its OS.

AEM is stateful, so treat it that way

AEM is not a stateless web app. The author and publish instances keep their repository on disk, and they expect it to be there after a restart.

So they ran as StatefulSets, each with its own Azure managed disk as a persistent volume. One author. Two publishers, so one can restart without taking publishing down. Dispatchers in front, with a 50 GiB cache volume each. The dispatcher is AEM's Apache-based caching layer, and it behaves well in containers because its cache can be rebuilt.

Container images for author, publish and dispatcher were built from scratch, with Helm charts per tier. Terraform defined the cluster, the network and the container registry. Traffic came in through Azure Application Gateway, with cert-manager for certificates.

One cluster, three zones, one spare

Instead of a set of VMs per environment, one cluster served all of them through namespaces, with separate node pools:

  • a small system pool for cluster services;
  • a production pool spread over three availability zones;
  • a non-production pool for the lower environments.

The production pool kept one node more than the workload needed. If a node or a zone had trouble, the pods had somewhere to go without anyone being paged. That spare node costs money every month. I'd make the same call again: the alternative was the old setup, where losing one VM meant losing that role.

Deploys stopped being a sequence of manual steps

Deploys had been manual Jenkins and Ansible runs. They moved to a pipeline with an atomic production step: the author and both publishers are updated as one release, so you never end up with publishers on different versions serving different content.

The CDN that cached nothing

This was the cheapest fix in the project. The CDN had been in front of the site the whole time, but with caching off it was a pass-through. Every visitor request went to the origin VMs.

When the CDN was pointed at the new origin, caching was switched on. If you have a CDN in front of anything, check its hit ratio today. A CDN with a hit ratio near zero is a very expensive proxy.

Cleaning up

After the cutover, the old QA and UAT VMs and the non-production cluster nodes sat around. In March 2026 I powered them off for a week. Nothing broke, so I deleted all 14. The estimated cluster cost dropped from about $1,796 to about $1,376 a month.

"Power off for a week, then delete" is my default for anything I'm not sure is unused. It's slower than deleting straight away, but the week of silence is the evidence.

Node counts confuse people

Some months later the cluster's node count was questioned: why did a cluster that had shown 3 nodes now show 8? It hadn't grown. It had always run 8: 2 system nodes, 5 for AEM in production and 1 spare. The lower number had been seen during a managed upgrade, when Azure rotates nodes out and in.

The lesson for me: write the expected node count, and the reason for each node, into the handover docs. People look at the portal, not at your design.

How it ended

The platform carried production from the November cutover through the 2025 holiday peak. In early 2026 the site moved off AEM entirely and the cluster was wound down. That's normal. Platforms exist to serve the business, and the business changed direction. The work bought a supported, zone-spread platform for the busiest season of the year instead of end-of-life VMs.

What I'd do differently

Save a before measurement of page speed. Everyone agreed the site got faster, but I didn't keep a saved report from before the move. Without one you end up with several different "before" numbers in people's memories. Run a page test and export it before you touch anything.

Put the CDN fix first. Switching caching on didn't need the migration. Doing it on day one would have made the old site faster while the new one was being built.

Size the spare capacity in writing. See the node count story above.

A short checklist for moving a legacy CMS to Kubernetes

  1. Find out which parts hold state on disk, and give each one a persistent volume.
  2. Run at least two of anything that serves traffic.
  3. Spread production across zones, and keep spare capacity on purpose.
  4. Make releases update all instances of a tier together.
  5. Check the CDN hit ratio before and after.
  6. Save performance reports before the move.

If you're sitting on a legacy CMS on ageing VMs, I can help you plan the way off them. See Kubernetes & Platform.