A Zero-Downtime AWS→Azure, ECS→AKS Migration
Re-platformed a fintech product across clouds and onto Kubernetes (100+ nodes, zero downtime), and wrote ~40% of its Go services.
This was two migrations at once: from AWS to Azure, and from ECS to Kubernetes, on a live fintech product that could not go dark. As the sole multi-cloud and Kubernetes engineer on the engagement I led the re-platform end to end, and as one of three application developers I also wrote a large share of the product running on top of it. The trick that made a cross-cloud move survivable was refusing to rebuild the platform in Azure's native shape, and instead standing it on portable primitives so the cloud underneath became something I could swap.
What we were re-platforming
LetsBloom was a security product for a global bank's fintech customers, built at CloudCover. It had grown up on AWS, running on Amazon ECS with the usual supporting cast, and the business needed it on Azure. That is the kind of ask that sounds like a lift-and-shift and turns out to be a re-architecture, because ECS has no equivalent on Azure: moving clouds meant moving orchestrators too.
Two migrations at once
Because there is no ECS on Azure, the honest target was Kubernetes: re-platform onto AKS and, in the same motion, translate a cloud's worth of ECS-shaped configuration into Kubernetes objects. Every ECS concept needed a home on the other side, and for each one I made a deliberate call: map it to an Azure-native service, or move it to a cloud-neutral tool that would look the same on both clouds.
Some pieces mapped to Azure-native services; others we deliberately moved to cloud-neutral tools. Tap a row.
The core rewrite. A task definition (container definitions, CPU/memory, environment, roles) becomes a Deployment with a Pod template. The task model gives way to pods managed by a ReplicaSet, with rolling updates handled by Kubernetes instead of ECS.
From a task definition to a Deployment
The unit of work is the clearest example. An ECS task definition and a Kubernetes Deployment describe the same idea, run N copies of these containers, but in different vocabularies. The rewrite is mechanical once you see the mapping: container definitions become a Pod template, desired count becomes replicas, task roles and awslogs give way to workload identity and the cluster's logging path.
# ECS task definition (before) # Kubernetes Deployment (after)
{ apiVersion: apps/v1
"family": "bloom-api", kind: Deployment
"cpu": "512", metadata:
"memory": "1024", name: bloom-api
"containerDefinitions": [{ spec:
"name": "api", replicas: 3 # was the ECS service desired count
"image": template:
"<acct>.dkr.ecr.../bloom-api", spec:
"portMappings": [ containers:
{ "containerPort": 8080 } - name: api
] image: <registry>.azurecr.io/bloom-api
}], ports:
"taskRoleArn": "arn:aws:iam::..." - containerPort: 8080
} # taskRoleArn -> workload identity + VaultBetting on portable primitives
The reason a two-front migration did not turn into chaos is that the platform never really depended on AWS in the first place. Service discovery ran on HashiCorp Consul, secrets on HashiCorp Vault, network policy on Calico, and every bit of infrastructure was defined in Terraform. Those are the same on ECS and on AKS, so the application never learned its cloud had changed, and the migration became a swap of the substrate under a stable set of primitives.
Four cloud-neutral primitives carried the platform, so the cloud underneath became replaceable. Tap one.
Services registered with and resolved each other through Consul, not the cloud's own discovery. So the service graph looked identical on ECS and on AKS, and no application had to relearn where its dependencies lived.
Cutting over with zero downtime
A fintech product does not get a maintenance window, so the cutover could never be a single flip. I built the Azure side in parallel with Terraform, replicated state onto it, and validated it under real conditions behind a private endpoint while ECS kept serving. Only then did traffic move, by weight, using DNS and Traffic Manager, with the AWS side held warm the entire time as an instant fail-back. The migration was only called done once there was nothing left to fail back to.
Both clouds run at once, traffic moves by weight, and rollback stays one control away. Step through it.
Terraform stands up the entire Azure side, AKS, ACR, ingress, Consul, Vault, and Calico, while ECS keeps serving every request. Nothing customer-facing has moved yet.
The application layer I built on top
I was not only re-platforming it; I was also one of three developers building the product. I wrote roughly 40% of LetsBloom's Go microservices, and two of the pieces I owned were substantial enough to stand on their own.
- A Casbin-based authentication and authorization framework that centralized access control across the services.
- Secure Notes, a UI over HashiCorp Vault and its secret engines, so the same Vault the platform trusted also backed a customer-facing feature.
- The DevSecOps practice and Kubernetes standards the team carried forward, which is what earned two promotions in under two years.
The task engine behind the workflows
The other piece I owned was orchestration. Bloom's longer-running work, the multi-step jobs that fan out and depend on each other, ran on a distributed task worker I built on Celery, modeling each job as a dependency graph rather than a flat queue. It is the same shape of problem I would later solve again for the landing-zone provisioner.
The award, and the culture behind it
CloudCover ran on a simple loop: experiment, learn, know. This migration is the one they handed me the experiment > learn > know spot award for, and I keep the card around because it captures how the place worked. You were rewarded for trying the harder, more correct thing, in this case moving a live product across clouds without anyone noticing, rather than for playing it safe.
