Skip to content

Commit fd2e78a

Browse files
Merge branch 'dev' of https://github.com/microsoft/content-processing-solution-accelerator into us-42666-pr565-followup
2 parents 3850d56 + 1fbb362 commit fd2e78a

16 files changed

Lines changed: 667 additions & 3610 deletions

.github/workflows/deploy.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,6 @@ jobs:
146146
--parameters \
147147
solutionName="${{ env.ENVIRONMENT_NAME }}" \
148148
enablePrivateNetworking="false" \
149-
contentUnderstandingLocation="WestUS" \
150149
deploymentType="GlobalStandard" \
151150
gptModelName="gpt-5.1" \
152151
gptModelVersion="2025-11-13" \

.github/workflows/validate-bicep-params.yml

Lines changed: 19 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,9 +33,16 @@ jobs:
3333
- name: Validate infra/ parameters
3434
id: validate_infra
3535
continue-on-error: true
36+
env:
37+
ACCELERATOR_NAME: ${{ env.accelerator_name }}
3638
run: |
3739
set +e
38-
python infra/scripts/validate_bicep_params.py --dir infra --strict --no-color --json-output infra_results.json 2>&1 | tee infra_output.txt
40+
RUN_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
41+
python infra/scripts/validate_bicep_params.py --dir infra --strict --no-color \
42+
--json-output infra_results.json \
43+
--html-output email_body.html \
44+
--accelerator-name "${ACCELERATOR_NAME}" \
45+
--run-url "${RUN_URL}" 2>&1 | tee infra_output.txt
3946
EXIT_CODE=${PIPESTATUS[0]}
4047
set -e
4148
echo "## Infra Param Validation" >> "$GITHUB_STEP_SUMMARY"
@@ -60,24 +67,23 @@ jobs:
6067
name: bicep-validation-results
6168
path: |
6269
infra_results.json
70+
email_body.html
6371
retention-days: 30
6472

6573
- name: Send schedule notification on failure
6674
if: github.event_name == 'schedule' && steps.result.outputs.status == 'failure'
6775
env:
6876
LOGICAPP_URL: ${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}
69-
GITHUB_REPOSITORY: ${{ github.repository }}
70-
GITHUB_RUN_ID: ${{ github.run_id }}
7177
ACCELERATOR_NAME: ${{ env.accelerator_name }}
7278
run: |
73-
RUN_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
74-
INFRA_OUTPUT=$(sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g' infra_output.txt)
79+
if [ ! -f email_body.html ]; then
80+
echo "<p>Email body was not generated. Please check the workflow logs.</p>" > email_body.html
81+
fi
7582
7683
jq -n \
7784
--arg name "${ACCELERATOR_NAME}" \
78-
--arg infra "$INFRA_OUTPUT" \
79-
--arg url "$RUN_URL" \
80-
'{subject: ("Bicep Parameter Validation Report - " + $name + " - Issues Detected"), body: ("<p>Dear Team,</p><p>The scheduled <strong>Bicep Parameter Validation</strong> for <strong>" + $name + "</strong> has detected parameter mapping errors.</p><p><strong>infra/ Results:</strong></p><pre>" + $infra + "</pre><p><strong>Run URL:</strong> <a href=\"" + $url + "\">" + $url + "</a></p><p>Please fix the parameter mapping issues at your earliest convenience.</p><p>Best regards,<br>Your Automation Team</p>")}' \
85+
--rawfile body email_body.html \
86+
'{subject: ("Bicep Parameter Validation Report - " + $name + " - Issues Detected"), body: $body}' \
8187
| curl -X POST "${LOGICAPP_URL}" \
8288
-H "Content-Type: application/json" \
8389
-d @- || echo "Failed to send notification"
@@ -86,18 +92,16 @@ jobs:
8692
if: github.event_name == 'schedule' && steps.result.outputs.status == 'success'
8793
env:
8894
LOGICAPP_URL: ${{ secrets.EMAILNOTIFICATION_LOGICAPP_URL_TA }}
89-
GITHUB_REPOSITORY: ${{ github.repository }}
90-
GITHUB_RUN_ID: ${{ github.run_id }}
9195
ACCELERATOR_NAME: ${{ env.accelerator_name }}
9296
run: |
93-
RUN_URL="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}"
94-
INFRA_OUTPUT=$(sed 's/&/\&amp;/g; s/</\&lt;/g; s/>/\&gt;/g' infra_output.txt)
97+
if [ ! -f email_body.html ]; then
98+
echo "<p>Email body was not generated. Please check the workflow logs.</p>" > email_body.html
99+
fi
95100
96101
jq -n \
97102
--arg name "${ACCELERATOR_NAME}" \
98-
--arg infra "$INFRA_OUTPUT" \
99-
--arg url "$RUN_URL" \
100-
'{subject: ("Bicep Parameter Validation Report - " + $name + " - Passed"), body: ("<p>Dear Team,</p><p>The scheduled <strong>Bicep Parameter Validation</strong> for <strong>" + $name + "</strong> has completed successfully. All parameter mappings are valid.</p><p><strong>infra/ Results:</strong></p><pre>" + $infra + "</pre><p><strong>Run URL:</strong> <a href=\"" + $url + "\">" + $url + "</a></p><p>Best regards,<br>Your Automation Team</p>")}' \
103+
--rawfile body email_body.html \
104+
'{subject: ("Bicep Parameter Validation Report - " + $name + " - Passed"), body: $body}' \
101105
| curl -X POST "${LOGICAPP_URL}" \
102106
-H "Content-Type: application/json" \
103107
-d @- || echo "Failed to send notification"

docs/CustomizingAzdParameters.md

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,15 @@ By default this template will use the environment name as the prefix to prevent
1111
| -------------------------------------- | ------- | --------------------------- | ------------------------------------------------------------------------------------- |
1212
| `AZURE_ENV_NAME` | string | `cps` | Sets the environment name prefix for all Azure resources (3-20 characters). |
1313
| `AZURE_LOCATION` | string | `eastus2` | Sets the primary Azure region for resource deployment. Allowed: `australiaeast`, `centralus`, `eastasia`, `eastus2`, `japaneast`, `northeurope`, `southeastasia`, `uksouth`. |
14-
| `AZURE_ENV_CU_LOCATION` | string | `WestUS` | Sets the location for the Azure AI Content Understanding service. Allowed: `WestUS`, `SwedenCentral`, `AustraliaEast`. |
15-
| `AZURE_ENV_AI_SERVICE_LOCATION` | string | `eastus` | Sets the location for Azure AI Services (OpenAI) deployment. |
16-
| `AZURE_ENV_MODEL_DEPLOYMENT_TYPE` | string | `GlobalStandard` | Defines the model deployment type. Allowed: `Standard`, `GlobalStandard`. |
14+
| `AZURE_ENV_AI_SERVICE_LOCATION` | string | `eastus2` | Sets the location for Azure AI Services. This single account hosts both Azure OpenAI and Content Understanding. Allowed: `australiaeast`, `eastus`, `eastus2`, `japaneast`, `southcentralus`, `southeastasia`, `swedencentral`, `uksouth`, `westeurope`, `westus`, `westus3`. |
15+
| `AZURE_ENV_MODEL_DEPLOYMENT_TYPE` | string | `GlobalStandard` | Defines the model deployment type. Allowed: `Standard`, `GlobalStandard`.<br>**Note:** the `azd` location-picker filters regions using the `usageName` metadata on `azureAiServiceLocation` in `infra/main.bicep` (currently `OpenAI.GlobalStandard.gpt-5.1,300`). If you set this parameter to `Standard`, also edit that metadata to `OpenAI.Standard.gpt-5.1,300` so the picker shows the correct subset of regions. |
1716
| `AZURE_ENV_GPT_MODEL_NAME` | string | `gpt-5.1` | Specifies the GPT model name. Default: `gpt-5.1`. |
1817
| `AZURE_ENV_GPT_MODEL_VERSION` | string | `2025-11-13` | Specifies the GPT model version. |
1918
| `AZURE_ENV_GPT_MODEL_CAPACITY` | integer | `300` | Sets the model capacity (minimum 1). Default: 300. Optimal: 500 for multi-document claim processing. |
2019
| `AZURE_ENV_CONTAINER_REGISTRY_ENDPOINT` | string | `cpscontainerreg.azurecr.io` | Sets the public container image endpoint for pulling pre-built images. |
2120
| `AZURE_ENV_IMAGETAG` | string | `latest_v2` | Sets the container image tag (e.g., `latest_v2`, `dev`, `demo`, `hotfix`). |
22-
| `AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID` | string | Guide to get your [Existing Workspace Resource ID](/docs/re-use-log-analytics.md) | Reuses an existing Log Analytics Workspace instead of provisioning a new one. |
23-
| `AZURE_EXISTING_AIPROJECT_RESOURCE_ID` | string | Guide to get your [Existing AI Project Resource ID](/docs/re-use-foundry-project.md) | Reuses an existing AI Foundry and AI Foundry Project instead of creating a new one. |
21+
| `AZURE_ENV_EXISTING_LOG_ANALYTICS_WORKSPACE_RID` | string | Guide to get your [Existing Workspace Resource ID](re-use-log-analytics.md) | Reuses an existing Log Analytics Workspace instead of provisioning a new one. |
22+
| `AZURE_EXISTING_AIPROJECT_RESOURCE_ID` | string | Guide to get your [Existing AI Project Resource ID](re-use-foundry-project.md) | Reuses an existing AI Foundry and AI Foundry Project instead of creating a new one. |
2423
| `AZURE_ENV_VM_SIZE` | string | `Standard_D2s_v5` | Overrides the jumpbox VM size (private networking only). Default: `Standard_D2s_v5`. |
2524

2625
## How to Set a Parameter

docs/LocalDevelopmentSetup.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,7 @@ Example resource names from deployment:
160160
- App Configuration: `appcs-{suffix}.azconfig.io`
161161
- Cosmos DB: `cosmos-{suffix}.documents.azure.com`
162162
- Storage Account: `st{suffix}.queue.core.windows.net`
163-
- Content Understanding: `aicu-{suffix}.cognitiveservices.azure.com`
163+
- Content Understanding: `aif-{suffix}.cognitiveservices.azure.com`
164164

165165
### Required Azure RBAC Permissions
166166

docs/TroubleShootingSteps.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -128,7 +128,7 @@ Use these as quick reference guides to unblock your deployments.
128128
| **RouteTableCannotBeAttachedForAzureBastionSubnet** | Route table attached to Azure Bastion subnet | This error occurs because Azure Bastion subnet (`AzureBastionSubnet`) has a platform restriction that prevents route tables from being attached.<br><br>**How to reproduce:**<br><ul><li>In `virtualNetwork.bicep`, add `attachRouteTable: true` to the `AzureBastionSubnet` configuration:<br>`{ name: 'AzureBastionSubnet', addressPrefixes: ['10.0.10.0/26'], attachRouteTable: true }`</li><li>Add a Route Table module to the template</li><li>Update subnet creation to attach route table conditionally:<br>`routeTableResourceId: subnet.?attachRouteTable == true ? routeTable.outputs.resourceId : null`</li><li>Deploy the template → Azure throws `RouteTableCannotBeAttachedForAzureBastionSubnet`</li></ul><br>**Resolution:**<br><ul><li>Remove the `attachRouteTable: true` flag from `AzureBastionSubnet` configuration</li><li>Ensure no route table is associated with `AzureBastionSubnet`</li><li>Route tables can only be attached to other subnets, not `AzureBastionSubnet`</li><li>For more details, refer to [Azure Bastion subnet requirements](https://learn.microsoft.com/en-us/azure/bastion/configuration-settings#subnet)</li></ul> |
129129
| **VMSizeIsNotPermittedToEnableAcceleratedNetworking** | VM size does not support accelerated networking | This error occurs when you attempt to enable accelerated networking on a VM size that does not support it. This deployment's jumpbox VM **requires** accelerated networking.<br><br>**Default VM size:** `Standard_D2s_v5` — supports accelerated networking.<br><br>**How this error happens:**<br><ul><li>You override the VM size (via `AZURE_ENV_VM_SIZE`) with a size that doesn't support accelerated networking (e.g., `Standard_A2m_v2`, A-series, or B-series VMs)</li><li>Azure rejects the deployment with `VMSizeIsNotPermittedToEnableAcceleratedNetworking`</li></ul><br>**Resolution:**<br><ul><li>Use the default `Standard_D2s_v5` (recommended)</li><li>If overriding VM size, choose one that supports accelerated networking:<br>`Standard_D2s_v4`, `Standard_D2as_v5` (AMD), `Standard_D2s_v3`</li><li>Verify VM size supports accelerated networking:<br>`az vm list-skus --location <region> --size <vm-size> --query "[?capabilities[?name=='AcceleratedNetworkingEnabled' && value=='True']]"`</li><li>Avoid A-series and B-series VMs — they do not support accelerated networking</li><li>See [VM sizes with accelerated networking](https://learn.microsoft.com/en-us/azure/virtual-network/accelerated-networking-overview)</li></ul> |
130130
| **NetworkSecurityGroupNotCompliantForAzureBastionSubnet** / **SecurityRuleParameterContainsUnsupportedValue** | NSG rules blocking required Azure Bastion ports | This error occurs when the Network Security Group (NSG) attached to `AzureBastionSubnet` explicitly denies inbound TCP ports 443 and/or 4443, which Azure Bastion requires for management and tunneling.<br><br>**How to reproduce:**<br><ul><li>Deploy the template with `enablePrivateNetworking=true` so the virtualNetwork module creates `AzureBastionSubnet` and a Network Security Group that denies ports 443 and 4443</li><li>Attempt to deploy Azure Bastion into that subnet</li><li>During validation, Bastion detects the deny rules and fails with `NetworkSecurityGroupNotCompliantForAzureBastionSubnet`</li></ul><br>**Resolution:**<br><ul><li>**Remove or modify deny rules** for ports 443 and 4443 in the NSG attached to `AzureBastionSubnet`</li><li>**Ensure required inbound rules** per [Azure Bastion NSG requirements](https://learn.microsoft.com/en-us/azure/bastion/bastion-nsg)</li><li>**Use Bicep conditions** to skip NSG attachments for `AzureBastionSubnet` if deploying Bastion</li><li>**Validate the NSG configuration** before deploying Bastion into the subnet</li></ul> |
131-
| **403 Forbidden - Content Understanding** | Azure AI Content Understanding returns 403 Forbidden in WAF (private networking) deployment | This error occurs when the **Azure AI Content Understanding** service returns a `403 Forbidden` response during document processing in a **WAF-enabled (private networking)** deployment.<br><br>**Why this happens:**<br>In WAF deployments (`enablePrivateNetworking=true`), the Content Understanding AI Services account (`aicu-<suffix>`) is configured with `publicNetworkAccess: Disabled`. All traffic must flow through the **private endpoint** (`pep-aicu-<suffix>`) and resolve via private DNS zones (`privatelink.cognitiveservices.azure.com`, `privatelink.services.ai.azure.com`, `privatelink.contentunderstanding.ai.azure.com`). If any part of this chain is misconfigured, the request either reaches the public endpoint (which is blocked) or fails to route entirely, resulting in a 403.<br><br>**Common causes:**<br><ul><li>Private DNS zones not linked to the VNet — DNS resolution falls back to the public IP, which is blocked</li><li>Private endpoint connection is not in **Approved** state</li><li>Content Understanding is deployed in a different region (`contentUnderstandingLocation`, defaults to `WestUS`) than the main deployment — the private endpoint still works cross-region, but DNS misconfiguration is more likely</li><li>Container Apps are not injected into the VNet or are on a subnet that cannot reach the private endpoint</li><li>Managed Identity used by the Container App does not have the required **Cognitive Services User** role on the Content Understanding resource</li></ul><br>**Resolution:**<br><ul><li>**Verify private endpoint status:**<br>`az network private-endpoint show --name pep-aicu-<suffix> --resource-group <rg-name> --query "privateLinkServiceConnections[0].privateLinkServiceConnectionState.status"`<br>Expected: `Approved`</li><li>**Verify private DNS zone VNet links:**<br>`az network private-dns zone list --resource-group <rg-name> -o table`<br>Ensure `privatelink.cognitiveservices.azure.com`, `privatelink.services.ai.azure.com`, and `privatelink.contentunderstanding.ai.azure.com` all have VNet links</li><li>**Test DNS resolution from the jumpbox VM** (inside the VNet):<br>`nslookup aicu-<suffix>.cognitiveservices.azure.com`<br>Should resolve to a private IP (e.g., `10.x.x.x`), NOT a public IP</li><li>**Verify RBAC role assignments:** Ensure the Container App managed identity has **Cognitive Services User** role on the Content Understanding resource:<br>`az role assignment list --scope /subscriptions/<sub-id>/resourceGroups/<rg-name>/providers/Microsoft.CognitiveServices/accounts/aicu-<suffix> --query "[?roleDefinitionName=='Cognitive Services User']" -o table`</li><li>**Check Container App VNet integration:** Confirm the Container App Environment is deployed into the VNet and can reach the backend subnet where the private endpoint resides</li><li>**Redeploy if needed:**<br>`azd up`</li></ul><br>**Reference:**<br><ul><li>[Configure private endpoints for Azure AI Services](https://learn.microsoft.com/en-us/azure/ai-services/cognitive-services-virtual-networks)</li><li>[Azure Private DNS zones](https://learn.microsoft.com/en-us/azure/dns/private-dns-overview)</li></ul> |
131+
| **403 Forbidden - Content Understanding** | Azure AI Content Understanding returns 403 Forbidden in WAF (private networking) deployment | This error occurs when the **Azure AI Content Understanding** API on the unified AI Services account (`aif-<suffix>`) returns a `403 Forbidden` response during document processing in a **WAF-enabled (private networking)** deployment.<br><br>**Why this happens:**<br>As of the CU GA migration, Content Understanding shares the same Azure AI Services account as Azure OpenAI (`aif-<suffix>`). In WAF deployments (`enablePrivateNetworking=true`), that account is configured with `publicNetworkAccess: Disabled`. All traffic must flow through the unified private endpoint (`pep-aiservices-<suffix>`) and resolve via four private DNS zones: `privatelink.cognitiveservices.azure.com`, `privatelink.openai.azure.com`, `privatelink.services.ai.azure.com`, and `privatelink.contentunderstanding.ai.azure.com`. If any link in this chain is misconfigured, the request either reaches the public endpoint (blocked) or fails to route, resulting in a 403.<br><br>**Common causes:**<br><ul><li>Private DNS zones not linked to the VNet — DNS resolution falls back to the public IP, which is blocked</li><li>Private endpoint connection is not in **Approved** state</li><li>Container Apps are not injected into the VNet or are on a subnet that cannot reach the private endpoint</li><li>Managed Identity used by the Container App does not have the required **Cognitive Services User** role on the unified AI Services account</li><li>Reusing an existing AI Foundry project (`existingFoundryProjectResourceId`): the repo no longer creates a CU-specific PE; the existing account must have its own private endpoint covering the four DNS zones above</li></ul><br>**Resolution:**<br><ul><li>**Verify private endpoint status:**<br>`az network private-endpoint show --name pep-aiservices-<suffix> --resource-group <rg-name> --query "privateLinkServiceConnections[0].privateLinkServiceConnectionState.status"`<br>Expected: `Approved`</li><li>**Verify private DNS zone VNet links:**<br>`az network private-dns zone list --resource-group <rg-name> -o table`<br>Ensure `privatelink.cognitiveservices.azure.com`, `privatelink.openai.azure.com`, `privatelink.services.ai.azure.com`, and `privatelink.contentunderstanding.ai.azure.com` all have VNet links</li><li>**Test DNS resolution from the jumpbox VM** (inside the VNet):<br>`nslookup aif-<suffix>.cognitiveservices.azure.com`<br>Should resolve to a private IP (e.g., `10.x.x.x`), NOT a public IP</li><li>**Verify RBAC role assignments:** ensure the Container App managed identity has **Cognitive Services User** role on the unified account:<br>`az role assignment list --scope /subscriptions/<sub-id>/resourceGroups/<rg-name>/providers/Microsoft.CognitiveServices/accounts/aif-<suffix> --query "[?roleDefinitionName=='Cognitive Services User']" -o table`</li><li>**Check Container App VNet integration:** confirm the Container App Environment is deployed into the VNet and can reach the backend subnet where the private endpoint resides</li><li>**Redeploy if needed:**<br>`azd up`</li></ul><br>**Reference:**<br><ul><li>[Configure private endpoints for Azure AI Services](https://learn.microsoft.com/en-us/azure/ai-services/cognitive-services-virtual-networks)</li><li>[Azure Private DNS zones](https://learn.microsoft.com/en-us/azure/dns/private-dns-overview)</li></ul> |
132132

133133
---------------------------------
134134

0 commit comments

Comments
 (0)