The default migration wave plan looks like this:
Wave 1: app-a
Wave 2: app-b
Wave 3: app-c
...
Small, schedulable, individually reversible. Everyone approves it.
Now suppose app-a and app-c exchange 40k calls an hour. After wave 1, every one of those calls crosses a paid network boundary. It keeps doing that until wave 3, which — because app-c has an unresolved vendor contract — slips to month fourteen.
You have optimised for per-wave risk, which is visible, and pessimised egress cost, which shows up on a bill three months later.
Build clusters from telemetry, not interviews
Do not ask people what talks to what. They will describe systems they have not thought about in years, and they will be wrong in the specific way that matters — omitting the periodic jobs.
Collect it empirically. VPC/NSG flow logs, eBPF tracing, database query logs, and load balancer logs get you most of the way:
# Rough shape: aggregate flow logs into a weighted edge list
# src_service dst_service bytes conn_count
aws ec2 describe-flow-logs ... # or equivalent
# → edges.csv
Then cluster the graph. Edge weight should reflect what you actually pay for and care about — bytes transferred and connection count, not just whether an edge exists:
import networkx as nx
from networkx.algorithms import community
G = nx.Graph()
for src, dst, bytes_, conns in edges:
# normalise so a chatty-but-small link still scores
G.add_edge(src, dst, weight=0.7 * norm(bytes_) + 0.3 * norm(conns))
clusters = community.louvain_communities(G, weight="weight", seed=42)
for i, c in enumerate(clusters):
print(f"wave {i}: {sorted(c)}")
Louvain is fine here. You are not looking for a mathematically optimal partition — you are looking for groupings that keep heavy edges inside a wave rather than across it.
Run collection across a full quarter-end cycle. Observational discovery only sees what executes during the window. The batch job that runs on the last business day of the quarter is exactly the one that takes down reporting at 3am eleven weeks into the programme.
Reading legacy code is now cheap
The remaining archaeology — stored procedures, cron definitions, shell scripts nobody has opened since 2014 — is where migration teams historically lost a quarter.
LLMs are genuinely good at this specific task: "read this stored procedure and list every table, file path, and external endpoint it touches." High-volume, pattern-heavy, and verifiable, which is the shape the technology suits.
One rule: treat the output as leads to verify, not conclusions. Cross-check every claimed dependency against your flow logs. Model explanations of legacy code are confidently wrong often enough that one unchecked summary can put a false assumption straight into your cutover plan.
Decide data placement before anything moves
Compute is portable. Petabytes are not.
Once a large dataset lands in a provider and region, everything that reads it is pinned there by egress economics and latency, whatever your architecture docs claim about portability.
This bites harder with AI workloads because they read large volumes repeatedly — embedding pipelines do full-corpus passes, retrieval systems read continuously. Corpus in provider A, model endpoint in provider B means paying egress on every pass, on a line that grows with adoption.
Identify your gravity wells first — usually the transactional store, the document corpus, and the event stream — pick their home, and place compute around them. Migrating apps first and sorting out data later is how you end up with one logical system split across two providers and a second migration to fix it.
Cutover patterns that do not need a weekend
Big-bang persists because it is easy to plan, not because it works. It concentrates all risk into a window with no meaningful rollback, staffed by people who have been awake fourteen hours, and the decision to abort gets made at 4am by someone with incomplete information and a strong incentive to push on.
Three alternatives, roughly in order of effort:
Strangler-fig routing. Move traffic per endpoint. Each step is small and individually reversible.
route /api/orders/* → new
route /api/inventory/* → legacy
Dual-write with reconciliation. Both systems run in parallel; a job compares them continuously and reports divergence. You get consistency evidence from real traffic before committing.
Shadow traffic. Send production requests to the new system, discard its responses, compare asynchronously. Surfaces behavioural differences with zero customer exposure.
Yes, all three require throwaway scaffolding. Weigh that against the expected cost of a failed cutover on a system that matters — usually an order of magnitude larger.
Whichever you pick, define rollback triggers numerically and in advance:
rollback_if:
error_rate_5xx: "> 0.5% over 5m"
p99_latency: "> 800ms over 5m"
reconciliation_drift: "> 0.01% of records"
decision_owner: named-individual # authority to call it alone
Rollback criteria negotiated during an incident are rollback criteria that never fire.
Landing zone: set defaults before wave one
Remediating a bad landing zone means touching every workload that already landed. Get these in before the first wave:
- Encryption on by default; public exposure denied unless explicitly granted.
- Tagging enforced at creation, so cost allocation works from day one.
- Centralised logging a workload cannot opt out of.
- Policy-as-code so controls are preventive, not detective. Detective controls in a fast migration just generate a backlog nobody clears.
- AI-specific, added now rather than later: model endpoints restricted to approved providers and regions, outbound allowlists for anything running agent tooling, and classification metadata carried with datasets into the new environment.
Frequently Asked Questions
Why not just migrate app by app?
Because it maximises the time tightly coupled systems spend separated by a paid network boundary, and hybrid periods reliably run longer than planned. Cluster migration shortens that exposure significantly.
How long should I collect flow logs before clustering?
At least one full quarter-end cycle. Shorter windows miss periodic jobs, which are disproportionately the cause of cutover failures.
Is Louvain the right clustering algorithm?
It is a reasonable default. You are not after an optimal partition — you want heavy edges inside waves rather than across them. Weight edges by bytes and connection count.
Can I trust LLM analysis of legacy code?
As leads to verify, yes; as conclusions, no. Always cross-check claimed dependencies against observed telemetry before they enter a cutover plan.
When is big-bang cutover acceptable?
When a few hours of downtime is genuinely acceptable for that system — and say plainly which systems those are rather than assuming.
What AI guardrails belong in the landing zone?
Approved model providers and regions, outbound network allowlists for agent tooling, and data classification metadata travelling with datasets. All cheap now, awkward to retrofit across a populated estate.
Full article, including the seven Rs re-scored and post-migration FinOps: Cloud Migration Best Practices for AI-Era Workloads.
We help teams sequence migrations and design landing zones — cloud application development and custom software development.


Top comments (0)