diff --git a/.github/workflows/azd-template-validation.yml b/.github/workflows/azd-template-validation.yml index 0fee728..d2e2abc 100644 --- a/.github/workflows/azd-template-validation.yml +++ b/.github/workflows/azd-template-validation.yml @@ -1,9 +1,14 @@ name: AZD Template Validation -on: +on: workflow_dispatch: push: branches: - main + paths: + - 'infra/**' + - 'azure.yaml' + - 'scripts/**' + - '.github/workflows/azure-dev.yml' permissions: contents: read @@ -16,6 +21,8 @@ jobs: name: azd template validation steps: - uses: actions/checkout@v4 + with: + submodules: recursive # This postprovision cleanup step (Stage 19) has been removed from azure.yaml because # azd down was failing in the pipeline. As a workaround, we are removing this step @@ -36,6 +43,9 @@ jobs: AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TEMP: /tmp fabricCapacityMode: 'none' + AZURE_PRINCIPAL_ID: ${{ vars.PRINCIPAL_ID || secrets.AZURE_CLIENT_ID }} + AZURE_PRINCIPAL_TYPE: 'ServicePrincipal' - name: print result run: cat ${{ steps.validation.outputs.resultFile }} diff --git a/.github/workflows/azure-dev.yml b/.github/workflows/azure-dev.yml index 5cea4c7..0e30910 100644 --- a/.github/workflows/azure-dev.yml +++ b/.github/workflows/azure-dev.yml @@ -24,26 +24,73 @@ jobs: AZURE_RESOURCE_GROUP: ${{ vars.AZURE_RESOURCE_GROUP }} AZURE_ENV_NAME: ${{ vars.AZURE_ENV_NAME }} AZURE_LOCATION: ${{ vars.AZURE_LOCATION }} - AZURE_USER_OBJECT_ID: '' + AZURE_PRINCIPAL_TYPE: 'ServicePrincipal' + TEMP: /tmp steps: - name: Checkout uses: actions/checkout@v4 + with: + submodules: recursive + - name: Install azd uses: Azure/setup-azd@v2 + - name: Azure Developer CLI Login run: | azd auth login ` --client-id "$Env:AZURE_CLIENT_ID" ` --federated-credential-provider "github" ` - --tenant-id "$Env:AZURE_TENANT_ID" + --tenant-id "$Env:AZURE_TENANT_ID" shell: pwsh + - name: Azure CLI Login uses: azure/login@v2 with: client-id: ${{ vars.AZURE_CLIENT_ID }} tenant-id: ${{ vars.AZURE_TENANT_ID }} subscription-id: ${{ vars.AZURE_SUBSCRIPTION_ID }} + + - name: Resolve Service Principal Object ID + run: | + # If PRINCIPAL_ID repo variable is set and is a valid GUID, use it directly + if [[ "${{ vars.PRINCIPAL_ID }}" =~ ^[0-9a-fA-F-]{36}$ ]]; then + echo "Using PRINCIPAL_ID from repo variables" + echo "AZURE_PRINCIPAL_ID=${{ vars.PRINCIPAL_ID }}" >> $GITHUB_ENV + else + # Resolve the Object ID from the Application (Client) ID + # Role assignments require the SP Object ID, not the Client/App ID + echo "Resolving Service Principal Object ID from Client ID..." + SP_OBJECT_ID=$(az ad sp show --id "${{ vars.AZURE_CLIENT_ID }}" --query id -o tsv 2>/dev/null) + if [[ -z "$SP_OBJECT_ID" ]]; then + echo "::error::Failed to resolve Service Principal Object ID from Client ID: ${{ vars.AZURE_CLIENT_ID }}" + exit 1 + fi + echo "Resolved SP Object ID: $SP_OBJECT_ID" + echo "AZURE_PRINCIPAL_ID=$SP_OBJECT_ID" >> $GITHUB_ENV + fi + + - name: Create Resource Group if needed + run: | + # Use provided RG name or derive from environment name + RESOURCE_GROUP="${AZURE_RESOURCE_GROUP:-rg-${AZURE_ENV_NAME}}" + echo "Using resource group: $RESOURCE_GROUP" + + RG_EXISTS=$(az group exists --name "$RESOURCE_GROUP") + if [ "$RG_EXISTS" = "false" ]; then + echo "Creating resource group: $RESOURCE_GROUP" + az group create --name "$RESOURCE_GROUP" --location ${{ vars.AZURE_LOCATION }} + else + echo "Resource group already exists: $RESOURCE_GROUP" + fi + + # Set for subsequent steps + echo "RESOURCE_GROUP=$RESOURCE_GROUP" >> $GITHUB_ENV + - name: Provision Infrastructure + id: provision-main run: azd provision --no-prompt env: - AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ secrets.AZD_INITIAL_ENVIRONMENT_CONFIG }} + AZD_INITIAL_ENVIRONMENT_CONFIG: ${{ secrets.AZD_INITIAL_ENVIRONMENT_CONFIG }} + AZURE_PRINCIPAL_TYPE: 'ServicePrincipal' + fabricCapacityMode: 'none' + fabricWorkspaceMode: 'none' diff --git a/azure.yaml b/azure.yaml index 3231dcb..67f2638 100644 --- a/azure.yaml +++ b/azure.yaml @@ -2,6 +2,7 @@ name: deploy-your-ai-application-in-production requiredVersions: azd: ">=1.15.0 != 1.23.9" + bicep: '>= 0.33.0' infra: provider: "bicep" diff --git a/docs/ACCESSING_PRIVATE_RESOURCES.md b/docs/ACCESSING_PRIVATE_RESOURCES.md index f1b50c4..4071e3c 100644 --- a/docs/ACCESSING_PRIVATE_RESOURCES.md +++ b/docs/ACCESSING_PRIVATE_RESOURCES.md @@ -18,9 +18,9 @@ azd env get-values | grep jumpVm # Or in Azure Portal: # 1. Navigate to your resource group -# 2. Find the VM (usually named like "vm-jump-") +# 2. Find the VM resource created for the jump box # 3. Click "Connect" → "Bastion" -# 4. Enter the username and password (auto-generated during deployment) +# 4. Enter the username and password you set via VM_ADMIN_USERNAME / VM_ADMIN_PASSWORD ``` ### 2. From Jump VM, Access Private Services @@ -169,7 +169,13 @@ You can configure services without private endpoints by modifying individual ser ### Jump VM credentials unknown -Credentials are auto-generated during deployment. To reset: +If you did not set the credentials before deployment, use the top-layer defaults or reset them: + +- Username: `VM_ADMIN_USERNAME` environment variable, or `vmUserName` in [infra/main.bicepparam](../infra/main.bicepparam) +- Default username when unset: `testvmuser` +- Password: `VM_ADMIN_PASSWORD` environment variable, or `vmAdminPassword` in [infra/main.bicepparam](../infra/main.bicepparam) + +To reset: ```bash az vm user update \ diff --git a/docs/deploy_app_from_foundry.md b/docs/deploy_app_from_foundry.md index bd21729..db42c3c 100644 --- a/docs/deploy_app_from_foundry.md +++ b/docs/deploy_app_from_foundry.md @@ -24,7 +24,7 @@ Since all resources are deployed with private endpoints, you must access Microso 2. Navigate to your resource group 3. Select the **Jump VM** (Windows Virtual Machine) 4. Click **Connect** → **Bastion** -5. Enter the VM credentials (set during deployment) +5. Enter the VM credentials you configured in the top layer (`VM_ADMIN_USERNAME` / `VM_ADMIN_PASSWORD`, or [infra/main.bicepparam](../infra/main.bicepparam)) 6. Once connected, open a browser and navigate to [Microsoft Foundry](https://ai.azure.com) ### 2. Configure Your Playground diff --git a/docs/deploymentguide.md b/docs/deploymentguide.md index 8ae20b6..ca930a2 100644 --- a/docs/deploymentguide.md +++ b/docs/deploymentguide.md @@ -202,8 +202,22 @@ Edit `infra/main.bicepparam` or set environment variables: | `postgreSqlNetworkIsolation` | PostgreSQL private networking toggle (defaults to `networkIsolation`) | `networkIsolation` | | `useExistingVNet` | Reuse an existing VNet | `false` | | `existingVnetResourceId` | Existing VNet resource ID (when `useExistingVNet=true`) | `` | -| `vmUserName` | Jump box VM admin username | `` | -| `vmAdminPassword` | Jump box VM admin password | (prompted) | +| `vmUserName` | Jump box VM admin username | `VM_ADMIN_USERNAME` env var or `testvmuser` | +| `vmAdminPassword` | Jump box VM admin password | `VM_ADMIN_PASSWORD` env var | + +For network-isolated deployments, set the VM credentials before running `azd up`: + +```powershell +azd env set VM_ADMIN_USERNAME "youradminuser" +azd env set VM_ADMIN_PASSWORD "Use-A-Strong-Password-Here!" +``` + +If you prefer source-controlled defaults, set them in [infra/main.bicepparam](../infra/main.bicepparam) instead: + +```bicep +param vmUserName = 'youradminuser' +param vmAdminPassword = 'Use-A-Strong-Password-Here!' +``` diff --git a/docs/post_deployment_steps.md b/docs/post_deployment_steps.md index aac6b8f..8438d16 100644 --- a/docs/post_deployment_steps.md +++ b/docs/post_deployment_steps.md @@ -209,9 +209,10 @@ For network-isolated deployments, use Azure Bastion to access resources: ![Image showing bastion blade](../img/provisioning/checkNetworkIsolation7.png) -4. Enter the VM admin credentials (set during deployment) and click **Connect** - - Admin username: `vmUserName` in [infra/main.bicep](../infra/main.bicep) - - Admin password: `vmAdminPassword` in [infra/main.bicepparam](../infra/main.bicepparam) (defaults to the `VM_ADMIN_PASSWORD` environment variable) +4. Enter the VM admin credentials and click **Connect** + - Admin username: `vmUserName` in [infra/main.bicepparam](../infra/main.bicepparam) or the `VM_ADMIN_USERNAME` environment variable + - Admin password: `vmAdminPassword` in [infra/main.bicepparam](../infra/main.bicepparam) or the `VM_ADMIN_PASSWORD` environment variable + - If `vmUserName` is not set in the top layer, the effective default is `testvmuser` - If you do not have them, reset the password in **Azure Portal** → **Virtual machine** → **Reset password**. ![Image showing bastion login](../img/provisioning/checkNetworkIsolation8.png) diff --git a/docs/quota_check.md b/docs/quota_check.md index d809a32..a505fb1 100644 --- a/docs/quota_check.md +++ b/docs/quota_check.md @@ -1,7 +1,7 @@ # Check Quota Availability Before Deployment -Before deploying the accelerator, **ensure sufficient quota availability** for the required model. -> **We recommend increasing the capacity to 100k tokens for optimal performance.** +Before deploying the accelerator, **ensure sufficient quota availability** for the required AI models and Fabric capacity. +> **The default capacities match the deployment parameters in `infra/main.bicepparam`.** ## Login if you have not done so already ``` @@ -9,56 +9,89 @@ az login ``` ## 📌 Default Models & Capacities: +These match the `modelDeploymentList` in the Bicep parameters: ``` -gpt-4o:150, gpt-4o-mini:150, gpt-4:150, text-embedding-3-small:100 +gpt-4.1-mini:40:GlobalStandard, text-embedding-3-large:40:Standard ``` + ## 📌 Default Regions: ``` -eastus, uksouth, eastus2, northcentralus, swedencentral, westus, westus2, southcentralus, canadacentral, australiaeast, japaneast, norwayeast +eastus, eastus2, swedencentral, uksouth, westus, westus2, southcentralus, canadacentral, australiaeast, japaneast, norwayeast ``` + +## 📌 Optional: Fabric Capacity Check +The accelerator also deploys a **Microsoft Fabric F8** capacity. Pass `--check-fabric` (bash) or `-CheckFabric` (PowerShell) to verify Fabric SKU availability. + ## Usage Scenarios: - No parameters passed → Default models and capacities will be checked in default regions. - Only model(s) provided → The script will check for those models in the default regions. - Only region(s) provided → The script will check default models in the specified regions. - Both models and regions provided → The script will check those models in the specified regions. - `--verbose` passed → Enables detailed logging output for debugging and traceability. +- `--check-fabric` passed → Also checks Microsoft Fabric capacity availability. -## **Input Formats** -> Use the --models, --regions, and --verbose options for parameter handling: +## **Input Formats — Bash** +> Use the --models, --regions, --verbose, and --check-fabric options for parameter handling: -✔️ Run without parameters to check default models & regions without verbose logging: - ``` - ./quota_check.sh +✔️ Run without parameters to check default models & regions: + ```sh + ./quota_check.sh ``` ✔️ Enable verbose logging: - ``` - ./quota_check.sh --verbose + ```sh + ./quota_check.sh --verbose ``` ✔️ Check specific model(s) in default regions: - ``` - ./quota_check.sh --models gpt-4o:150,text-embedding-3-small:100 + ```sh + ./quota_check.sh --models gpt-4.1-mini:40:GlobalStandard,text-embedding-3-large:40:Standard ``` ✔️ Check default models in specific region(s): - ``` -./quota_check.sh --regions eastus,westus - ``` -✔️ Passing Both models and regions: - ``` - ./quota_check.sh --models gpt-4o:150 --regions eastus,westus2 + ```sh + ./quota_check.sh --regions eastus,westus ``` ✔️ All parameters combined: + ```sh + ./quota_check.sh --models gpt-4.1-mini:40 --regions eastus,westus --verbose + ``` +✔️ Also check Fabric capacity availability: + ```sh + ./quota_check.sh --check-fabric --verbose ``` - ./quota_check.sh --models gpt-4:150,text-embedding-3-small:100 --regions eastus,westus --verbose + +## **Input Formats — PowerShell** +> Use the -Models, -Regions, -Verbose, and -CheckFabric parameters: + +✔️ Run without parameters: + ```powershell + .\quota_check.ps1 + ``` +✔️ Check specific model(s): + ```powershell + .\quota_check.ps1 -Models "gpt-4.1-mini:40:GlobalStandard,text-embedding-3-large:40:Standard" ``` -✔️ Multiple models with single region: +✔️ Check specific region(s): + ```powershell + .\quota_check.ps1 -Regions "eastus,westus2" ``` - ./quota_check.sh --models gpt-4:150,text-embedding-3-small:100 --regions eastus2 --verbose +✔️ All parameters combined: + ```powershell + .\quota_check.ps1 -Models "gpt-4.1-mini:40" -Regions "eastus,westus" -CheckFabric -Verbose ``` ## **Sample Output** The final table lists regions with available quota. You can select any of these regions for deployment. -![quota-check-output](../img/Documentation/quota-check-output.png) +``` +╔══════════════════════════════════════════════════════════════╗ +║ QUOTA CHECK SUMMARY ║ +╚══════════════════════════════════════════════════════════════╝ + +Region gpt-4.1-mini text-embedding-3-large Status +────────────────────────────────────────────────────────────────────────────────────────── +eastus ✅ 200/240 (need 40) ✅ 120/200 (need 40) ✅ PASS +eastus2 ❌ 10/240 (need 40) ✅ 50/200 (need 40) ❌ FAIL +swedencentral ✅ 100/240 (need 40) ✅ 80/200 (need 40) ✅ PASS +``` --- ## **If using Azure Portal and Cloud Shell** @@ -74,22 +107,33 @@ The final table lists regions with available quota. You can select any of these chmod +x quota_check.sh ./quota_check.sh ``` - - Refer to [Input Formats](#input-formats) for detailed commands. + - Refer to [Input Formats — Bash](#input-formats--bash) for detailed commands. ## **If using VS Code or Codespaces** + +### Option 1: Bash (Linux, macOS, Git Bash, WSL, Cloud Shell) 1. Open the terminal in VS Code or Codespaces. -2. Use a terminal that can run bash. This is only for the quota check script; deployment uses PowerShell. +2. Use a terminal that can run bash. ![git_bash](../img/provisioning/git_bash.png) -3. Navigate to the `scripts` folder where the script files are located and make the script as executable: +3. Navigate to the `scripts` folder and make the script executable: ```sh cd scripts chmod +x quota_check.sh ``` -4. Run the appropriate script based on your requirement: - - **To check quota for the deployment** - +4. Run the script: ```sh ./quota_check.sh ``` - - Refer to [Input Formats](#input-formats) for detailed commands. \ No newline at end of file + - Refer to [Input Formats — Bash](#input-formats--bash) for detailed commands. + +### Option 2: PowerShell (Windows, Linux, macOS) +1. Open a PowerShell terminal in VS Code. +2. Navigate to the `scripts` folder: + ```powershell + cd scripts + ``` +3. Run the script: + ```powershell + .\quota_check.ps1 + ``` + - Refer to [Input Formats — PowerShell](#input-formats--powershell) for detailed commands. \ No newline at end of file diff --git a/infra/main.bicepparam b/infra/main.bicepparam index 947940c..a3342c0 100644 --- a/infra/main.bicepparam +++ b/infra/main.bicepparam @@ -9,7 +9,7 @@ param location = readEnvironmentVariable('AZURE_LOCATION', '') param cosmosLocation = readEnvironmentVariable('AZURE_COSMOS_LOCATION', '') // Entra object ID of the identity to grant RBAC (user, group, service principal, or UAI). Set this if Graph lookup is blocked. param principalId = readEnvironmentVariable('AZURE_PRINCIPAL_ID', '') -param principalType = 'User' +param principalType = readEnvironmentVariable('AZURE_PRINCIPAL_TYPE', 'User') // ======================================== // OPTIONAL INPUTS (Existing Resources) @@ -204,7 +204,8 @@ param containerAppsList = [ } ] -param vmAdminPassword = readEnvironmentVariable('VM_ADMIN_PASSWORD', '$(secretOrRandomPassword)') +param vmUserName = readEnvironmentVariable('VM_ADMIN_USERNAME', 'testvmuser') +param vmAdminPassword = readEnvironmentVariable('VM_ADMIN_PASSWORD', 'JumpboxAdminP@ssw0rd1234!') param vmSize = 'Standard_D2s_v4' // ======================================== diff --git a/scripts/quota_check.ps1 b/scripts/quota_check.ps1 new file mode 100644 index 0000000..209f1bf --- /dev/null +++ b/scripts/quota_check.ps1 @@ -0,0 +1,306 @@ +<# +.SYNOPSIS + Checks Azure OpenAI quota and (optionally) Fabric capacity availability across regions. + +.DESCRIPTION + Verifies that the Azure subscription has sufficient OpenAI model quota in each + candidate region for the models required by this accelerator. + + Default models (from infra/main.bicepparam): + gpt-4.1-mini GlobalStandard 40K TPM + text-embedding-3-large Standard 40K TPM + +.PARAMETER Models + Comma-separated model list. Format: name:capacity[:sku] + When sku is omitted it defaults to GlobalStandard. + Example: "gpt-4.1-mini:40:GlobalStandard,text-embedding-3-large:40:Standard" + +.PARAMETER Regions + Comma-separated Azure region list. + Example: "eastus,westus2,swedencentral" + +.PARAMETER CheckFabric + When set, also validates that the Fabric F8 SKU is available in each region. + +.PARAMETER Verbose + Enables detailed output. + +.EXAMPLE + .\quota_check.ps1 +.EXAMPLE + .\quota_check.ps1 -Models "gpt-4.1-mini:40" -Regions "eastus,westus" +.EXAMPLE + .\quota_check.ps1 -CheckFabric -Verbose +#> + +[CmdletBinding()] +param( + [string]$Models, + [string]$Regions, + [switch]$CheckFabric +) + +$ErrorActionPreference = 'Stop' + +# ---- Defaults ---- +$DefaultModels = 'gpt-4.1-mini:40:GlobalStandard,text-embedding-3-large:40:Standard' +$DefaultRegions = 'eastus,eastus2,swedencentral,uksouth,westus,westus2,southcentralus,canadacentral,australiaeast,japaneast,norwayeast' + +# ---- Resolve inputs ---- +function Resolve-ModelList { + param([string]$ModelString) + $result = @() + foreach ($entry in ($ModelString -split ',')) { + $entry = $entry.Trim() + if ([string]::IsNullOrWhiteSpace($entry)) { continue } + $parts = $entry -split ':' + if ($parts.Count -lt 2) { + Write-Error "Invalid model format: '$entry'. Expected name:capacity[:sku]" + exit 1 + } + $result += [PSCustomObject]@{ + Name = $parts[0] + Capacity = [int]$parts[1] + Sku = if ($parts.Count -ge 3) { $parts[2] } else { 'GlobalStandard' } + } + } + return $result +} + +$modelList = Resolve-ModelList -ModelString $(if ($Models) { $Models } else { $DefaultModels }) +$regionList = ($(if ($Regions) { $Regions } else { $DefaultRegions })) -split ',' | ForEach-Object { $_.Trim() } + +# ---- Auth check ---- +Write-Host '' +Write-Host '╔══════════════════════════════════════════════════════════════╗' -ForegroundColor Cyan +Write-Host '║ Deploy Your AI Application In Production - Quota Check ║' -ForegroundColor Cyan +Write-Host '╚══════════════════════════════════════════════════════════════╝' -ForegroundColor Cyan +Write-Host '' + +try { + $account = az account show --output json 2>$null | ConvertFrom-Json +} catch { + Write-Host '❌ Not logged into Azure CLI. Please run "az login" first.' -ForegroundColor Red + exit 1 +} + +if (-not $account) { + Write-Host '❌ Not logged into Azure CLI. Please run "az login" first.' -ForegroundColor Red + exit 1 +} + +$subscriptionName = $account.name +$subscriptionId = $account.id +Write-Host "🔑 Subscription: $subscriptionName ($subscriptionId)" +Write-Host '' + +# ---- Display config ---- +Write-Host '📋 Configuration:' -ForegroundColor Yellow +Write-Host ' Models:' +foreach ($m in $modelList) { + Write-Host " • $($m.Name) (SKU: $($m.Sku), Required capacity: $($m.Capacity)K TPM)" +} +Write-Host " Regions: $($regionList -join ', ')" +Write-Host " Check Fabric: $CheckFabric" +Write-Host " Verbose: $VerbosePreference" +Write-Host '' + +# ---- Results tracking ---- +$results = @() +$validRegions = @() + +# ---- Main quota check loop ---- +foreach ($region in $regionList) { + Write-Host '════════════════════════════════════════════════════════' -ForegroundColor DarkGray + Write-Host "🔍 Checking region: $region" -ForegroundColor White + + $quotaInfo = $null + try { + $quotaJson = az cognitiveservices usage list --location $region --output json 2>$null + if ($quotaJson) { + $quotaInfo = $quotaJson | ConvertFrom-Json + } + } catch { + # Swallow – region will be skipped + } + + if (-not $quotaInfo -or $quotaInfo.Count -eq 0) { + Write-Host ' ⚠️ Failed to retrieve quota info. Skipping.' -ForegroundColor DarkYellow + $regionResult = [PSCustomObject]@{ Region = $region; Status = 'SKIP'; Details = @{} } + $results += $regionResult + continue + } + + $allPass = $true + $details = @{} + + foreach ($m in $modelList) { + $quotaKey = "OpenAI.$($m.Sku).$($m.Name)" + $required = $m.Capacity + $displayName = "$($m.Name) ($($m.Sku))" + + $usage = $quotaInfo | Where-Object { $_.name.value -eq $quotaKey } + + # Azure quota keys for gpt-4.1 family omit the first hyphen (gpt4.1-mini not gpt-4.1-mini) + if (-not $usage -and $m.Name -match '^gpt-') { + $altName = $m.Name -replace '^gpt-', 'gpt' + $altKey = "OpenAI.$($m.Sku).$altName" + $usage = $quotaInfo | Where-Object { $_.name.value -eq $altKey } + if ($usage -and ($VerbosePreference -eq 'Continue')) { + Write-Host " (Matched via alternate key: $altKey)" -ForegroundColor DarkGray + } + } + + if (-not $usage) { + Write-Host " ⚠️ $displayName — No quota info found in $region" -ForegroundColor DarkYellow + if ($VerbosePreference -eq 'Continue') { + Write-Host " (Looked for quota key: $quotaKey)" -ForegroundColor DarkGray + } + $allPass = $false + $details[$m.Name] = [PSCustomObject]@{ Available = -1; Limit = -1; Status = 'N/A' } + continue + } + + $current = [int]$usage.currentValue + $limit = [int]$usage.limit + $available = $limit - $current + + if ($available -lt $required) { + Write-Host " ❌ $displayName | Used: $current | Limit: $limit | Available: $available | Need: $required" -ForegroundColor Red + $allPass = $false + $details[$m.Name] = [PSCustomObject]@{ Available = $available; Limit = $limit; Status = 'FAIL' } + } else { + Write-Host " ✅ $displayName | Used: $current | Limit: $limit | Available: $available | Need: $required" -ForegroundColor Green + $details[$m.Name] = [PSCustomObject]@{ Available = $available; Limit = $limit; Status = 'PASS' } + } + } + + # ---- Optional Fabric check ---- + if ($CheckFabric) { + $fabricSku = 'F8' + Write-Host " 🔍 Checking Fabric capacity ($fabricSku) availability..." -ForegroundColor White + try { + $skuJson = az rest ` + --method get ` + --url "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.Fabric/skus?api-version=2023-11-01" ` + --output json 2>$null + $skus = ($skuJson | ConvertFrom-Json).value + + $match = $skus | Where-Object { $_.name -eq $fabricSku } + if ($match -and ($match.locations -contains $region -or $match.locations -match $region)) { + Write-Host " ✅ Fabric $fabricSku — Available in $region" -ForegroundColor Green + $details['Fabric'] = [PSCustomObject]@{ Available = 1; Limit = 1; Status = 'PASS' } + } else { + Write-Host " ⚠️ Fabric $fabricSku — Could not confirm availability in $region" -ForegroundColor DarkYellow + $details['Fabric'] = [PSCustomObject]@{ Available = 0; Limit = 0; Status = 'WARN' } + } + } catch { + Write-Host " ⚠️ Fabric check failed for $region" -ForegroundColor DarkYellow + $details['Fabric'] = [PSCustomObject]@{ Available = 0; Limit = 0; Status = 'WARN' } + } + } + + if ($allPass) { + $validRegions += $region + Write-Host " 🎉 Region '$region' has sufficient quota for all models!" -ForegroundColor Green + } + + $results += [PSCustomObject]@{ + Region = $region + Status = $(if ($allPass) { 'PASS' } else { 'FAIL' }) + Details = $details + } +} + +# ---- Summary table ---- +Write-Host '' +Write-Host '╔══════════════════════════════════════════════════════════════╗' -ForegroundColor Cyan +Write-Host '║ QUOTA CHECK SUMMARY ║' -ForegroundColor Cyan +Write-Host '╚══════════════════════════════════════════════════════════════╝' -ForegroundColor Cyan +Write-Host '' + +# Build header +$header = '{0,-22}' -f 'Region' +foreach ($m in $modelList) { + $header += '{0,-30}' -f $m.Name +} +if ($CheckFabric) { + $header += '{0,-16}' -f 'Fabric' +} +$header += '{0,-10}' -f 'Status' +Write-Host $header -ForegroundColor White + +$separatorLen = 22 + ($modelList.Count * 30) + 10 +if ($CheckFabric) { $separatorLen += 16 } +Write-Host ('─' * $separatorLen) -ForegroundColor DarkGray + +foreach ($r in $results) { + $line = '{0,-22}' -f $r.Region + + foreach ($m in $modelList) { + $d = $r.Details[$m.Name] + if ($null -eq $d -or $d.Status -eq 'N/A') { + $cell = '⚠️ N/A' + } elseif ($d.Status -eq 'PASS') { + $cell = "✅ $($d.Available)/$($d.Limit) (need $($m.Capacity))" + } else { + $cell = "❌ $($d.Available)/$($d.Limit) (need $($m.Capacity))" + } + $line += '{0,-30}' -f $cell + } + + if ($CheckFabric) { + $fd = $r.Details['Fabric'] + if (-not $fd) { + $line += '{0,-16}' -f '—' + } elseif ($fd.Status -eq 'PASS') { + $line += '{0,-16}' -f '✅ Available' + } else { + $line += '{0,-16}' -f '⚠️ Unknown' + } + } + + $statusStr = switch ($r.Status) { + 'PASS' { '✅ PASS' } + 'FAIL' { '❌ FAIL' } + 'SKIP' { '⚠️ SKIP' } + default { $r.Status } + } + $line += '{0,-10}' -f $statusStr + + $color = switch ($r.Status) { + 'PASS' { 'Green' } + 'FAIL' { 'Red' } + default { 'DarkYellow' } + } + Write-Host $line -ForegroundColor $color +} + +# ---- Final recommendation ---- +Write-Host '' +Write-Host '════════════════════════════════════════════════════════' -ForegroundColor DarkGray + +if ($validRegions.Count -eq 0) { + Write-Host '❌ No region found with sufficient quota for all models!' -ForegroundColor Red + Write-Host '' + Write-Host ' Recommendations:' -ForegroundColor Yellow + Write-Host ' 1. Request a quota increase via Azure Portal → Quotas' + Write-Host ' 2. Try different regions with the -Regions parameter' + Write-Host ' 3. Reduce model capacity requirements with the -Models parameter' + Write-Host '' + Write-Host ' Models needed:' + foreach ($m in $modelList) { + Write-Host " • $($m.Name) (SKU: $($m.Sku), Capacity: $($m.Capacity)K TPM)" + } + exit 1 +} else { + Write-Host '✅ Regions with sufficient quota:' -ForegroundColor Green + foreach ($r in $validRegions) { + Write-Host " • $r" -ForegroundColor Green + } + Write-Host '' + Write-Host ' To deploy, set your desired region:' -ForegroundColor White + Write-Host ' azd env set AZURE_LOCATION ' -ForegroundColor White + Write-Host ' azd up' -ForegroundColor White + exit 0 +} diff --git a/scripts/quota_check.sh b/scripts/quota_check.sh new file mode 100644 index 0000000..a481534 --- /dev/null +++ b/scripts/quota_check.sh @@ -0,0 +1,348 @@ +#!/bin/bash + +# ============================================================================= +# Quota Check Script for Deploy Your AI Application In Production +# ============================================================================= +# Checks Azure OpenAI quota and Fabric capacity availability across regions +# for the models required by this accelerator. +# +# No external dependencies beyond Azure CLI (az). Uses az --query (JMESPath) +# for JSON parsing instead of python3 or jq. +# +# Default models (from infra/main.bicepparam): +# gpt-4.1-mini:40 (GlobalStandard), text-embedding-3-large:40 (Standard) +# +# Default regions: +# eastus, eastus2, swedencentral, uksouth, westus, westus2, +# southcentralus, canadacentral, australiaeast, japaneast, norwayeast +# +# Usage: +# ./quota_check.sh +# ./quota_check.sh --verbose +# ./quota_check.sh --models gpt-4.1-mini:40,text-embedding-3-large:40 +# ./quota_check.sh --regions eastus,westus2 +# ./quota_check.sh --models gpt-4.1-mini:40 --regions eastus,westus --verbose +# ./quota_check.sh --check-fabric +# ============================================================================= + +set -euo pipefail + +# ---- Defaults ---- +DEFAULT_MODELS="gpt-4.1-mini:40:GlobalStandard,text-embedding-3-large:40:Standard" +DEFAULT_REGIONS="eastus,eastus2,swedencentral,uksouth,westus,westus2,southcentralus,canadacentral,australiaeast,japaneast,norwayeast" +VERBOSE=false +CHECK_FABRIC=false + +# ---- Parse arguments ---- +MODELS_INPUT="" +REGIONS_INPUT="" + +while [[ $# -gt 0 ]]; do + case "$1" in + --models) + MODELS_INPUT="$2" + shift 2 + ;; + --regions) + REGIONS_INPUT="$2" + shift 2 + ;; + --verbose) + VERBOSE=true + shift + ;; + --check-fabric) + CHECK_FABRIC=true + shift + ;; + --help|-h) + echo "Usage: $0 [OPTIONS]" + echo "" + echo "Options:" + echo " --models MODEL_LIST Comma-separated models (format: name:capacity[:sku])" + echo " Default: $DEFAULT_MODELS" + echo " --regions REGION_LIST Comma-separated Azure regions" + echo " Default: $DEFAULT_REGIONS" + echo " --check-fabric Also check Microsoft Fabric capacity SKU availability" + echo " --verbose Enable detailed logging" + echo " --help, -h Show this help message" + echo "" + echo "Examples:" + echo " $0" + echo " $0 --models gpt-4.1-mini:40,text-embedding-3-large:40 --regions eastus,westus" + echo " $0 --check-fabric --verbose" + exit 0 + ;; + *) + echo "❌ Unknown option: $1" + echo " Run '$0 --help' for usage." + exit 1 + ;; + esac +done + +# ---- Resolve models ---- +resolve_models() { + local input="$1" + local resolved=() + IFS=',' read -ra entries <<< "$input" + for entry in "${entries[@]}"; do + local name capacity sku + IFS=':' read -r name capacity sku <<< "$entry" + if [[ -z "$name" || -z "$capacity" ]]; then + echo "❌ Invalid model format: '$entry'. Expected name:capacity[:sku]" >&2 + exit 1 + fi + sku="${sku:-GlobalStandard}" + resolved+=("${name}:${capacity}:${sku}") + done + echo "${resolved[*]}" +} + +if [[ -n "$MODELS_INPUT" ]]; then + MODELS_RAW="$MODELS_INPUT" +else + MODELS_RAW="$DEFAULT_MODELS" +fi + +IFS=' ' read -ra MODELS <<< "$(resolve_models "$MODELS_RAW")" + +if [[ -n "$REGIONS_INPUT" ]]; then + IFS=',' read -ra REGIONS <<< "$REGIONS_INPUT" +else + IFS=',' read -ra REGIONS <<< "$DEFAULT_REGIONS" +fi + +# ---- Helper: query quota for a specific key in a region ---- +# Uses az CLI --query (JMESPath) — no python3/jq dependency. +# Returns "currentValue\tlimit" (tab-separated) or empty string. +query_quota() { + local region="$1" + local quota_key="$2" + az cognitiveservices usage list \ + --location "$region" \ + --query "[?name.value=='${quota_key}'].{c:currentValue,l:limit} | [0]" \ + --output tsv 2>/dev/null || echo "" +} + +# ---- Authentication check ---- +echo "" +echo "╔══════════════════════════════════════════════════════════════╗" +echo "║ Deploy Your AI Application In Production - Quota Check ║" +echo "╚══════════════════════════════════════════════════════════════╝" +echo "" + +if ! az account show &>/dev/null; then + echo "❌ Not logged into Azure CLI. Please run 'az login' first." + exit 1 +fi + +SUBSCRIPTION_NAME=$(az account show --query "name" -o tsv 2>/dev/null) +SUBSCRIPTION_ID=$(az account show --query "id" -o tsv 2>/dev/null) +echo "🔑 Subscription: $SUBSCRIPTION_NAME ($SUBSCRIPTION_ID)" +echo "" + +# ---- Display configuration ---- +echo "📋 Configuration:" +echo " Models:" +for m in "${MODELS[@]}"; do + IFS=':' read -r mname mcap msku <<< "$m" + echo " • $mname (SKU: $msku, Required capacity: ${mcap}K TPM)" +done +echo " Regions: ${REGIONS[*]}" +echo " Check Fabric: $CHECK_FABRIC" +echo " Verbose: $VERBOSE" +echo "" + +# ---- Build model info arrays ---- +MODEL_NAMES=() +MODEL_CAPS=() +MODEL_SKUS=() +MODEL_PRIMARY_KEYS=() +MODEL_ALT_KEYS=() + +for m in "${MODELS[@]}"; do + IFS=':' read -r mname mcap msku <<< "$m" + MODEL_NAMES+=("$mname") + MODEL_CAPS+=("$mcap") + MODEL_SKUS+=("$msku") + MODEL_PRIMARY_KEYS+=("OpenAI.${msku}.${mname}") + # Azure quota keys for gpt-4.1 family omit the first hyphen (gpt4.1-mini not gpt-4.1-mini) + if [[ "$mname" == gpt-* ]]; then + alt_mname="${mname/gpt-/gpt}" + MODEL_ALT_KEYS+=("OpenAI.${msku}.${alt_mname}") + else + MODEL_ALT_KEYS+=("") + fi +done + +MODEL_COUNT=${#MODEL_NAMES[@]} + +# ---- Results tracking ---- +declare -A REGION_STATUS +VALID_REGIONS=() + +# ---- Main quota check loop ---- +for REGION in "${REGIONS[@]}"; do + echo "════════════════════════════════════════════════════════" + echo "🔍 Checking region: $REGION" + + ALL_PASS=true + safe_region="${REGION//[^a-zA-Z0-9]/_}" + + for ((i=0; i/dev/null || echo "") + + if echo "$SKU_CHECK" | grep -qi "$REGION" 2>/dev/null; then + echo " ✅ Fabric $FABRIC_SKU — Available in $REGION" + else + echo " ⚠️ Fabric $FABRIC_SKU — Could not confirm availability in $REGION" + if $VERBOSE; then + echo " (Fabric SKU availability check returned no match for $REGION)" + fi + fi + fi + + if $ALL_PASS; then + REGION_STATUS["$REGION"]="pass" + VALID_REGIONS+=("$REGION") + echo " 🎉 Region '$REGION' has sufficient quota for all models!" + else + REGION_STATUS["$REGION"]="fail" + fi +done + +# ---- Summary table ---- +echo "" +echo "╔══════════════════════════════════════════════════════════════╗" +echo "║ QUOTA CHECK SUMMARY ║" +echo "╚══════════════════════════════════════════════════════════════╝" +echo "" + +printf "%-22s" "Region" +for ((i=0; i" + echo " azd up" + exit 0 +fi