Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
7 changes: 7 additions & 0 deletions docs/DeploymentGuide.md
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,13 @@ 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 using the Production/WAF deployment (`enablePrivateNetworking=true`), networking is configured as follows:

- Backend Container App endpoints are internal-only (`ingressExternal=false`) and not publicly reachable.
- Container Apps Environment is deployed in internal mode with VNet integration.
- The web frontend remains public and routes browser API traffic through same-origin `/api` proxying to the private backend over VNet.
- Private DNS is configured for the internal Container Apps Environment domain.
Comment on lines +200 to +202

**To use production configuration:**

Copy the contents from the production configuration file to your main parameters file:
Expand Down
2 changes: 2 additions & 0 deletions docs/TechnicalArchitecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ The API also provides schema management, schema set (collection) management, and
### Claim Process Monitor Web
Using Azure Container App, this app acts as the UI for the process monitoring queue. The app is built with React and TypeScript. It acts as an API client to create an experience for uploading new documents, creating and managing claim batches, monitoring current and historical processes, and reviewing output results including summarization and gap analysis.

In WAF/private networking deployments (`enablePrivateNetworking=true`), the frontend remains public while backend APIs are internal-only. The web container proxies `/api/*` traffic to the private API Container App over VNet so backend endpoints are not directly exposed to the public internet.

### App Configuration
Using Azure App Configuration, app settings and configurations are centralized and used with the Content Processor, Content Process API, Content Process Workflow, and Claim Process Monitor Web.

Expand Down
14 changes: 11 additions & 3 deletions infra/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -931,7 +931,8 @@ module avmContainerAppEnv 'br/public:avm/res/app/managed-environment:0.13.2' = {
}
]
enableTelemetry: enableTelemetry
publicNetworkAccess: 'Enabled' // Always enabled for Container Apps Environment
publicNetworkAccess: 'Enabled'
internal: false

// <========== WAF related parameters

Expand Down Expand Up @@ -1132,7 +1133,7 @@ module avmContainerApp_API 'br/public:avm/res/app/container-app:0.22.1' = {
}
]
}
ingressExternal: true
ingressExternal: enablePrivateNetworking ? false : true

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With ingressExternal set to false in private networking mode, the API Container App FQDN will no longer be reachable from outside the VNet. The repo's post-deployment scripts currently use CONTAINER_API_APP_FQDN to wait for readiness and register schemas from the deployer's machine; that flow will fail for enablePrivateNetworking=true. Consider updating the post-deployment workflow to call the API through the web app's new /api proxy (using CONTAINER_WEB_APP_FQDN) or to execute the registration from within the VNet (e.g., via the jumpbox).

Suggested change
ingressExternal: enablePrivateNetworking ? false : true
ingressExternal: true

Copilot uses AI. Check for mistakes.
activeRevisionsMode: 'Single'
ingressTransport: 'auto'
corsPolicy: {
Expand Down Expand Up @@ -1201,6 +1202,10 @@ module avmContainerApp_Web 'br/public:avm/res/app/container-app:0.22.1' = {
env: [
{
name: 'APP_API_BASE_URL'
value: enablePrivateNetworking ? '/api' : 'https://${avmContainerApp_API.outputs.fqdn}'
}
{
name: 'APP_BACKEND_API_URL'
value: 'https://${avmContainerApp_API.outputs.fqdn}'
}
{
Expand Down Expand Up @@ -1808,7 +1813,7 @@ module avmContainerApp_API_update 'br/public:avm/res/app/container-app:0.22.1' =
}
]
}
ingressExternal: true
ingressExternal: enablePrivateNetworking ? false : true
activeRevisionsMode: 'Single'
ingressTransport: 'auto'
corsPolicy: {
Expand Down Expand Up @@ -1938,5 +1943,8 @@ output CONTAINER_REGISTRY_LOGIN_SERVER string = avmContainerRegistry.outputs.log
@description('The name of the Content Understanding AI Services account.')
output CONTENT_UNDERSTANDING_ACCOUNT_NAME string = avmAiServices_cu.outputs.name

@description('Whether private networking (WAF) is enabled.')
output ENABLE_PRIVATE_NETWORKING bool = enablePrivateNetworking

@description('The resource group the resources were deployed into.')
output AZURE_RESOURCE_GROUP string = resourceGroup().name
18 changes: 18 additions & 0 deletions infra/main.parameters.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,24 @@
"existingFoundryProjectResourceId": {
"value": "${AZURE_EXISTING_AIPROJECT_RESOURCE_ID}"
},
"enableMonitoring": {
"value": true
},
"enablePrivateNetworking": {
"value": true
},
"enableScalability": {
"value": true
},
Comment on lines +35 to +43
"vmAdminUsername": {
"value": "${AZURE_ENV_VM_ADMIN_USERNAME}"
},
"vmAdminPassword": {
"value": "${AZURE_ENV_VM_ADMIN_PASSWORD}"
},
"vmSize": {
"value": "${AZURE_ENV_VM_SIZE}"
},
Comment on lines +39 to +52
"containerRegistryEndpoint": {
"value": "${AZURE_ENV_CONTAINER_REGISTRY_ENDPOINT}"
},
Expand Down
14 changes: 11 additions & 3 deletions infra/main_custom.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -934,7 +934,8 @@ module avmContainerAppEnv 'br/public:avm/res/app/managed-environment:0.13.2' = {
}
]
enableTelemetry: enableTelemetry
publicNetworkAccess: 'Enabled' // Always enabled for Container Apps Environment
publicNetworkAccess: 'Enabled'
internal: false

// <========== WAF related parameters

Expand Down Expand Up @@ -1145,7 +1146,7 @@ module avmContainerApp_API 'br/public:avm/res/app/container-app:0.22.1' = {
}
]
}
ingressExternal: true
ingressExternal: enablePrivateNetworking ? false : true

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With ingressExternal set to false in private networking mode, the API Container App FQDN will no longer be reachable from outside the VNet. The repo's post-deployment scripts currently use CONTAINER_API_APP_FQDN to wait for readiness and register schemas from the deployer's machine; that flow will fail for enablePrivateNetworking=true. Consider updating the post-deployment workflow to call the API through the web app's new /api proxy (using CONTAINER_WEB_APP_FQDN) or to execute the registration from within the VNet (e.g., via the jumpbox).

Suggested change
ingressExternal: enablePrivateNetworking ? false : true
ingressExternal: true

Copilot uses AI. Check for mistakes.
activeRevisionsMode: 'Single'
ingressTransport: 'auto'
corsPolicy: {
Expand Down Expand Up @@ -1219,6 +1220,10 @@ module avmContainerApp_Web 'br/public:avm/res/app/container-app:0.22.1' = {
env: [
{
name: 'APP_API_BASE_URL'
value: enablePrivateNetworking ? '/api' : 'https://${avmContainerApp_API.outputs.fqdn}'
}
{
name: 'APP_BACKEND_API_URL'
value: 'https://${avmContainerApp_API.outputs.fqdn}'
}
{
Expand Down Expand Up @@ -1841,7 +1846,7 @@ module avmContainerApp_API_update 'br/public:avm/res/app/container-app:0.22.1' =
}
]
}
ingressExternal: true
ingressExternal: enablePrivateNetworking ? false : true
activeRevisionsMode: 'Single'
ingressTransport: 'auto'
corsPolicy: {
Expand Down Expand Up @@ -1979,5 +1984,8 @@ output AZURE_CONTAINER_REGISTRY_ENDPOINT string = avmContainerRegistry.outputs.l
@description('The name of the Content Understanding AI Services account.')
output CONTENT_UNDERSTANDING_ACCOUNT_NAME string = avmAiServices_cu.outputs.name

@description('Whether private networking (WAF) is enabled.')
output ENABLE_PRIVATE_NETWORKING bool = enablePrivateNetworking

@description('The resource group the resources were deployed into.')
output AZURE_RESOURCE_GROUP string = resourceGroup().name
17 changes: 16 additions & 1 deletion infra/scripts/post_deployment.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,26 @@ Write-Host " [Link] Portal URL: $WORKFLOW_APP_PORTAL_URL"

Write-Host ""
Write-Host "[Package] Registering schemas and creating schema set..."

# Check if private networking (WAF) is enabled
$ENABLE_PRIVATE_NETWORKING = $null
try {
$ENABLE_PRIVATE_NETWORKING = azd env get-value ENABLE_PRIVATE_NETWORKING 2>$null
} catch { }

# When private networking is enabled, the API is internal-only (ingressExternal=false).
# Use the web app's /api proxy to reach the backend through same-origin routing.
if ($ENABLE_PRIVATE_NETWORKING -eq "true") {
Write-Host " [Info] Private networking (WAF) is enabled. Using web app /api proxy to reach backend."
$ApiBaseUrl = "https://$CONTAINER_WEB_APP_FQDN/api"
} else {
$ApiBaseUrl = "https://$CONTAINER_API_APP_FQDN"
}

Write-Host " [Wait] Waiting for API to be ready..."

$MaxRetries = 10
$RetryInterval = 15
$ApiBaseUrl = "https://$CONTAINER_API_APP_FQDN"
$ApiReady = $false

for ($i = 1; $i -le $MaxRetries; $i++) {
Expand Down
14 changes: 13 additions & 1 deletion infra/scripts/post_deployment.sh
Original file line number Diff line number Diff line change
Expand Up @@ -60,11 +60,23 @@ echo " 🔗 Portal URL: $WORKFLOW_APP_PORTAL_URL"

echo ""
echo "📦 Registering schemas and creating schema set..."

# Check if private networking (WAF) is enabled
ENABLE_PRIVATE_NETWORKING=$(azd env get-value ENABLE_PRIVATE_NETWORKING 2>/dev/null || echo "")

# When private networking is enabled, the API is internal-only (ingressExternal=false).
# Use the web app's /api proxy to reach the backend through same-origin routing.
if [ "$ENABLE_PRIVATE_NETWORKING" = "true" ]; then
echo " ℹ️ Private networking (WAF) is enabled. Using web app /api proxy to reach backend."
API_BASE_URL="https://$CONTAINER_WEB_APP_FQDN/api"
else
API_BASE_URL="https://$CONTAINER_API_APP_FQDN"
fi

echo " ⏳ Waiting for API to be ready..."

MAX_RETRIES=10
RETRY_INTERVAL=15
API_BASE_URL="https://$CONTAINER_API_APP_FQDN"

for i in $(seq 1 $MAX_RETRIES); do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" "$API_BASE_URL/schemavault/" 2>/dev/null || echo "000")
Expand Down
7 changes: 7 additions & 0 deletions src/ContentProcessorWeb/env.sh
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
#!/bin/sh

# Ensure APP_BACKEND_API_URL has a safe default so nginx can always start.
# When not set, the /api/ proxy_pass will point to a non-routable placeholder
# and return 502, which is acceptable — the direct API path still works.
export APP_BACKEND_API_URL="${APP_BACKEND_API_URL:-http://localhost:8080}"

for i in $(env | grep ^APP_)
do
key=$(echo $i | cut -d '=' -f 1)
value=$(echo $i | cut -d '=' -f 2-)
echo $key=$value
# Use sed to replace only the exact matches of the key
find /usr/share/nginx/html -type f -exec sed -i "s|\b${key}\b|${value}|g" '{}' +
sed -i "s|\b${key}\b|${value}|g" /etc/nginx/nginx.conf
done
echo 'done'
21 changes: 21 additions & 0 deletions src/ContentProcessorWeb/nginx-custom.conf
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,31 @@ http {
types_hash_max_size 2048;
types_hash_bucket_size 128;

# Allow large file uploads through the /api proxy
client_max_body_size 100m;

server {
listen 3000;
server_name localhost;

# Route browser API calls through the web container so private backend
# endpoints remain internal-only in WAF/private networking deployments.
# APP_BACKEND_API_URL is substituted at runtime by env.sh; if unset, this
# block is effectively a no-op (returns 502) which is safe for nginx startup.
location /api/ {
proxy_http_version 1.1;
proxy_set_header Host $proxy_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 60s;
proxy_send_timeout 120s;
proxy_read_timeout 120s;
proxy_pass APP_BACKEND_API_URL/;
}
Comment on lines +21 to +31

Copilot AI Apr 22, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

location /api/ relies on proxy_pass APP_BACKEND_API_URL/; being substituted at runtime. If APP_BACKEND_API_URL is not set in the container environment, env.sh will not replace the placeholder and nginx will fail to start due to an invalid proxy_pass URL. Ensure all deployment paths that run this image always set APP_BACKEND_API_URL (even when the app is not using the /api proxy), or adjust the nginx config/entrypoint to provide a safe default when the variable is missing.

Copilot uses AI. Check for mistakes.

location / {
root /usr/share/nginx/html;
try_files $uri $uri/ /index.html;
Expand Down
Loading