Cross-cloud moves, AWS to Azure, are driven by cost optimization, an enterprise Microsoft agreement, M&A consolidation, or compliance, rarely by the IaaS itself. Base compute rates are similar across hyperscalers, so savings usually come from rightsizing, committed-use discounts, and avoiding egress, not from the move. Be crystal clear on your motive first, it shapes the entire plan. And know going in: lifting VMs is easy; untangling managed services is the real work.
The managed-service layer is where the cost and lock-in live
EC2 → Azure VMs is mechanical. The difficulty, and the lock-in, is in the managed services:
- RDS/Aurora → Azure SQL / Azure Database for PostgreSQL/MySQL (via DMS).
- S3 → Azure Blob Storage; DynamoDB → Cosmos DB (data-model rework).
- Lambda → Azure Functions; SQS/SNS → Service Bus / Event Grid.
- IAM → Entra ID + Azure RBAC (a different model, not a translation).
- CloudFormation → Bicep/ARM, or keep Terraform and re-target the Azure provider.
Map each service before you move, this layer is where re-architecture, testing, and surprises concentrate.
Build the mapping table before you touch a VM
Build the mapping table before you touch a single VM, because it is what tells you the true size of the project. For each AWS service in use, write down the Azure target, whether the move is mechanical or a re-architecture, and who owns the validation. RDS and Aurora go to Azure SQL or Azure Database for PostgreSQL/MySQL through DMS, and if you are already on PostgreSQL or MySQL the engine match keeps the move honest. S3 to Blob Storage is close in shape but the storage tiers and lifecycle rules differ, so re-express your lifecycle policies rather than assuming they carry over. DynamoDB to Cosmos DB is the one that bites: the partition-key and consistency models differ enough that you should treat it as a data-model exercise, not a copy. Anything you cannot map cleanly is a candidate to leave on AWS or to refactor, and it is far cheaper to discover that on a spreadsheet than mid-cutover.
Lead with the landing zone
Build the Azure landing zone first: subscriptions/management groups, networking (VNet, peering, firewalls), identity (Entra ID), and guardrails/policy. Establish connectivity (VPN or ExpressRoute) back to AWS for replication. Inventory AWS resources tagged by application so you migrate coherent app groups, not orphaned pieces.
Use the native migration services
- Servers: Azure Migrate (agent-based replication of EC2 instances to Azure VMs).
- Databases: Azure Database Migration Service (full load + CDC to keep the target current).
- Storage: AzCopy to sync S3 → Blob (mind egress, see below).
Re-platform infrastructure as code rather than clicking it twice; a clean IaC baseline on Azure is worth the effort.
Recreate the environment as code, not in the portal
The temptation on a cross-cloud move is to hand-build the target in the portal to “just get it working,” and it is a trap. Recreate the environment as code from the start, either by keeping Terraform and swapping in the Azure provider or by adopting Bicep/ARM, and you get three things that matter: a reviewable diff of what production will look like, the ability to stand up an identical staging copy for pilot testing, and a repeatable path so the second and third app groups cost far less than the first. Bind resources to your Entra ID and RBAC model in the same code so identity is not an afterthought bolted on later. Treat the IaC baseline as a deliverable in its own right, not a byproduct of the migration.
Watch the egress, it can dominate the bill
Moving large data sets out of AWS incurs egress fees that can be substantial, and they’re a one-time cost that surprises teams mid-project. Factor egress into both the migration plan and the business case. Then validate that Azure’s committed-use/reserved pricing actually beats your AWS spend for the steady-state workloads, if it doesn’t, the move may not pay for itself.
Pay the egress once, not on every pass
Sequence the data movement so egress is paid once, not repeatedly. Run the bulk S3-to-Blob sync with AzCopy early, while the source is still live, then reconcile with a small delta pass during the freeze window rather than recopying the whole store. Prune obsolete objects and expired snapshots before the first sync, since you are billed to move every byte whether you still need it or not. Remember that egress is a migration cost, not a steady-state one: it does not appear in the ongoing per-vCPU comparison, so keep it as a separate line in the business case. If Azure’s committed-use pricing only narrowly beats your AWS spend, a large egress bill can be the difference between a move that pays for itself and one that does not, which is precisely the kind of figure the calculator above is meant to surface.
Cutover and validation
- Replicate a pilot workload + its database end to end and validate before scaling.
- For each app group: final delta sync, freeze source writes, switch DNS/traffic (Azure Traffic Manager / Front Door), validate apps, integrations, and cost.
- Run a hypercare window with AWS as fallback; keep replication tasks until cutover is signed off.
The acceptance bar is functional + integration parity, performance at or above baseline, and validated cost, not just “the VMs booted.”
Where the real work lands
AWS → Azure is straightforward for VMs and truly hard for managed services, that layer drives the re-architecture, the lock-in, and the egress bill. Lead with the landing zone, use Azure Migrate + DMS, model egress and steady-state cost honestly, and cut over app group by app group with AWS as fallback. Model your per-vCPU comparison in the calculator above, and treat the figures as illustrative until your pilot proves both function and cost.