Multi-cloud container deployment pipeline: from local Docker development to production Kubernetes — deployed across AWS (ECS + EKS) and Azure (AKS).
Takes a Flask API through four deployment stages, each increasing in complexity:
| Phase | Platform | How It Runs |
|---|---|---|
| Phase 1 | Local (WSL2) | Docker container on localhost |
| Phase 2 | AWS ECS | Fargate serverless container via ECR |
| Phase 3 | AWS EKS | Kubernetes pods with LoadBalancer |
| Phase 4 | Azure AKS | ACR + AKS + GitHub Actions CI/CD |
Each phase deploys the same containerized app to a progressively more complex environment.
┌──────────────────────────────────┐
│ Flask API (app.py) │
│ / /health /info /ping │
└──────────────┬───────────────────┘
│
Docker Image
(multi-stage build)
│
┌──────────────┬─────────────────┼───────────────────────┐
│ │ │ │
Phase 1: Local Phase 2: ECS Phase 3: EKS Phase 4: AKS
────────────── ───────────── ───────────── ─────────────
Docker run ECR → Fargate ECR → K8s Pods ACR → K8s Pods
localhost:5000 Public IP:5000 LoadBalancer:80 LoadBalancer:80
1 task, 0.25 2 replicas 2 replicas
vCPU, 0.5 GB t3.small nodes D2s_v3 node
| Tool | Purpose |
|---|---|
| Flask | Python API (5 endpoints) |
| Docker | Multi-stage container build |
| Gunicorn | Production WSGI server |
| GitHub Actions | CI: lint, test, build, push to GHCR + ACR |
| AWS ECR | Private container registry (Phase 2-3) |
| AWS ECS (Fargate) | Serverless container deployment |
| AWS EKS | Managed Kubernetes cluster |
| Azure ACR | Private container registry (Phase 4) |
| Azure AKS | Managed Kubernetes cluster |
| kubectl | Kubernetes deployment management |
| eksctl | EKS cluster provisioning |
the-migration-arc/
├── app/
│ ├── app.py # Flask API (5 routes)
│ ├── Dockerfile # Multi-stage build (builder + runtime)
│ └── requirements.txt # flask + gunicorn
├── k8s/
│ ├── aws/
│ │ ├── deployment.yaml # K8s Deployment (ECR image, 2 replicas)
│ │ └── service.yaml # LoadBalancer Service (80 → 5000)
│ └── azure/
│ ├── deployment.yaml # K8s Deployment (ACR image, 2 replicas)
│ └── service.yaml # LoadBalancer Service (80 → 5000)
├── images/
│ ├── aws/ # ECS + EKS deployment screenshots
│ └── azure/ # AKS deployment screenshots
├── tests/
│ └── test_app.py # 5 unit tests (pytest)
├── .github/workflows/
│ ├── ci.yml # CI: lint → test → build → push to GHCR
│ └── azure-deploy.yml # CI: lint → test → build → push to ACR
├── Makefile # Local dev commands (build/run/stop/test)
├── DECISIONS.md # Technical decision log per phase
└── README.md
| Route | Response |
|---|---|
GET / |
Service name, status, message |
GET /health |
Health check with UTC timestamp |
GET /info |
Hostname, OS, Python version, app version |
GET /ping |
{"pong": true} |
GET /metrics/custom |
Request count, uptime, environment |
Built and ran the container locally:
make build # docker build -t migration-arc-app:local ./app
make run # docker run --rm -p 5000:5000 migration-arc-app:local
curl localhost:5000Key decisions:
- Multi-stage Dockerfile - build dependencies stay out of runtime image
- Non-root user (
appuser) for container security - Gunicorn with 2 workers instead of Flask dev server
Pushed container to ECR, deployed as Fargate task:
- Created private ECR repo (
migration-arc-flask) - Authenticated Docker to ECR, tagged and pushed image
- Created ECS cluster (
migration-arc-project-cluster) - Defined task (0.25 vCPU, 0.5 GB, port 5000)
- Created service with public IP + security group (TCP 5000 open)
Result: Flask API accessible via public IP on port 5000.
Created managed Kubernetes cluster, deployed as pods:
- Provisioned EKS cluster with
eksctl(2x t3.small nodes) - Created Deployment (2 replicas pulling from ECR)
- Created LoadBalancer Service (port 80 → container 5000)
- App accessible via AWS ELB URL
Result: Flask API running on Kubernetes with load balancing across 2 pods.
Deployed to Azure Kubernetes Service with container registry and automated pipeline:
- Created resource group (
migration-arc-rg) in Canada Central - Created Azure Container Registry (
migrationarcacr) — Basic SKU - Built and pushed Docker image to ACR (
migration-arc-app:v1) - Provisioned AKS cluster (
migration-arc-aks) — 1 node, Standard_D2s_v3, Free tier - Attached ACR to AKS via managed identity (AcrPull role)
- Applied K8s manifests — 2 replicas + LoadBalancer Service
- App accessible via Azure Load Balancer public IP
- Created GitHub Actions pipeline (
azure-deploy.yml) — auto builds and pushes to ACR on every merge to main
Result: Flask API running on AKS with CI/CD pipeline. Every push to main triggers lint → test → build → push to ACR.
- On push/PR to main: Lint with flake8 → Run 5 pytest tests
- On merge to main: Build Docker image → Push to GitHub Container Registry (GHCR)
- Uses Docker layer caching for fast builds
- On push to main: Lint with flake8 → Run 5 pytest tests
- If tests pass: Build Docker image → Push to ACR with commit SHA + latest tags
- ACR credentials stored as GitHub Actions secrets
- Supports manual trigger via
workflow_dispatch
| # | Issue | Root Cause | Fix |
|---|---|---|---|
| 1 | docker build failed - "no such file or directory" |
Dockerfile is in ./app/, not root. Build context wrong |
Used make build which runs docker build ./app |
| 2 | ECS cluster creation failed - "Unable to assume service linked role" | New AWS account, ECS service-linked role did not exist | Ran aws iam create-service-linked-role --aws-service-name ecs.amazonaws.com |
| 3 | ECS cluster creation failed again - CloudFormation stack conflict | First failed attempt left orphaned CloudFormation stack | Deleted failed stack in CloudFormation console, retried with new cluster name |
| 4 | eksctl create cluster - AccessDeniedException |
IAM user rajan-admin lacked EKS permissions |
Added EKS policies + AdministratorAccess |
| 5 | YAML parse error - "could not find expected ':'" | Mixed tabs and spaces from Windows editor (Notepad) | Recreated YAML files in terminal using heredoc |
| 6 | Pods stuck in Pending - FailedScheduling |
t3.micro nodes too small - system pods consumed all capacity |
Upgraded to t3.small nodes via eksctl create nodegroup |
| 7 | AKS node pool rejected B-series VMs | B-series (burstable) not allowed for AKS system node pools | Switched to D2s_v3 (general purpose) |
| 8 | AKS node pool - vCPU quota error | Azure for Students has 0 quota for v5 VM families | Used v3 family (D2s_v3) which had 10 vCPU quota |
| 9 | Service principal creation failed | Azure for Students blocks directory-level permissions | Used ACR admin credentials for CI/CD instead of service principal |
| 10 | GitHub Actions flake8 lint failure | PEP 8: missing 2 blank lines before function definitions | Fixed spacing in app.py |
| Resource | Cost (24/7) | Notes |
|---|---|---|
| AWS | ||
| ECS Fargate (0.25 vCPU) | ~$9/month | Serverless, pay per task |
| EKS control plane | $72/month | Fixed cost regardless of nodes |
| EKS nodes (2x t3.small) | ~$30/month | EC2 instances |
| EKS LoadBalancer | ~$18/month | AWS NLB/ALB |
| ECR storage | ~$0.01/month | Just image storage |
| Azure | ||
| AKS control plane | $0/month | Free tier |
| AKS node (1x D2s_v3) | ~$68/month | VM instance |
| AKS LoadBalancer | ~$18/month | Azure LB + public IP |
| ACR Basic | ~$5/month | Registry storage |
| Total (all live) | ~$220/month | |
| After cleanup | ~$0.01/month | Only ECR repo kept |
Infrastructure was deployed for demonstration, verified working, then torn down to manage costs. Screenshots and deployment evidence in
images/and commit history.
- Docker multi-stage builds reduce image size and separate build-time from runtime dependencies
- ECS vs EKS tradeoff: ECS simpler for single containers; EKS worth it when you need scaling, rolling updates, and multi-container orchestration
- Fargate eliminates server management but costs more per unit than EC2
- Kubernetes YAML - indentation matters, never edit with editors that mix tabs/spaces
- IAM least privilege is ideal but impractical for eksctl - it creates VPCs, roles, CloudFormation stacks, and EC2 instances
- Node sizing matters - t3.micro cannot run app pods because AWS system pods consume most capacity
- Service-linked roles are auto-created on first use in established accounts but may need manual creation in new accounts
- AKS Free tier makes Azure competitive for learning - no control plane cost vs EKS $72/month
- Azure for Students has VM quota limits per family - v5 series had 0 quota, v3 had 10
- ACR + AKS integration via managed identity is cleaner than manual docker login - one attachment handles auth
- GitHub Actions secrets keep credentials out of code - ACR admin password never touches the repo
- Multi-cloud K8s manifests are nearly identical - same Deployment/Service YAML, only the image registry URL changes
Infrastructure was deployed, verified working, then torn down to manage costs. Screenshots below serve as proof of successful deployment.
| Screenshot | What It Shows |
|---|---|
![]() |
ECS cluster running on Fargate |
![]() |
Fargate service with desired count and running tasks |
![]() |
Individual Fargate task with public IP assigned |
![]() |
Flask API responding on ECS public IP:5000 |
| Screenshot | What It Shows |
|---|---|
![]() |
CloudFormation stacks created by eksctl for EKS cluster + node groups |
| Screenshot | What It Shows |
|---|---|
![]() |
Docker image pushed to ECR private registry |
![]() |
IAM user with ECR, ECS, and EKS policies attached |
![]() |
$5/month budget alert configured |
git clone https://github.com/imrajankumar95/the-migration-arc.git
cd the-migration-arc
make build
make run
# Visit http://localhost:5000Run tests:
make testRajan Kumar - Cloud Computing student at George Brown College, Toronto. Building toward a Cloud/DevOps co-op role (Fall 2026).
















