Module 6: Cloud Migration Patterns
1. Introduction: Cloud Migration Patterns
Overview
This is the first capsule of Module 6 of the Deployment & Cloud Infrastructure Guide. This is the most complex module in the guide — and probably the most valuable for your career as an engineer. Here you'll learn the engineering patterns that let the same code work against LocalStack in development and against AWS in production, without changing a single line of business logic. It's what separates a prototype that "works on my machine" from a system that operates in real production.
Why it matters: In the previous modules you learned individual tools: Docker Compose (M2), Lambda (M3), LocalStack (M4), AWS Services (M5). Each one works in its context. But the real problem isn't using a tool — it's migrating between environments without breaking anything. Your LocalStack boto3 code has endpoint_url="http://localhost:4566" hardcoded. Your AWS config has different credentials. Your tests assume a specific environment. This doesn't scale. This module teaches you to design your application so that configuration decides the environment and the code simply runs — identical on LocalStack, AWS staging, and AWS production.
The promise of LocalStack in Module 4 was: "develop locally, migrate with confidence." This module delivers on that promise. By the end, you'll have an AI app that runs against LocalStack and AWS with the same codebase, tests that verify both environments, and a migration runbook that another engineer can follow step by step.
Where Are We in the Guide?
Context
Phase 1: Deployment Strategies (Modules 1-3)
├── Module 1: Understanding Deployment Options ✅ COMPLETED
├── Module 2: Local & Container Deployment ✅ COMPLETED
└── Module 3: Serverless & Lambda for AI ✅ COMPLETED
Phase 2: Cloud Infrastructure & Migration (Modules 4-6)
├── Module 4: LocalStack — AWS Local Development ✅ COMPLETED
├── Module 5: AWS Services for AI ✅ COMPLETED
└── Module 6: Cloud Migration Patterns ← YOU ARE HERE
Phase 3: Alternatives & Production (Modules 7-8)
├── Module 7: Alternative Platforms (Render, Railway, Fly.io)
└── Module 8: Integrator Project — Deployed AI System
Total estimated guide duration: 10-12 hours (self-paced).
Transition from Module 5
In Module 5 you went deeper into real AWS: S3 as the data layer, Lambda for inference, IAM with least privilege, event-driven integration, SageMaker basics, and cost estimation. You have a functional AI service on AWS. But that service has a problem: it's coupled to its environment.
If you look at the M5 code, you'll find:
- Hardcoded endpoints —
endpoint_urlin some files, absent in others. Every environment change requires editing code. - Scattered configuration — Environment variables here, constants there, credentials mixed with logic.
- Tests tied to one environment — Your tests work against LocalStack OR against AWS, but not against both with the same code.
- No degradation — If S3 doesn't respond, the app crashes. There's no fallback, no circuit breaker.
This module solves each of these problems with concrete engineering patterns:
| Problem | Pattern | Capsule |
|---|---|---|
| Hardcoded endpoints | Environment Abstraction | 02 |
| Scattered configuration | Multi-environment Config Management | 03 |
| Coupled clients | Dependency Injection for boto3 | 04 |
| Tests tied to one environment | Multi-environment testing | 05 |
| Unavailable features | Cloud Feature Flags | 06 |
| No degradation | Graceful Degradation | 07 |
| Everything integrated | Project: Migration-Ready AI App | 08 |
What Cloud Migration Patterns Are
The real problem: different environments, same code
Development (LocalStack) Staging (AWS) Production (AWS)
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ endpoint: localhost │ │ endpoint: AWS │ │ endpoint: AWS │
│ credentials: test │ │ credentials: IAM │ │ credentials: IAM │
│ S3: local bucket │ │ S3: staging bucket │ │ S3: prod bucket │
│ Lambda: local │ │ Lambda: staging │ │ Lambda: prod │
│ SageMaker: ❌ N/A │ │ SageMaker: ✅ │ │ SageMaker: ✅ │
│ IAM: no enforced │ │ IAM: enforced │ │ IAM: strict │
│ Cost: $0 │ │ Cost: low │ │ Cost: variable │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
│ │ │
└──────────────────────────┼─────────────────────────┘
│
SAME SOURCE CODE
The goal is that your application doesn't know (or care) which environment it runs in. The configuration tells it which endpoints to use, which credentials, which features are available. The code runs the same logic every time.
Before vs After: the contrast that matters
Before (M5 code, coupled to one environment):
import boto3
s3 = boto3.client("s3", endpoint_url="http://localhost:4566")
bucket = "ai-assets-dev"
def process_document(doc_key: str):
response = s3.get_object(Bucket=bucket, Key=doc_key)
content = response["Body"].read().decode("utf-8")
# ... processing ...
return result
Problem: if you want to run this on AWS, you need to edit the code. Remove endpoint_url, change the bucket name. If you go back to development, edit it again.
After (migration-ready code):
from config import get_settings
from clients import get_s3_client
settings = get_settings()
s3 = get_s3_client(settings)
def process_document(doc_key: str):
response = s3.get_object(Bucket=settings.s3_bucket, Key=doc_key)
content = response["Body"].read().decode("utf-8")
# ... identical processing ...
return result
Environment change: ENVIRONMENT=aws → the app connects to AWS. ENVIRONMENT=local → connects to LocalStack. Zero changes to business logic.
The 5 patterns of this module
Cloud Migration Patterns:
├── 1. Environment Abstraction
│ └── Abstract endpoints and config so the code is environment-agnostic
│
├── 2. Multi-environment Config Management
│ └── Pydantic Settings + .env files + per-environment validation
│
├── 3. Dependency Injection for Clients
│ └── Factory pattern: inject boto3 clients configured for the right environment
│
├── 4. Multi-environment Testing
│ └── Same test suite against LocalStack AND AWS with pytest fixtures
│
├── 5. Feature Flags + Graceful Degradation
│ └── Enable/disable features per environment + degrade without crashing
These aren't theoretical patterns. They're code you'll implement and that your app will use in real production. Each one builds on the previous: first you abstract (02), then you configure (03), then you inject (04), then you test (05), then you handle what isn't available (06-07).
Module Objective
By the end of this module you'll be able to:
- ✅ Implement environment abstraction that lets the same code interact with LocalStack or AWS by changing only configuration
- ✅ Design per-environment config management (development/LocalStack, staging/AWS, production/AWS) with Pydantic settings, validation, and secrets management
- ✅ Use dependency injection for boto3 clients: inject clients configured according to the environment, without hardcoding endpoints in business logic
- ✅ Write tests that run against LocalStack AND AWS with the same test suite, using feature flags for AWS-only features
- ✅ Implement feature flags for services that aren't available in every environment (SageMaker on AWS, not on LocalStack)
- ✅ Design graceful degradation: if a cloud service doesn't respond, the app degrades functionality instead of crashing
- ✅ Document an operational migration runbook that another engineer can follow step by step
- ✅ Produce a Migration-Ready AI App that runs against LocalStack and AWS without code changes
Professional objective
When an engineering team says "we need to migrate from staging to production" or "we're going to add a QA environment," you'll know exactly what to do: config per environment, injected clients, tests that verify, feature flags for different capabilities, and a documented runbook. It's not just that your app works — it's that you can operate the migration as a repeatable, verifiable process. This is the skill that separates a developer from a production engineer.
Module Roadmap
Capsule map
| # | Capsule | What you'll learn | Type |
|---|---|---|---|
| 01 | Introduction (this one) | Context, objectives, setup, roadmap | Intro |
| 02 | Environment Abstraction | Abstract endpoints and config for environment-agnostic code | Technical |
| 03 | Multi-environment Config Management | Pydantic Settings, .env files, validation, per-environment secrets | Technical |
| 04 | Dependency Injection boto3 Clients | Factory pattern, injecting configured S3/Lambda clients | Technical |
| 05 | Multi-environment Testing | Pytest fixtures, conftest.py, tests against LocalStack AND AWS | Technical |
| 06 | Cloud Feature Flags | Enable/disable features per environment, SageMaker only on AWS | Technical |
| 07 | Graceful Degradation | Fallbacks, circuit breakers, health check with degradation levels | Technical |
| 08 | Project: Migration-Ready AI App | Migration-ready AI app with abstraction layers, tests, runbook | Project |
Learning flow
First you'll understand environment abstraction (capsule 02): how to abstract endpoints and configuration so the code doesn't know whether it's talking to LocalStack or AWS. Then you'll implement structured config management (capsule 03): Pydantic settings, per-environment .env files, validation, and secrets management. Next you'll use dependency injection (capsule 04) to create boto3 clients configured with a factory pattern — the business logic receives ready-made clients, it doesn't build them. With the config and client infrastructure in place, you'll write multi-environment tests (capsule 05): same test suite, different backends, with fixtures that detect the environment. You'll implement feature flags (capsule 06) to handle services that only exist in certain environments (SageMaker on AWS, not on LocalStack). You'll design graceful degradation (capsule 07) so the app doesn't crash if a cloud service doesn't respond. Finally you'll integrate everything into a Migration-Ready AI App (capsule 08).
The progression is: abstraction → configuration → injection → testing → feature flags → degradation → project.
Estimated module duration: 1.5-2 hours.
Connection with the Project
This module's project: Migration-Ready AI App
The Migration-Ready AI App is the Module 5 AI app refactored with abstraction layers:
- Environment abstraction: endpoints and credentials by config
- Config management: Pydantic settings with per-environment validation
- Dependency injection: factory creates boto3 clients according to the environment
- Multi-environment tests: same suite against LocalStack and AWS
- Feature flags: SageMaker enabled only on AWS
- Graceful degradation: fallbacks if a service doesn't respond
- Migration runbook: step-by-step document to migrate
Migration-Ready AI App:
├── config/
│ ├── settings.py ← Pydantic Settings
│ ├── .env.local ← LocalStack config
│ ├── .env.staging ← AWS staging config
│ └── .env.production ← AWS production config
├── clients/
│ ├── factory.py ← boto3 clients factory
│ └── health.py ← Health checks
├── services/
│ ├── document_processor.py ← Business logic (environment-agnostic)
│ └── feature_flags.py ← Feature flags
├── tests/
│ ├── conftest.py ← Multi-environment fixtures
│ ├── test_s3_operations.py
│ └── test_migration.py
├── migration/
│ └── RUNBOOK.md ← Operational migration runbook
└── handler.py ← Lambda handler
Connection with earlier and later modules
Module 4: LocalStack → you tested S3 + Lambda locally
↓
Module 5: AWS Services → you deepened integration, IAM, costs
↓
Module 6: Migration Patterns → you abstract so it works on both ← YOU ARE HERE
↓
Module 7: Alternative Platforms → the abstraction layer makes it easy to evaluate alternatives
↓
Module 8: Integrator Project → deploy the migration-ready artifact to production
The M6 migration-ready app is the artifact deployed in M8. And the abstraction layer you build here makes the M7 evaluation easier: if the decision matrix says "Render instead of AWS," your migration-ready app can adapt because the infrastructure is abstracted.
Prerequisites
What you already know
- ✅ S3 for AI assets — Organization, boto3 operations, lifecycle (Module 5)
- ✅ Lambda for inference — Handler, retry, structured output (Modules 3, 5)
- ✅ LocalStack — Local S3 and Lambda, boto3 with endpoint_url (Module 4)
- ✅ IAM basics — Roles, policies, least privilege (Module 5)
- ✅ Docker Compose — Multi-container, services (Module 2)
- ✅ Intermediate Python — Classes, async, error handling, pydantic
- ✅ AI apps — You've invoked LLMs from code (OpenAI SDK)
What you'll learn here (new)
- Environment abstraction: environment-agnostic code with config switching
- Pydantic Settings: typed config management with automatic validation
- Factory pattern for boto3: injecting clients configured per environment
- Multi-environment testing: conftest.py with environment detection and feature flags
- Cloud feature flags: enable/disable capabilities per environment
- Graceful degradation: circuit breakers, fallbacks, degraded health check
- Migration runbook: step-by-step operational documentation
If you're missing something
| You're missing | Recommended resource |
|---|---|
| S3 and Lambda on AWS | Module 5 of this guide |
| LocalStack | Module 4 of this guide |
| Lambda fundamentals | Module 3 of this guide |
| Docker Compose | Module 2 of this guide |
| Python + FastAPI | Python REST APIs for AI Guide — NIEVA |
| AI apps | AI Engineering Bootcamp — NIEVA |
Technical Setup
Required tools
# Python 3.10+ (same as previous modules)
python --version
# Module dependencies
pip install boto3 pydantic pydantic-settings python-dotenv pytest
# Verify
python -c "from pydantic_settings import BaseSettings; print('pydantic-settings OK')"
python -c "import boto3; print(f'boto3 {boto3.__version__} OK')"
python -c "import pytest; print(f'pytest {pytest.__version__} OK')"
# Docker and LocalStack (from M4)
docker --version
docker run -d --name localstack \
-p 4566:4566 \
-e SERVICES=s3,lambda,iam \
localstack/localstack
# AWS CLI
aws --version
Module structure
mkdir -p module-06/{config,clients,services,tests,migration}
cd module-06
# Final structure
# module-06/
# ├── config/
# │ ├── settings.py # Pydantic Settings
# │ ├── .env.local # LocalStack config
# │ ├── .env.staging # AWS staging config
# │ └── .env.production # AWS production config
# ├── clients/
# │ ├── factory.py # boto3 clients factory
# │ └── health.py # Health checks
# ├── services/
# │ ├── document_processor.py # Business logic
# │ └── feature_flags.py # Feature flags
# ├── tests/
# │ ├── conftest.py # Multi-environment fixtures
# │ ├── test_s3_operations.py
# │ └── test_migration.py
# ├── migration/
# │ └── RUNBOOK.md # Migration runbook
# └── handler.py # Lambda handler
Quick environment check
import boto3
import os
def verify_environment():
"""Verifies that the development environment is ready."""
checks = {}
try:
s3 = boto3.client(
"s3",
endpoint_url="http://localhost:4566",
aws_access_key_id="test",
aws_secret_access_key="test",
region_name="us-east-1",
)
s3.list_buckets()
checks["localstack"] = "✅ Connected"
except Exception as e:
checks["localstack"] = f"❌ {e}"
try:
from pydantic_settings import BaseSettings
checks["pydantic_settings"] = "✅ Installed"
except ImportError:
checks["pydantic_settings"] = "❌ pip install pydantic-settings"
try:
import pytest
checks["pytest"] = "✅ Installed"
except ImportError:
checks["pytest"] = "❌ pip install pytest"
for check, status in checks.items():
print(f" {check}: {status}")
return all("✅" in v for v in checks.values())
if verify_environment():
print("\nEnvironment ready for Module 6.")
else:
print("\nSome components are missing. Check the errors above.")
Limits: What This Module Does NOT Cover
- ❌ Multi-cloud — We don't migrate from AWS to GCP or Azure. The abstraction pattern is similar, but the scope is LocalStack → AWS.
- ❌ Infrastructure as Code — We don't use Terraform or CDK. The migration is at the application-code level, not declarative infrastructure.
- ❌ CI/CD pipelines — We don't configure deployment pipelines. That's part of the Integrator Project (M8).
- ❌ Kubernetes — There's no container orchestration. The focus is serverless (Lambda) and storage (S3).
- ❌ Service mesh — There's no Istio, Envoy, or service meshes. It's over-engineering for our scope.
- ❌ Blue/green deployment — Advanced deployment strategies are out of scope. Here we migrate config, not traffic.
What we do cover (and why)
| Topic | Reason |
|---|---|
| Environment abstraction | The code shouldn't know which environment it runs in |
| Config management | Typed, validated, and secure configuration per environment |
| Dependency injection | Cloud clients injected, not built in business logic |
| Multi-environment testing | Verify the migration works before running it |
| Feature flags | Handle different capabilities across environments |
| Graceful degradation | The app works (degraded) even if a service fails |
Evidence of Success
By the end of this module, you'll know you succeeded if:
- ✅ Your AI app runs against LocalStack with
ENVIRONMENT=localwithout changing code - ✅ The same app runs against AWS with
ENVIRONMENT=awswithout changing code - ✅ Your tests pass against LocalStack AND against AWS with the same test suite
- ✅ SageMaker is enabled only when
ENVIRONMENT=aws(feature flag) - ✅ If S3 doesn't respond, the app returns a degraded response instead of crashing
- ✅ You have a migration runbook that another engineer can follow step by step
- ✅ You can add a new environment (e.g.,
ENVIRONMENT=qa) by changing only configuration
Quick self-assessment test
If you can answer these questions, you're on the right track:
- How do you switch your app from LocalStack to AWS without editing source code?
- What happens if your app tries to use SageMaker on LocalStack?
- How do you verify that your migration from LocalStack to AWS didn't break anything?
- What does your app do if S3 is temporarily down?
Summary
- This is the most complex module in the guide — and the most valuable. The patterns you learn here are the ones senior engineers use to operate systems in real production.
- Same code, any environment. Your app shouldn't know whether it runs on LocalStack, AWS staging, or AWS production. Configuration decides, the code runs.
- Migration as a process, not an event. It's not "press a button." It's a process with steps, verification, rollback. The runbook captures that process.
- Testing as a safety net. Migration without tests is a leap of faith. With multi-environment tests, it's a verifiable, repeatable process.
- The patterns are transferable. Abstraction, config management, dependency injection, feature flags — they apply to any infrastructure change, not just LocalStack → AWS.
- The code in this module becomes the artifact of the Integrator Project (M8).
Additional Resources
- The Twelve-Factor App — Config — Principles of per-environment configuration
- Pydantic Settings Management — Official Pydantic Settings documentation
- Martin Fowler — Feature Toggles — Reference on feature flags
- AWS Well-Architected — Reliability — Graceful degradation on AWS
- Microsoft — Circuit Breaker Pattern — Circuit breaker pattern
- LocalStack Documentation — LocalStack reference
- Dependency Injection in Python — DI patterns in Python