diff --git a/docs/DeploymentGuide.md b/docs/DeploymentGuide.md index 47ac1b7e..15c4cd56 100644 --- a/docs/DeploymentGuide.md +++ b/docs/DeploymentGuide.md @@ -191,6 +191,8 @@ Review the configuration options below. You can customize any settings that meet | **Framework** | Basic configuration | [Well-Architected Framework](https://learn.microsoft.com/en-us/azure/well-architected/) | | **Features** | Core functionality | Reliability, security, operational excellence | +When you use the production WAF configuration for Container Migration, only the frontend Container App stays publicly reachable through Application Gateway/WAF. The backend API and processor Container Apps stay on internal ingress inside the Container Apps environment, and the existing VNet subnet/NSG rules continue to limit traffic to internal paths. + **To use production configuration:** Copy the contents from the production configuration file to your main parameters file: diff --git a/infra/main.bicep b/infra/main.bicep index 752669da..5c250eb8 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -1220,7 +1220,7 @@ module containerAppBackend 'br/public:avm/res/app/container-app:0.18.1' = { } ] ingressTargetPort: backendContainerPort - ingressExternal: true + ingressExternal: enablePrivateNetworking ? false : true // WAF: internal-only ingress for backend API scaleSettings: { maxReplicas: enableScalability ? 3 : 1 minReplicas: 1 @@ -1278,7 +1278,14 @@ module containerAppFrontend 'br/public:avm/res/app/container-app:0.18.1' = { env: [ { name: 'API_URL' - value: 'https://${containerAppBackend.outputs.fqdn}' + // Frontend calls same-origin /api; frontend server proxies to backend. + value: '/api' + } + { + name: 'BACKEND_API_URL' + value: enablePrivateNetworking + ? 'https://${backendContainerAppName}.internal.${containerAppsEnvironment.outputs.defaultDomain}' + : 'https://${containerAppBackend.outputs.fqdn}' } { name: 'APP_ENV' diff --git a/infra/main_custom.bicep b/infra/main_custom.bicep index 878b8d1b..8219bf06 100644 --- a/infra/main_custom.bicep +++ b/infra/main_custom.bicep @@ -1165,7 +1165,7 @@ module containerAppBackend 'br/public:avm/res/app/container-app:0.18.1' = { } ] ingressTargetPort: backendContainerPort - ingressExternal: true + ingressExternal: enablePrivateNetworking ? false : true scaleSettings: { maxReplicas: enableScalability ? 3 : 1 minReplicas: 1 @@ -1229,7 +1229,13 @@ module containerAppFrontend 'br/public:avm/res/app/container-app:0.18.1' = { env: [ { name: 'API_URL' - value: 'https://${containerAppBackend.outputs.fqdn}' + value: '/api' + } + { + name: 'BACKEND_API_URL' + value: enablePrivateNetworking + ? 'https://${backendContainerAppName}.internal.${containerAppsEnvironment.outputs.defaultDomain}' + : 'https://${containerAppBackend.outputs.fqdn}' } { name: 'APP_ENV' @@ -1382,7 +1388,9 @@ output AZURE_CONTAINER_REGISTRY_ENDPOINT string = containerRegistry.outputs.logi output SERVICE_BACKEND_NAME string = containerAppBackend.outputs.name @description('Backend service container app URI') -output SERVICE_BACKEND_URI string = 'https://${containerAppBackend.outputs.fqdn}' +output SERVICE_BACKEND_URI string = enablePrivateNetworking + ? 'https://${backendContainerAppName}.internal.${containerAppsEnvironment.outputs.defaultDomain}' + : 'https://${containerAppBackend.outputs.fqdn}' @description('Processor service container app name') output SERVICE_PROCESSOR_NAME string = containerAppProcessor.outputs.name diff --git a/src/frontend/frontend_server.py b/src/frontend/frontend_server.py index 1ab61f5a..b9be3995 100644 --- a/src/frontend/frontend_server.py +++ b/src/frontend/frontend_server.py @@ -1,10 +1,11 @@ import os +import httpx import uvicorn from dotenv import load_dotenv from fastapi import FastAPI, Request from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import FileResponse, HTMLResponse, JSONResponse +from fastapi.responses import FileResponse, JSONResponse, Response from fastapi.staticfiles import StaticFiles # Load environment variables from .env file @@ -12,6 +13,8 @@ app = FastAPI() +BACKEND_API_URL = os.getenv("BACKEND_API_URL", "").rstrip("/") + # Read allowed origins from environment; fall back to same-origin only _allowed_origins = os.getenv("ALLOWED_ORIGINS", "").split(",") _allowed_origins = [o.strip() for o in _allowed_origins if o.strip()] @@ -72,6 +75,60 @@ async def get_config(request: Request): return config +@app.api_route("/api/{full_path:path}", methods=["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"]) +async def proxy_api(full_path: str, request: Request): + if not BACKEND_API_URL: + return JSONResponse(status_code=503, content={"detail": "Backend API URL is not configured"}) + + target_url = f"{BACKEND_API_URL}/api/{full_path}" + if request.url.query: + target_url = f"{target_url}?{request.url.query}" + + headers = dict(request.headers) + headers.pop("host", None) + headers.pop("content-length", None) + + body = await request.body() + + try: + async with httpx.AsyncClient(timeout=120.0, follow_redirects=True) as client: + proxied = await client.request( + method=request.method, + url=target_url, + headers=headers, + content=body, + ) + except httpx.TimeoutException: + return JSONResponse( + status_code=504, + content={"detail": "Upstream backend request timed out"}, + ) + except httpx.RequestError: + return JSONResponse( + status_code=502, + content={"detail": "Failed to reach upstream backend"}, + ) + + excluded_headers = { + "content-encoding", + "transfer-encoding", + "connection", + "content-length", + "content-type", + } + + response = Response( + content=proxied.content, + status_code=proxied.status_code, + media_type=proxied.headers.get("content-type"), + ) + for key, value in proxied.headers.multi_items(): + if key.lower() not in excluded_headers: + response.raw_headers.append((key.encode("latin-1"), value.encode("latin-1"))) + + return response + + @app.get("/{full_path:path}") async def serve_app(full_path: str): # Resolve the requested path and ensure it stays within BUILD_DIR diff --git a/src/frontend/requirements.txt b/src/frontend/requirements.txt index a8bc36ae..ca3183ae 100644 --- a/src/frontend/requirements.txt +++ b/src/frontend/requirements.txt @@ -1,6 +1,7 @@ fastapi==0.116.1 uvicorn[standard]==0.35.0 # uvicorn removed and added above to allow websocket support +httpx==0.28.1 jinja2==3.1.6 azure-identity==1.24.0 python-dotenv==1.1.1