{
  "version": 1,
  "id": "multicloud-migration-patterns",
  "title": "Migration Patterns",
  "summary": "Rehost, replatform and refactor, and why the strangler fig beats a big-bang migration.",
  "estimatedMinutes": 15,
  "prerequisites": [],
  "source": "courses-notebook CLD302_multi_cloud_architecture 07_migration_patterns",
  "quiz": [
    {
      "prompt": "Which migration strategy means containerizing an app and swapping the DB without changing the app's design?",
      "options": [
        "Rehost (lift-and-shift)",
        "Replatform",
        "Rearchitect (microservices rewrite)",
        "Retire"
      ],
      "answer": 1
    },
    {
      "prompt": "The Strangler Fig pattern is preferred over Big Bang migration because:",
      "options": [
        "It is the fastest possible approach",
        "It incrementally replaces functionality while running old and new in parallel",
        "It requires no testing",
        "It works only for stateless apps"
      ],
      "answer": 1
    }
  ],
  "lesson": "## 7.1  The 7 Rs of Cloud Migration\n\n| Strategy | Also called | Effort | Risk | When to use |\n|----------|------------|--------|------|-------------|\n| **Retire** | Remove | None | None | Application is no longer needed |\n| **Retain** | Revisit | None | None | Too risky to migrate now; revisit in 6-12 months |\n| **Rehost** | Lift-and-shift | Low | Low | Speed is priority; optimise later |\n| **Relocate** | Hypervisor lift | Low | Low | VMware-to-VMware Cloud migration |\n| **Replatform** | Lift-and-tinker | Medium | Medium | Swap DB engine, containerise, minimal code changes |\n| **Repurchase** | Drop-and-shop | Medium | Medium | Replace with SaaS (e.g. Salesforce, Workday) |\n| **Rearchitect** | Refactor | High | High | Modernise to microservices, serverless, cloud-native |\n\n## 7.2  Migration Execution Patterns\n\n### Strangler Fig\nGradually replace a monolith by routing specific features/endpoints to new services:\n```\n                    \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\nClient \u2192 Proxy \u2500\u2500\u2500\u2500\u2500\u2524                     \u251c\u2500\u2500 /api/orders  \u2192 [NEW: GCP microservice]\n                    \u2502   Strangler Facade   \u251c\u2500\u2500 /api/products \u2192 [NEW: AWS Lambda]\n                    \u2502   (API Gateway /    \u251c\u2500\u2500 /api/legacy   \u2192 [OLD: Monolith on-prem]\n                    \u2502    Load Balancer)   |\n                    \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n```\nOver time, legacy routes shrink to zero and the monolith is retired.\n\n### Parallel Run\nRun old and new systems simultaneously; compare outputs before cutting over:\n- Shadow traffic to new system; verify results match\n- Gradually increase shadow traffic percentage\n- Cut over when confidence threshold reached (e.g. 99.9% match)\n\n### Blue-Green Cutover\nBlue = current production. Green = new cloud target. Switch DNS/load balancer atomically.\n- Instant rollback: flip DNS back to Blue\n- Requires running two full environments simultaneously during transition\n\n```python\n# Migration wave planner\n# Models application portfolio prioritisation and wave scheduling\n\nfrom dataclasses import dataclass, field\nfrom typing import Dict, List, Optional\nfrom enum import Enum\n\n\nclass MigrationStrategy(str, Enum):\n    RETIRE      = 'Retire'\n    RETAIN      = 'Retain'\n    REHOST      = 'Rehost'\n    REPLATFORM  = 'Replatform'\n    REARCHITECT = 'Rearchitect'\n\n\n@dataclass\nclass Application:\n    name: str\n    business_criticality: int   # 1 (low) \u2013 5 (critical)\n    complexity: int             # 1 (simple) \u2013 5 (complex)\n    dependencies: List[str]     # names of apps this depends on\n    strategy: MigrationStrategy\n    effort_weeks: int\n    team: str\n    target_cloud: str\n\n\ndef dependency_score(app: Application, all_apps: Dict[str, 'Application']) -> int:\n    \"\"\"Count how many OTHER apps depend on this app (higher = migrate last).\"\"\"\n    return sum(\n        1 for a in all_apps.values()\n        if app.name in a.dependencies and a.name != app.name\n    )\n\n\ndef migration_priority(app: Application, all_apps: Dict[str, 'Application']) -> float:\n    \"\"\"\n    Priority score: high business value, low complexity, few dependents \u2192 migrate early.\n    Low score = higher priority (sort ascending).\n    \"\"\"\n    if app.strategy in (MigrationStrategy.RETIRE, MigrationStrategy.RETAIN):\n        return 999.0  # always last\n    dep_score = dependency_score(app, all_apps)\n    return (app.complexity * 2 + dep_score * 3) - app.business_criticality\n\n\ndef assign_waves(apps: List[Application], max_parallel_teams: int = 2) -> Dict[int, List[Application]]:\n    \"\"\"Assign applications to migration waves respecting dependencies.\"\"\"\n    app_map = {a.name: a for a in apps}\n    sorted_apps = sorted(\n        [a for a in apps if a.strategy not in (MigrationStrategy.RETIRE, MigrationStrategy.RETAIN)],\n        key=lambda a: migration_priority(a, app_map),\n    )\n\n    waves: Dict[int, List[Application]] = {}\n    migrated = set()\n    wave = 1\n\n    while sorted_apps:\n        wave_apps = []\n        remaining = []\n        for app in sorted_apps:\n            deps_migrated = all(d in migrated for d in app.dependencies)\n            if deps_migrated and len(wave_apps) < max_parallel_teams:\n                wave_apps.append(app)\n                migrated.add(app.name)\n            else:\n                remaining.append(app)\n        if not wave_apps:\n            # Circular dependency or can't parallelise further \u2014 force next\n            wave_apps = remaining[:1]\n            migrated.add(wave_apps[0].name)\n            remaining = remaining[1:]\n        waves[wave] = wave_apps\n        sorted_apps = remaining\n        wave += 1\n\n    return waves\n\n\n# Sample application portfolio\nportfolio = [\n    Application('auth-service',     business_criticality=5, complexity=2, dependencies=[],\n                strategy=MigrationStrategy.REPLATFORM, effort_weeks=4, team='platform', target_cloud='aws'),\n    Application('user-api',         business_criticality=4, complexity=2, dependencies=['auth-service'],\n                strategy=MigrationStrategy.REHOST, effort_weeks=3, team='backend', target_cloud='aws'),\n    Application('ml-pipeline',      business_criticality=3, complexity=4, dependencies=['user-api'],\n                strategy=MigrationStrategy.REARCHITECT, effort_weeks=12, team='ml', target_cloud='gcp'),\n    Application('reporting-db',     business_criticality=3, complexity=3, dependencies=[],\n                strategy=MigrationStrategy.REPLATFORM, effort_weeks=6, team='data', target_cloud='aws'),\n    Application('analytics-ui',     business_criticality=2, complexity=1, dependencies=['reporting-db'],\n                strategy=MigrationStrategy.REHOST, effort_weeks=2, team='frontend', target_cloud='azure'),\n    Application('legacy-crm',       business_criticality=2, complexity=5, dependencies=[],\n                strategy=MigrationStrategy.RETAIN, effort_weeks=0, team='none', target_cloud='on-prem'),\n    Application('old-batch-system', business_criticality=1, complexity=1, dependencies=[],\n                strategy=MigrationStrategy.RETIRE, effort_weeks=0, team='none', target_cloud='none'),\n]\n\napp_map = {a.name: a for a in portfolio}\nwaves   = assign_waves(portfolio, max_parallel_teams=2)\n\nprint('=== Migration Wave Plan ===')\ntotal_weeks = 0\nfor wave_num, wave_apps in sorted(waves.items()):\n    wave_duration = max(a.effort_weeks for a in wave_apps)\n    total_weeks  += wave_duration\n    print(f'\\nWave {wave_num} ({wave_duration} weeks):')\n    for app in wave_apps:\n        deps_str = f' [deps: {app.dependencies}]' if app.dependencies else ''\n        print(f'  \u2022 {app.name:<25} {app.strategy.value:<14} \u2192 {app.target_cloud:<8} team={app.team}{deps_str}')\n\n# Retirements and retentions\nspecial = [a for a in portfolio if a.strategy in (MigrationStrategy.RETIRE, MigrationStrategy.RETAIN)]\nif special:\n    print('\\nNo-migration items:')\n    for app in special:\n        print(f'  \u2022 {app.name:<25} {app.strategy.value}')\n\nprint(f'\\nTotal migration duration (serial waves): ~{total_weeks} weeks')\n```\n\n## 7.3  Migration Risk and Rollback\n\n| Risk | Mitigation |\n|------|------------|\n| **Data loss during cutover** | Dual-write to old and new stores; validate checksums |\n| **Performance regression** | Load test at 2\u00d7 expected peak before cutover |\n| **DNS propagation lag** | Set TTL to 30s 48 hours before cutover |\n| **Dependency not migrated** | Dependency mapping + reverse dependency check in wave plan |\n| **Credentials not ported** | Secret rotation during migration; use Secrets Manager, not env vars |\n| **Rollback window** | Keep old environment live for 48-72 hours post-cutover |\n\n**Definition of done for a migration wave:**\n1. All smoke tests pass on new cloud\n2. Synthetic monitoring active\n3. Rollback procedure tested (not just written)\n4. Cost baseline captured\n5. Old environment decommission date scheduled\n\n```python\n# Strangler fig pattern simulator\n# Models incremental traffic migration from monolith to cloud microservices\n\nfrom dataclasses import dataclass, field\nfrom typing import Dict, List, Tuple\nimport random\n\n\n@dataclass\nclass RoutingRule:\n    path_prefix: str\n    target: str          # 'monolith' | 'new-service'\n    weight: float        # 0.0-1.0 fraction to new service (rest to monolith)\n\n\n@dataclass\nclass ServiceMetrics:\n    name: str\n    latency_p50_ms: float\n    latency_p99_ms: float\n    error_rate: float    # 0.0-1.0\n\n\nMONOLITH_METRICS   = ServiceMetrics('monolith',     320, 1200, 0.008)\nNEW_SERVICE_METRICS = ServiceMetrics('new-service',  45,   180, 0.001)\n\n\ndef route_request(path: str, rules: List[RoutingRule]) -> Tuple[str, ServiceMetrics]:\n    \"\"\"Route a request and return which system handled it.\"\"\"\n    for rule in rules:\n        if path.startswith(rule.path_prefix):\n            target_metrics = (\n                NEW_SERVICE_METRICS if random.random() < rule.weight\n                else MONOLITH_METRICS\n            )\n            return (rule.target if random.random() < rule.weight else 'monolith'), target_metrics\n    return 'monolith', MONOLITH_METRICS\n\n\ndef simulate_migration_phase(\n    phase_name: str,\n    rules: List[RoutingRule],\n    n_requests: int = 1000,\n) -> None:\n    random.seed(42)\n    handled: Dict[str, int] = {'monolith': 0, 'new-service': 0}\n    errors: Dict[str, int]  = {'monolith': 0, 'new-service': 0}\n    latencies: Dict[str, List[float]] = {'monolith': [], 'new-service': []}\n\n    test_paths = ['/api/orders', '/api/products', '/api/users', '/api/legacy']\n    for i in range(n_requests):\n        path = random.choice(test_paths)\n        target, metrics = route_request(path, rules)\n        handled[target] += 1\n        # Simulate latency with jitter\n        lat = random.gauss(metrics.latency_p50_ms, metrics.latency_p50_ms * 0.2)\n        latencies[target].append(max(1, lat))\n        if random.random() < metrics.error_rate:\n            errors[target] += 1\n\n    print(f'\\n=== {phase_name} ===')\n    for svc in ['monolith', 'new-service']:\n        count = handled[svc]\n        if count == 0:\n            continue\n        avg_lat = sum(latencies[svc]) / count\n        err_pct = errors[svc] / count * 100\n        pct = count / n_requests * 100\n        print(f'  {svc:<15} {pct:>5.1f}% of traffic  avg latency={avg_lat:>6.0f}ms  errors={err_pct:.2f}%')\n\n\n# Phase 1: 0% migrated (all on monolith)\nsimulate_migration_phase('Phase 1: Baseline (0% migrated)', [\n    RoutingRule('/api/orders',   'new-service', weight=0.0),\n    RoutingRule('/api/products', 'new-service', weight=0.0),\n])\n\n# Phase 2: /api/orders fully migrated, /api/products at 50%\nsimulate_migration_phase('Phase 2: Orders migrated, Products 50%', [\n    RoutingRule('/api/orders',   'new-service', weight=1.0),\n    RoutingRule('/api/products', 'new-service', weight=0.5),\n])\n\n# Phase 3: All migrated endpoints at 100%\nsimulate_migration_phase('Phase 3: Full cutover', [\n    RoutingRule('/api/orders',   'new-service', weight=1.0),\n    RoutingRule('/api/products', 'new-service', weight=1.0),\n    RoutingRule('/api/users',    'new-service', weight=1.0),\n])\n```\n\n## Summary\n\n1. **Migrate easy wins first** \u2014 Low complexity, no dependents, low business criticality gives the team confidence before tackling critical workloads.\n2. **Strangler fig is the default pattern** \u2014 It allows incremental migration with instant rollback at every step. Big-bang cutovers compound risk.\n3. **Dependency mapping is prerequisite** \u2014 Migrating an app without migrating what it depends on leaves it stranded. Build the dependency graph before the wave plan.\n4. **Rollback is part of the migration** \u2014 Test the rollback procedure on a low-criticality app before you need it on a critical one.\n\n**Next \u2192** [`08_exercises`](08_exercises.ipynb) \u2014 apply everything you've learned.\n"
}
