2026-05-14 — Container Apps migration Phase 0: code scaffolding¶
Motivation¶
The migration plan in docs/architecture/container-apps.md
defines a five-phase rollout from the current Function App + SWA setup to a
single bundled Container App with six sidecars. This change lands Phase 0:
all the source code and infrastructure-as-code that the bundled topology
needs, with no Azure deployment performed. Production (rg-elb-prod,
func-elb-prod-*, kind-coast-*.azurestaticapps.net) is unchanged.
The user explicitly asked to "마이그레이션 진행해" (proceed with the migration)
while away. Phase 0 is the safe autonomous step: it produces reviewable code
without spending money or touching production. Phases 1–5 are gated by
explicit user approval because they involve azd provision, MSAL App
Registration changes, and DNS cutover.
User-facing change¶
None at runtime. The new code lives in new directories:
api_app/— FastAPI implementation of the futureapisidecar.web/Dockerfile,web/nginx.conf— frontend sidecar image inputs (the existing Vite SPA inweb/src/is reused unchanged).infra/modules/containerAppsEnvironment.bicep,infra/modules/containerAppControl.bicep,infra/modules/storageState.bicep— Bicep modules for the new topology.scripts/dev/docker-compose.local.yml— local 2-sidecar smoke-test.
The existing Function App (api/), the existing SWA build of web/, and
infra/main.bicep + infra/modules/platform.bicep are not modified.
Files added¶
| Path | Purpose |
|---|---|
api_app/__init__.py |
Package marker, __version__. |
api_app/main.py |
FastAPI app factory; mounts /api/health, /api/me, /api/monitor/*. Structured-ish JSON logging. |
api_app/auth.py |
MSAL bearer-token validator as a FastAPI Depends() dependency. Mirrors the JWKS caching strategy of api/auth/token.py; kept independent so the api_app/ package has no azure.functions import. |
api_app/routes/__init__.py |
Sub-routers package. |
api_app/routes/health.py |
GET /api/health — no auth, returns version + revision id. |
api_app/routes/me.py |
GET /api/me — auth-required, returns caller oid/tid/upn. |
api_app/routes/monitor.py |
GET /api/monitor/cluster — auth-required stub. Phase 3 swaps this for the real cluster card backed by api/services/monitoring.py. |
api_app/requirements.txt |
Pinned: fastapi==0.115.5, uvicorn[standard]==0.32.0, httpx==0.27.2, pyjwt[crypto]==2.10.0, azure-identity==1.19.0. |
api_app/Dockerfile |
Two-stage build (python:3.11-slim deps → runtime). Non-root user (uid 10001). HEALTHCHECK on /api/health. Two uvicorn workers. |
api_app/.dockerignore |
Standard Python ignores. |
web/Dockerfile |
Two-stage build (node:20-alpine Vite build → nginx:alpine). Listens on :8081 (loopback target for the api reverse proxy). |
web/nginx.conf |
Ports staticwebapp.config.json security headers, sets immutable cache for /assets/*, no-cache for /index.html, SPA navigation fallback to /index.html. |
web/.dockerignore |
Standard Node ignores. |
infra/modules/containerAppsEnvironment.bicep |
Workload-profile Container Apps Environment, VNet-integrated via infrastructureSubnetId. Wired to a Log Analytics workspace passed by resource id. |
infra/modules/storageState.bicep |
Adds children to the existing platform Storage account: tables jobstate + jobhistory; containers audit, dead-letter, job-payloads, schedules; file shares redis-data + terminal-home; lifecycle policy that cools/deletes audit blobs. |
infra/modules/containerAppControl.bicep |
Single ca-elb-control Container App. minReplicas: 1, maxReplicas: 1. Public ingress on the api sidecar at :8080. The api sidecar is enabled; the other five sidecars (frontend, worker, beat, redis, terminal) are documented inline as TODO blocks describing image, resource budget, command, env vars, and volume mounts so phase 2 is a fill-in-the-template. |
scripts/dev/docker-compose.local.yml |
Builds and runs the api + frontend sidecars on host ports 8080 and 8081 for local validation. |
docs/features_change/2026-05/2026-05-14-container-app-phase0-scaffolding.md |
This document. |
Validation evidence¶
$ python -c "from api_app.main import app; ..."
{"ts":"...","level":"INFO","logger":"api_app.main","msg":"api sidecar started, version=0.0.1"}
routes:
['GET', 'HEAD'] /openapi.json
['GET'] /api/health
['GET'] /api/me
['GET'] /api/monitor/cluster
$ TestClient + AUTH_DEV_BYPASS=true ...
health: 200 {'status': 'ok', 'version': '0.0.1', 'revision': 'local'}
me (no token): 401 {'detail': 'missing bearer token'}
monitor (no token): 401 {'detail': 'missing bearer token'}
$ az bicep build --file infra/modules/containerAppsEnvironment.bicep --stdout > /dev/null
OK
$ az bicep build --file infra/modules/storageState.bicep --stdout > /dev/null
OK
$ az bicep build --file infra/modules/containerAppControl.bicep --stdout > /dev/null
OK
Phase 1 ready-to-go list¶
To progress to Phase 1 ("Containerize the API on a private network"), an operator needs to:
- Decide a target environment (recommended: a fresh
rg-elb-ca-staginginkoreacentralto avoid colliding withrg-elb-prod). - Provision the platform VNet + subnets (
snet-containerapps/23 delegated toMicrosoft.App/environments,snet-private-endpoints,snet-aks), the platform Log Analytics workspace, and the platform ACR (or reuse the existing one). - Build and push the api image:
- Wire
containerAppsEnvironment.bicepandcontainerAppControl.bicepinto a newinfra/main.staging.bicepor extendinfra/main.bicepbehind a feature flag, thenazd provision(separateazd env). - Verify
GET /api/healthresponds on the new ingress hostname.
Until then, this PR is purely additive code and can ship without any Azure side effects.
Out of scope (deferred to later phases)¶
- Phase 2 — wire the other five sidecars (frontend, worker, beat, redis, terminal), build their images, mount the Azure Files volumes, dispatch Celery tasks, add the WebSocket terminal proxy.
- Phase 3 — migrate the route set from
api/toapi_app/, delete the per-VM terminal API surface, delete cloud-init. - Phase 4 — verify and tighten private networking (Key Vault, Storage, ACR, AKS, the Azure Files private endpoint that backs the redis mount).
- Phase 5 — production cutover; delete the SWA and Function App resources after a full release window.