Skip to content

Commit a2f83bd

Browse files
Merge pull request #531 from microsoft/dev
chore: Dev merge to main
2 parents 70a2d6f + 5474f83 commit a2f83bd

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

42 files changed

+284
-105
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
name: AZD Template Validation
2+
on:
3+
schedule:
4+
- cron: '30 1 * * 4' # Every Thursday at 7:00 AM IST (1:30 AM UTC)
5+
workflow_dispatch:
6+
7+
permissions:
8+
contents: read
9+
id-token: write
10+
pull-requests: write
11+
12+
jobs:
13+
template_validation:
14+
runs-on: ubuntu-latest
15+
name: azd template validation
16+
environment: production
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Set timestamp
21+
run: echo "HHMM=$(date -u +'%H%M')" >> $GITHUB_ENV
22+
23+
- uses: microsoft/template-validation-action@v0.4.3
24+
with:
25+
validateAzd: ${{ vars.TEMPLATE_VALIDATE_AZD }}
26+
validateTests: ${{ vars.TEMPLATE_VALIDATE_TESTS }}
27+
useDevContainer: ${{ vars.TEMPLATE_USE_DEV_CONTAINER }}
28+
id: validation
29+
env:
30+
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
31+
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
32+
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
33+
AZURE_ENV_NAME: azd-${{ vars.AZURE_ENV_NAME }}-${{ env.HHMM }}
34+
AZURE_LOCATION: ${{ vars.AZURE_LOCATION }}
35+
AZURE_ENV_AI_SERVICE_LOCATION: ${{ vars.AZURE_LOCATION }}
36+
AZURE_ENV_MODEL_CAPACITY: 1 # keep low to avoid potential quota issues
37+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
38+
39+
- name: print result
40+
run: cat ${{ steps.validation.outputs.resultFile }}

.github/workflows/azure-dev.yaml

Lines changed: 46 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,57 @@
1-
name: Azure Template Validation
1+
name: Azure Dev Deploy
2+
23
on:
34
workflow_dispatch:
45

56
permissions:
67
contents: read
78
id-token: write
8-
pull-requests: write
99

1010
jobs:
11-
template_validation_job:
12-
environment: production
11+
deploy:
1312
runs-on: ubuntu-latest
14-
name: Template validation
15-
13+
environment: production
14+
env:
15+
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
16+
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
17+
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
18+
AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }}
19+
AZURE_LOCATION: ${{ vars.AZURE_LOCATION }}
20+
AZURE_ENV_MODEL_CAPACITY: 1 # keep low to avoid potential quota issues
21+
AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }}
1622
steps:
17-
# Step 1: Checkout the code from your repository
18-
- name: Checkout code
19-
uses: actions/checkout@v5
20-
21-
# Step 2: Validate the Azure template using microsoft/template-validation-action
22-
- name: Validate Azure Template
23-
uses: microsoft/template-validation-action@v0.4.3
24-
id: validation
23+
- name: Checkout Code
24+
uses: actions/checkout@v4
25+
26+
- name: Set timestamp and env name
27+
run: |
28+
HHMM=$(date -u +'%H%M')
29+
echo "AZURE_ENV_NAME=azd-${{ vars.AZURE_ENV_NAME }}-${HHMM}" >> $GITHUB_ENV
30+
31+
- name: Install azd
32+
uses: Azure/setup-azd@v2
33+
34+
- name: Login to Azure
35+
uses: azure/login@v2
2536
with:
26-
useDevContainer: false
27-
env:
28-
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
29-
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
30-
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
31-
AZURE_ENV_NAME: ${{ secrets.AZURE_ENV_NAME }}
32-
AZURE_LOCATION: ${{ secrets.AZURE_LOCATION }}
33-
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
34-
AZURE_DEV_COLLECT_TELEMETRY: ${{ vars.AZURE_DEV_COLLECT_TELEMETRY }}
35-
36-
# Step 3: Print the result of the validation
37-
- name: Print result
38-
run: cat ${{ steps.validation.outputs.resultFile }}
37+
client-id: ${{ secrets.AZURE_CLIENT_ID }}
38+
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
39+
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
40+
41+
- name: Login to AZD
42+
shell: bash
43+
run: |
44+
azd auth login \
45+
--client-id "$AZURE_CLIENT_ID" \
46+
--federated-credential-provider "github" \
47+
--tenant-id "$AZURE_TENANT_ID"
48+
49+
- name: Provision and Deploy
50+
shell: bash
51+
run: |
52+
if ! azd env select "$AZURE_ENV_NAME"; then
53+
azd env new "$AZURE_ENV_NAME" --subscription "$AZURE_SUBSCRIPTION_ID" --location "$AZURE_LOCATION" --no-prompt
54+
fi
55+
azd config set defaults.subscription "$AZURE_SUBSCRIPTION_ID"
56+
azd env set AZURE_ENV_AI_SERVICE_LOCATION="$AZURE_LOCATION"
57+
azd up --no-prompt

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -278,6 +278,8 @@ Follow the quick deploy steps on the deployment guide to deploy this solution
278278

279279
<br/>
280280

281+
> **Note**: Some tenants may have additional security restrictions that run periodically and could impact the application (e.g., blocking public network access). If you experience issues or the application stops working, check if these restrictions are the cause. In such cases, consider deploying the WAF-supported version to ensure compliance. To configure, [Click here](./docs/DeploymentGuide.md#31-choose-deployment-type-optional).
282+
281283
> ⚠️ **Important: Check Azure OpenAI Quota Availability**
282284
<br/>To ensure sufficient quota is available in your subscription, please follow [quota check instructions guide](./docs/quota_check.md) before you deploy the solution.
283285

azure.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ name: content-processing
55

66
requiredVersions:
77
azd: '>= 1.18.0 != 1.23.9'
8+
bicep: '>= 0.33.0'
89

910
metadata:
1011
template: content-processing@1.0

docs/DeploymentGuide.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ This guide walks you through deploying the Content Processing Solution Accelerat
66

77
🆘 **Need Help?** If you encounter any issues during deployment, check our [Troubleshooting Guide](./TroubleShootingSteps.md) for solutions to common problems.
88

9+
> **Note**: Some tenants may have additional security restrictions that run periodically and could impact the application (e.g., blocking public network access). If you experience issues or the application stops working, check if these restrictions are the cause. In such cases, consider deploying the WAF-supported version to ensure compliance. To configure, [Click here](#31-choose-deployment-type-optional).
10+
911
## Step 1: Prerequisites & Setup
1012

1113
### 1.1 Azure Account Requirements

docs/TroubleShootingSteps.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Use these as quick reference guides to unblock your deployments.
2727
| **InternalSubscriptionIsOverQuotaForSku** | Subscription quota exceeded for the requested SKU | [View Solution](#quota--capacity-limitations) |
2828
| **InvalidResourceGroup** | Invalid resource group configuration | [View Solution](#resource-group--deployment-management) |
2929
| **RequestDisallowedByPolicy** | Azure Policy blocking the requested operation | [View Solution](#subscription--access-issues) |
30+
| **403 Forbidden - Content Understanding** | Content Understanding returns 403 in WAF/private networking deployment | [View Solution](#network--infrastructure-configuration) |
3031

3132
## 📖 Table of Contents
3233

@@ -127,6 +128,7 @@ Use these as quick reference guides to unblock your deployments.
127128
| **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> |
128129
| **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> |
129130
| **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> |
130132

131133
---------------------------------
132134

infra/main.bicep

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1890,5 +1890,8 @@ output CONTAINER_REGISTRY_NAME string = avmContainerRegistry.outputs.name
18901890
@description('The login server of the Azure Container Registry.')
18911891
output CONTAINER_REGISTRY_LOGIN_SERVER string = avmContainerRegistry.outputs.loginServer
18921892

1893+
@description('The name of the Content Understanding AI Services account.')
1894+
output CONTENT_UNDERSTANDING_ACCOUNT_NAME string = avmAiServices_cu.outputs.name
1895+
18931896
@description('The resource group the resources were deployed into.')
18941897
output AZURE_RESOURCE_GROUP string = resourceGroup().name

infra/scripts/post_deployment.sh

Lines changed: 33 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,8 @@ else
9898

9999
# Read schema entries from manifest
100100
SCHEMA_COUNT=$(cat "$SCHEMA_INFO_FILE" | grep -o '"File"' | wc -l)
101-
REGISTERED_IDS=""
102-
REGISTERED_NAMES=""
101+
REGISTERED_IDS=()
102+
REGISTERED_NAMES=()
103103

104104
for idx in $(seq 0 $((SCHEMA_COUNT - 1))); do
105105
# Parse entry fields using grep/sed (no python needed)
@@ -128,8 +128,8 @@ else
128128

129129
if [ -n "$EXISTING_ID" ]; then
130130
echo " Schema '$CLASS_NAME' already exists with ID: $EXISTING_ID"
131-
REGISTERED_IDS="$REGISTERED_IDS $EXISTING_ID"
132-
REGISTERED_NAMES="$REGISTERED_NAMES $CLASS_NAME"
131+
REGISTERED_IDS+=("$EXISTING_ID")
132+
REGISTERED_NAMES+=("$CLASS_NAME")
133133
continue
134134
fi
135135

@@ -148,8 +148,8 @@ else
148148
if [ "$HTTP_CODE" = "200" ]; then
149149
SCHEMA_ID=$(echo "$BODY" | sed 's/.*"Id"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/')
150150
echo " Successfully registered: $DESCRIPTION's Schema Id - $SCHEMA_ID"
151-
REGISTERED_IDS="$REGISTERED_IDS $SCHEMA_ID"
152-
REGISTERED_NAMES="$REGISTERED_NAMES $CLASS_NAME"
151+
REGISTERED_IDS+=("$SCHEMA_ID")
152+
REGISTERED_NAMES+=("$CLASS_NAME")
153153
else
154154
echo " Failed to upload '$FILE_NAME'. HTTP Status: $HTTP_CODE"
155155
echo " Error Response: $BODY"
@@ -205,10 +205,9 @@ else
205205
ALREADY_IN_SET=$(curl -s "${SCHEMASETVAULT_URL}${SCHEMASET_ID}/schemas" 2>/dev/null || echo "[]")
206206

207207
# Iterate over registered schemas
208-
IDX=0
209-
for SCHEMA_ID in $REGISTERED_IDS; do
210-
IDX=$((IDX + 1))
211-
CLASS_NAME=$(echo "$REGISTERED_NAMES" | tr ' ' '\n' | sed -n "${IDX}p")
208+
for i in "${!REGISTERED_IDS[@]}"; do
209+
SCHEMA_ID="${REGISTERED_IDS[$i]}"
210+
CLASS_NAME="${REGISTERED_NAMES[$i]}"
212211

213212
if echo "$ALREADY_IN_SET" | grep -q "\"Id\"[[:space:]]*:[[:space:]]*\"$SCHEMA_ID\""; then
214213
echo " Schema '$CLASS_NAME' ($SCHEMA_ID) already in schema set - skipped"
@@ -236,5 +235,29 @@ else
236235
echo ""
237236
echo "============================================================"
238237
echo "Schema registration process completed."
238+
echo " Schemas registered: ${#REGISTERED_IDS[@]}"
239239
echo "============================================================"
240240
fi
241+
242+
# --- Refresh Content Understanding Cognitive Services account ---
243+
echo ""
244+
echo "============================================================"
245+
echo "Refreshing Content Understanding Cognitive Services account..."
246+
echo "============================================================"
247+
248+
CU_ACCOUNT_NAME=$(azd env get-value CONTENT_UNDERSTANDING_ACCOUNT_NAME 2>/dev/null || echo "")
249+
250+
if [ -z "$CU_ACCOUNT_NAME" ]; then
251+
echo " ⚠️ CONTENT_UNDERSTANDING_ACCOUNT_NAME not found in azd env. Skipping refresh."
252+
else
253+
echo " Refreshing account: $CU_ACCOUNT_NAME in resource group: $RESOURCE_GROUP"
254+
if az cognitiveservices account update \
255+
-g "$RESOURCE_GROUP" \
256+
-n "$CU_ACCOUNT_NAME" \
257+
--tags refresh=true \
258+
--output none; then
259+
echo " ✅ Successfully refreshed Cognitive Services account '$CU_ACCOUNT_NAME'."
260+
else
261+
echo " ❌ Failed to refresh Cognitive Services account '$CU_ACCOUNT_NAME'."
262+
fi
263+
fi

infra/scripts/validate_bicep_params.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,9 @@ def parse_parameters_env_vars(json_path: Path) -> dict[str, list[str]]:
108108
data = json.loads(sanitized)
109109
params = data.get("parameters", {})
110110
except json.JSONDecodeError:
111-
pass
111+
# Keep validation resilient for partially templated/malformed files:
112+
# if JSON parsing fails, treat as having no parsable parameters.
113+
params = {}
112114

113115
# Walk each top-level parameter and scan its entire serialized value
114116
# for ${VAR} references from the original text.

src/ContentProcessor/azure_cicd.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ trigger:
22
branches:
33
include:
44
- main
5+
paths:
6+
include:
7+
- src/ContentProcessor/**
58

69
# When multiple commits land quickly on main, only run the latest.
710
batch: true

0 commit comments

Comments
 (0)