Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions docs/DeploymentGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
11 changes: 9 additions & 2 deletions infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
Ashwal-Microsoft marked this conversation as resolved.
scaleSettings: {
maxReplicas: enableScalability ? 3 : 1
minReplicas: 1
Expand Down Expand Up @@ -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'
Expand Down
14 changes: 11 additions & 3 deletions infra/main_custom.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -1165,7 +1165,7 @@ module containerAppBackend 'br/public:avm/res/app/container-app:0.18.1' = {
}
]
ingressTargetPort: backendContainerPort
ingressExternal: true
ingressExternal: enablePrivateNetworking ? false : true
Comment thread
Ashwal-Microsoft marked this conversation as resolved.
scaleSettings: {
maxReplicas: enableScalability ? 3 : 1
minReplicas: 1
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down
59 changes: 58 additions & 1 deletion src/frontend/frontend_server.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
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
load_dotenv()

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()]
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/frontend/requirements.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Loading