From 155543fdd56af7eb18a861435bf27f353727c1d7 Mon Sep 17 00:00:00 2001 From: abnaraya Date: Sun, 10 May 2026 16:58:40 +0200 Subject: [PATCH 01/15] added azd artifacts --- .github/copilot-instructions.md | 4 + .gitignore | 3 + README.md | 32 + azure.yaml | 47 ++ demo-data/vm-inventory.xlsx | Bin 70848 -> 205312 bytes infra/main-resources.bicep | 92 +++ infra/main-resources.json | 761 ++++++++++++++++++++++ infra/main.bicep | 102 +++ infra/main.json | 1005 +++++++++++++++++++++++++++++ infra/main.parameters.json | 42 ++ infra/modules/acr.bicep | 23 + infra/modules/aoai.bicep | 66 ++ infra/modules/appservice.bicep | 76 +++ infra/modules/appservice.json | 157 +++++ infra/modules/monitoring.bicep | 35 + infra/modules/roles-aoai.bicep | 20 + infra/modules/roles.bicep | 42 ++ infra/scripts/postdeploy.ps1 | 61 ++ infra/scripts/postprovision.ps1 | 47 ++ infra/scripts/preprovision.ps1 | 65 ++ src/Dashboard/setup-entra-app.ps1 | 83 ++- 21 files changed, 2738 insertions(+), 25 deletions(-) create mode 100644 azure.yaml create mode 100644 infra/main-resources.bicep create mode 100644 infra/main-resources.json create mode 100644 infra/main.bicep create mode 100644 infra/main.json create mode 100644 infra/main.parameters.json create mode 100644 infra/modules/acr.bicep create mode 100644 infra/modules/aoai.bicep create mode 100644 infra/modules/appservice.bicep create mode 100644 infra/modules/appservice.json create mode 100644 infra/modules/monitoring.bicep create mode 100644 infra/modules/roles-aoai.bicep create mode 100644 infra/modules/roles.bicep create mode 100644 infra/scripts/postdeploy.ps1 create mode 100644 infra/scripts/postprovision.ps1 create mode 100644 infra/scripts/preprovision.ps1 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 134a65f..c63d7bb 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -272,6 +272,10 @@ The production app runs as a Docker container on Azure App Service, fed from Azu - ACR: `crfinopsagent.azurecr.io` (Basic SKU, admin enabled) - Container app: `finops-agent-container` on `ASP-rgfinopsagent-b74f` P0v3 plan +### Customer-facing `azd up` path + +For external users deploying into their own subscription, the repo ships with `azure.yaml` + `infra/` (Bicep) + `infra/scripts/` (PowerShell hooks). A single `azd up` provisions everything: RG, Log Analytics + App Insights, ACR (admin disabled, MI-based pull), Azure OpenAI + model deployment, App Service Plan + Web App with system-assigned MI, role assignments (`AcrPull`, `Cognitive Services User`), and the multi-tenant Entra app registration (via `setup-entra-app.ps1 -OutputJson` invoked from the preprovision hook). The postdeploy hook runs `az acr build` so users do not need a local Docker daemon. See README "Deploy to Azure (`azd up`)" for the full UX. **Note:** the production owner deployment (`crfinopsagent` / `finops-agent-container`) predates this and is unaffected — `azd up` always creates new resources under the chosen `azd env` name. + ## Code Conventions - Use clean, well-structured C# for the .NET backend following Microsoft coding conventions. diff --git a/.gitignore b/.gitignore index ddc7446..85b5df3 100644 --- a/.gitignore +++ b/.gitignore @@ -69,6 +69,9 @@ secrets.json ## Squad infrastructure (local only) .squad/ +## azd local environment state (per-developer; contains secrets like AZURE_ENTRA_CLIENT_SECRET) +.azure/ + .copilot/skills/agent-collaboration/SKILL.md .gitignore .copilot/skills/squad-conventions/SKILL.md diff --git a/README.md b/README.md index 3d6dce0..adff90b 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,38 @@ Vue 3 SPA → .NET 10 minimal API → GitHub Copilot SDK → Azure read APIs (Co ## [See the architecture diagram →](https://azure-finops-agent.com/slides#4) +## Deploy to Azure (`azd up`) + +One command provisions and deploys everything into your own subscription: + +```powershell +az login # sign into the target tenant +az account set --subscription +azd auth login # sign into azd (same tenant) +azd up # provision + build image + deploy +``` + +What `azd up` does: + +1. Creates a resource group, Log Analytics + Application Insights, Azure Container Registry (Basic, admin disabled), Azure OpenAI account + model deployment, Linux App Service Plan, and the containerised Web App with system-assigned managed identity. +2. Grants the Web App's MI `AcrPull` on the registry and `Cognitive Services User` on Azure OpenAI (BYOK). +3. Creates a multi-tenant Microsoft Entra ID app registration with the 5 incremental-consent permission tiers (ARM, Microsoft Graph, Log Analytics, Azure Storage) — see [setup-entra-app.ps1](src/Dashboard/setup-entra-app.ps1). +4. Builds the Docker image server-side via `az acr build` (no local Docker daemon required) and restarts the Web App. + +Override defaults via `azd env set` before running `azd up`: + +| Variable | Default | Notes | +|---|---|---| +| `AZURE_LOCATION` | _(prompted)_ | Region for the resource group and most resources | +| `AZURE_OPENAI_LOCATION` | `swedencentral` | AOAI region (model availability is region-restricted) | +| `APP_SERVICE_PLAN_SKU` | `B1` | Use `P0V3` for production-grade (~$77/mo vs ~$13/mo) | +| `AZURE_OPENAI_MODEL_NAME` / `_VERSION` | `gpt-4o` / `2024-11-20` | Must be available in `AZURE_OPENAI_LOCATION` | +| `AZURE_OPENAI_DEPLOYMENT_NAME` | `gpt-4o` | Surfaced as `AzureOpenAI__DeploymentName` to the app | +| `EXISTING_AOAI_RESOURCE_ID` | _(empty)_ | Full resource ID to reuse an existing AOAI account instead of creating one | +| `AZURE_ENTRA_APP_ID` / `_CLIENT_SECRET` | _(empty)_ | Reuse an existing Entra app instead of creating one | + +Tear down with `azd down --purge` (purge is required because Cognitive Services soft-deletes by default). + ## Running Locally ### Prerequisites diff --git a/azure.yaml b/azure.yaml new file mode 100644 index 0000000..0b0cef3 --- /dev/null +++ b/azure.yaml @@ -0,0 +1,47 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/Azure/azure-dev/main/schemas/v1.0/azure.yaml.json +# Azure Developer CLI (azd) configuration for Azure FinOps Agent. +# `azd up` provisions all infrastructure (Bicep under ./infra) and deploys the +# container image to App Service. PowerShell hooks fill the gaps Bicep can't +# cover (Entra app registration, ACR image build, redirect-URI patching). + +name: azure-finops-agent +metadata: + template: azure-finops-agent@1.0.0 + +infra: + provider: bicep + path: infra + module: main + +# The web app is built and pushed by the postdeploy hook (`az acr build`), +# which avoids needing a local Docker daemon. We declare no `services` block so +# `azd deploy` is a no-op — provisioning + the postdeploy hook do all the work. + +hooks: + preprovision: + posix: + shell: pwsh + run: ./infra/scripts/preprovision.ps1 + interactive: true + windows: + shell: pwsh + run: ./infra/scripts/preprovision.ps1 + interactive: true + postprovision: + posix: + shell: pwsh + run: ./infra/scripts/postprovision.ps1 + interactive: true + windows: + shell: pwsh + run: ./infra/scripts/postprovision.ps1 + interactive: true + postdeploy: + posix: + shell: pwsh + run: ./infra/scripts/postdeploy.ps1 + interactive: true + windows: + shell: pwsh + run: ./infra/scripts/postdeploy.ps1 + interactive: true diff --git a/demo-data/vm-inventory.xlsx b/demo-data/vm-inventory.xlsx index d264c75ca447536f5437e4254a6e73b24368c1d4..cfa59b2d5aa513a370d3bd331fd35477b89caa50 100644 GIT binary patch literal 205312 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
  • -6zM_&u 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

    VZOEgE_1ihkD4Ur+hFu+18Il|Ayfn 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

    Er#{j7*1PM8Ol}Ri^g)7PH^wD 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/auth/microsoft/callback`). +# 2. Print a concise summary of what was provisioned. +# +# Safe to re-run: az ad app update is idempotent and we de-dupe before sending. + +$ErrorActionPreference = 'Stop' + +Write-Host "`n=== azd postprovision ===" -ForegroundColor Cyan + +$envValues = azd env get-values | ConvertFrom-StringData +$objectId = $envValues['AZURE_ENTRA_OBJECT_ID'] +$webHost = $envValues['WEB_APP_HOSTNAME'] +$webUrl = $envValues['WEB_APP_URL'] + +if (-not $objectId -or -not $webHost) { + Write-Host " AZURE_ENTRA_OBJECT_ID or WEB_APP_HOSTNAME missing from azd env — skipping redirect-URI patch." -ForegroundColor Yellow +} else { + $objectId = $objectId.Trim('"') + $webHost = $webHost.Trim('"') + + $desired = @( + 'http://localhost:5000/auth/microsoft/callback', + "https://$webHost/auth/microsoft/callback" + ) + + Write-Host " Patching Entra app redirect URIs for hostname: $webHost" -ForegroundColor Yellow + $existingJson = az ad app show --id $objectId --query 'web.redirectUris' -o json 2>$null + $existing = if ($existingJson) { $existingJson | ConvertFrom-Json } else { @() } + $merged = @($existing + $desired | Select-Object -Unique) + + az ad app update --id $objectId --web-redirect-uris @merged --output none + if ($LASTEXITCODE -eq 0) { + Write-Host " Redirect URIs updated:" -ForegroundColor Green + foreach ($u in $merged) { Write-Host " $u" -ForegroundColor Gray } + } else { + Write-Host " Failed to update redirect URIs (exit $LASTEXITCODE). Run manually:" -ForegroundColor Red + Write-Host " az ad app update --id $objectId --web-redirect-uris $($merged -join ' ')" -ForegroundColor Gray + } +} + +Write-Host "`n Web App: $webUrl" -ForegroundColor Cyan +Write-Host " Next: image will be built and pushed by the postdeploy hook." -ForegroundColor Gray +Write-Host "=== postprovision complete ===`n" -ForegroundColor Cyan diff --git a/infra/scripts/preprovision.ps1 b/infra/scripts/preprovision.ps1 new file mode 100644 index 0000000..f9a7ae3 --- /dev/null +++ b/infra/scripts/preprovision.ps1 @@ -0,0 +1,65 @@ +# Preprovision hook (azd) — runs before `azd provision`. +# +# Responsibilities: +# 1. Verify az CLI is logged into a subscription. +# 2. If AZURE_ENTRA_APP_ID is not already in azd env, create the multi-tenant +# Entra ID app registration via setup-entra-app.ps1 -OutputJson and stash +# appId / clientSecret / tenantId in azd env so Bicep picks them up in this +# same provision run. +# 3. Capture AZURE_PRINCIPAL_ID for downstream role assignments if needed. +# +# Idempotent: if AZURE_ENTRA_APP_ID is already set, skip Entra creation. + +$ErrorActionPreference = 'Stop' +$repoRoot = Split-Path -Parent $PSScriptRoot | Split-Path -Parent + +Write-Host "`n=== azd preprovision ===" -ForegroundColor Cyan + +# ── 1. az CLI login check ── +$account = az account show 2>$null | ConvertFrom-Json +if (-not $account) { + Write-Host " az CLI not logged in. Running 'az login'..." -ForegroundColor Yellow + az login --output none + $account = az account show 2>$null | ConvertFrom-Json +} +Write-Host " Subscription: $($account.name) ($($account.id))" -ForegroundColor Gray +Write-Host " Tenant: $($account.tenantId)" -ForegroundColor Gray + +# Capture deployer principal ID for optional downstream role grants. +$signedInUser = az ad signed-in-user show --query id -o tsv 2>$null +if ($signedInUser) { + azd env set AZURE_PRINCIPAL_ID $signedInUser | Out-Null +} + +# ── 2. Entra app registration ── +$envValues = azd env get-values | ConvertFrom-StringData +$existingAppId = $envValues['AZURE_ENTRA_APP_ID'] + +if ($existingAppId -and $existingAppId.Trim('"').Length -gt 0) { + Write-Host " Reusing existing Entra app: $($existingAppId.Trim('"'))" -ForegroundColor Green +} else { + Write-Host " Creating Entra ID app registration (multi-tenant, 5 consent tiers)..." -ForegroundColor Yellow + + $entraScript = Join-Path $repoRoot 'src/Dashboard/setup-entra-app.ps1' + $envName = $envValues['AZURE_ENV_NAME'].Trim('"') + $appName = "Azure FinOps Agent ($envName)" + + # Production redirect URI is unknown until Bicep runs. Register localhost now; + # postprovision.ps1 patches the App Service hostname in afterwards. + $jsonOutput = & $entraScript -AppName $appName -OutputJson + if ($LASTEXITCODE -ne 0 -or -not $jsonOutput) { + Write-Host " setup-entra-app.ps1 failed." -ForegroundColor Red + exit 1 + } + + $appInfo = $jsonOutput | ConvertFrom-Json + azd env set AZURE_ENTRA_APP_ID $appInfo.appId | Out-Null + azd env set AZURE_ENTRA_CLIENT_SECRET $appInfo.clientSecret | Out-Null + azd env set AZURE_ENTRA_OBJECT_ID $appInfo.objectId | Out-Null + + Write-Host " Created app: $($appInfo.appId)" -ForegroundColor Green + Write-Host " Secret stored in azd env (AZURE_ENTRA_CLIENT_SECRET)." -ForegroundColor Gray + Write-Host " Service-principal propagation can take 30-60s; Bicep will retry on first failure." -ForegroundColor DarkGray +} + +Write-Host "=== preprovision complete ===`n" -ForegroundColor Cyan diff --git a/src/Dashboard/setup-entra-app.ps1 b/src/Dashboard/setup-entra-app.ps1 index 9d3367f..31fcc60 100644 --- a/src/Dashboard/setup-entra-app.ps1 +++ b/src/Dashboard/setup-entra-app.ps1 @@ -36,26 +36,44 @@ param( [string]$AppName = "Azure FinOps Agent", [string]$ProductionUrl = "", - [int]$SecretExpiryMonths = 12 + [int]$SecretExpiryMonths = 12, + # Extra redirect URIs to register beyond the localhost + ProductionUrl defaults. + # Used by the azd preprovision hook to register additional callbacks (e.g. App Service hostname). + [string[]]$ExtraRedirectUris = @(), + # When set, suppresses all human-readable Write-Host output and prints a single + # JSON object {appId, clientSecret, tenantId, redirectUris} to stdout — intended + # for consumption by the azd preprovision hook. + [switch]$OutputJson ) $ErrorActionPreference = "Stop" -Write-Host "`n=== Azure FinOps Agent — Entra ID App Registration Setup ===" -ForegroundColor Cyan -Write-Host "This script creates a multi-tenant app registration with read-only permissions.`n" -ForegroundColor Gray +# When -OutputJson is set, route all chatty status output to stderr so stdout is +# pure JSON the caller can pipe into ConvertFrom-Json. +function Write-Status { + param([string]$Message, [string]$ForegroundColor = 'Gray') + if ($OutputJson) { + [Console]::Error.WriteLine($Message) + } else { + Write-Host $Message -ForegroundColor $ForegroundColor + } +} + +Write-Status "`n=== Azure FinOps Agent — Entra ID App Registration Setup ===" 'Cyan' +Write-Status "This script creates a multi-tenant app registration with read-only permissions.`n" 'Gray' # ── 1. Verify az CLI is logged in ── -Write-Host "[1/6] Checking Azure CLI login..." -ForegroundColor Yellow +Write-Status "[1/6] Checking Azure CLI login..." 'Yellow' $account = az account show 2>$null | ConvertFrom-Json if (-not $account) { - Write-Host " Not logged in. Run 'az login' first." -ForegroundColor Red + Write-Status " Not logged in. Run 'az login' first." 'Red' exit 1 } -Write-Host " Tenant: $($account.tenantId)" -ForegroundColor Gray -Write-Host " User: $($account.user.name)" -ForegroundColor Gray +Write-Status " Tenant: $($account.tenantId)" 'Gray' +Write-Status " User: $($account.user.name)" 'Gray' # ── 2. Build redirect URIs ── -Write-Host "`n[2/6] Configuring redirect URIs..." -ForegroundColor Yellow +Write-Status "`n[2/6] Configuring redirect URIs..." 'Yellow' $redirectUris = @( "http://localhost:5000/auth/microsoft/callback" ) @@ -69,12 +87,15 @@ if ($ProductionUrl) { $redirectUris += "$($uri.Scheme)://www.$($uri.Host)/auth/microsoft/callback" } } +foreach ($u in $ExtraRedirectUris) { + if ($u -and ($redirectUris -notcontains $u)) { $redirectUris += $u } +} foreach ($u in $redirectUris) { - Write-Host " $u" -ForegroundColor Gray + Write-Status " $u" 'Gray' } # ── 3. Create the app registration ── -Write-Host "`n[3/6] Creating app registration '$AppName'..." -ForegroundColor Yellow +Write-Status "`n[3/6] Creating app registration '$AppName'..." 'Yellow' # Build the redirect URIs JSON for the web platform $redirectUrisJson = ($redirectUris | ForEach-Object { "`"$_`"" }) -join "," @@ -88,7 +109,7 @@ $appJson = az ad app create ` 2>$null if (-not $appJson) { - Write-Host " Failed to create app registration. Check permissions." -ForegroundColor Red + Write-Status " Failed to create app registration. Check permissions." 'Red' exit 1 } @@ -96,11 +117,11 @@ $app = $appJson | ConvertFrom-Json $clientId = $app.appId $objectId = $app.id -Write-Host " App ID (ClientId): $clientId" -ForegroundColor Green -Write-Host " Object ID: $objectId" -ForegroundColor Gray +Write-Status " App ID (ClientId): $clientId" 'Green' +Write-Status " Object ID: $objectId" 'Gray' # ── 4. Add API permissions (all read-only) ── -Write-Host "`n[4/6] Adding API permissions (read-only)..." -ForegroundColor Yellow +Write-Status "`n[4/6] Adding API permissions (read-only)..." 'Yellow' # Known permission GUIDs (Microsoft-published, stable across all tenants) # Azure Service Management @@ -162,17 +183,17 @@ $requiredAccess | Out-File -FilePath $tempFile -Encoding utf8 -NoNewline az ad app update --id $objectId --required-resource-accesses "@$tempFile" --output none 2>$null Remove-Item $tempFile -Force -Write-Host " Azure ARM: user_impersonation (delegated)" -ForegroundColor Gray -Write-Host " Microsoft Graph: User.Read, Organization.Read.All, Reports.Read.All," -ForegroundColor Gray -Write-Host " User.Read.All, Group.Read.All (all delegated, read-only)" -ForegroundColor Gray -Write-Host " Log Analytics: Data.Read (delegated, read-only)" -ForegroundColor Gray -Write-Host " Azure Storage: user_impersonation (delegated, for cost exports)" -ForegroundColor Gray -Write-Host "" -Write-Host " NOTE: All Graph and Log Analytics scopes use incremental consent —" -ForegroundColor DarkYellow -Write-Host " users only see consent prompts when they opt into each tier." -ForegroundColor DarkYellow +Write-Status " Azure ARM: user_impersonation (delegated)" 'Gray' +Write-Status " Microsoft Graph: User.Read, Organization.Read.All, Reports.Read.All," 'Gray' +Write-Status " User.Read.All, Group.Read.All (all delegated, read-only)" 'Gray' +Write-Status " Log Analytics: Data.Read (delegated, read-only)" 'Gray' +Write-Status " Azure Storage: user_impersonation (delegated, for cost exports)" 'Gray' +Write-Status "" +Write-Status " NOTE: All Graph and Log Analytics scopes use incremental consent —" 'DarkYellow' +Write-Status " users only see consent prompts when they opt into each tier." 'DarkYellow' # ── 5. Create client secret ── -Write-Host "`n[5/6] Creating client secret (valid $SecretExpiryMonths months)..." -ForegroundColor Yellow +Write-Status "`n[5/6] Creating client secret (valid $SecretExpiryMonths months)..." 'Yellow' $endDate = (Get-Date).AddMonths($SecretExpiryMonths).ToString("yyyy-MM-ddTHH:mm:ssZ") $secretJson = az ad app credential reset ` @@ -183,14 +204,26 @@ $secretJson = az ad app credential reset ` 2>$null if (-not $secretJson) { - Write-Host " Failed to create client secret." -ForegroundColor Red + Write-Status " Failed to create client secret." 'Red' exit 1 } $secret = ($secretJson | ConvertFrom-Json).password -Write-Host " Secret created (expires: $endDate)" -ForegroundColor Gray +Write-Status " Secret created (expires: $endDate)" 'Gray' # ── 6. Output configuration ── +if ($OutputJson) { + [pscustomobject]@{ + appId = $clientId + objectId = $objectId + clientSecret = $secret + tenantId = $account.tenantId + redirectUris = $redirectUris + secretExpiry = $endDate + } | ConvertTo-Json -Compress + return +} + Write-Host "`n[6/6] Setup complete!" -ForegroundColor Green Write-Host "`n$('=' * 60)" -ForegroundColor Cyan Write-Host " ADD THESE VALUES TO YOUR CONFIGURATION" -ForegroundColor Cyan From 47fca2e7066fe05ca572f16d156860f40506ac3c Mon Sep 17 00:00:00 2001 From: abnaraya Date: Wed, 20 May 2026 13:44:05 +0200 Subject: [PATCH 02/15] Untested - adding azd provisioning. --- README.md | 8 ++++---- infra/main-resources.json | 6 +++--- infra/main.bicep | 8 ++++---- infra/main.json | 16 ++++++++-------- infra/main.parameters.json | 6 +++--- infra/scripts/preprovision.ps1 | 25 ++++++++++++++++++++++++- 6 files changed, 46 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index adff90b..383d44e 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Vue 3 SPA → .NET 10 minimal API → GitHub Copilot SDK → Azure read APIs (Co One command provisions and deploys everything into your own subscription: ```powershell -az login # sign into the target tenant +az login --tenant # sign into the target tenant az account set --subscription azd auth login # sign into azd (same tenant) azd up # provision + build image + deploy @@ -53,10 +53,10 @@ Override defaults via `azd env set` before running `azd up`: | `AZURE_LOCATION` | _(prompted)_ | Region for the resource group and most resources | | `AZURE_OPENAI_LOCATION` | `swedencentral` | AOAI region (model availability is region-restricted) | | `APP_SERVICE_PLAN_SKU` | `B1` | Use `P0V3` for production-grade (~$77/mo vs ~$13/mo) | -| `AZURE_OPENAI_MODEL_NAME` / `_VERSION` | `gpt-4o` / `2024-11-20` | Must be available in `AZURE_OPENAI_LOCATION` | -| `AZURE_OPENAI_DEPLOYMENT_NAME` | `gpt-4o` | Surfaced as `AzureOpenAI__DeploymentName` to the app | +| `AZURE_OPENAI_MODEL_NAME` / `_VERSION` | `gpt-5.4` / `2025-08-07` | **Reasoning model required** — the agent sets `ReasoningEffort=xhigh` in code. gpt-4o / gpt-4 will not work. | +| `AZURE_OPENAI_DEPLOYMENT_NAME` | `gpt-5.4` | Surfaced as `AzureOpenAI__DeploymentName` to the app | | `EXISTING_AOAI_RESOURCE_ID` | _(empty)_ | Full resource ID to reuse an existing AOAI account instead of creating one | -| `AZURE_ENTRA_APP_ID` / `_CLIENT_SECRET` | _(empty)_ | Reuse an existing Entra app instead of creating one | +| `AZURE_ENTRA_APP_ID` / `AZURE_ENTRA_CLIENT_SECRET` | _(empty)_ | Reuse an existing Entra app instead of creating one | Tear down with `azd down --purge` (purge is required because Cognitive Services soft-deletes by default). diff --git a/infra/main-resources.json b/infra/main-resources.json index a745b94..af8f747 100644 --- a/infra/main-resources.json +++ b/infra/main-resources.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "8223718829157142936" + "templateHash": "18137422051515054256" } }, "parameters": { @@ -260,7 +260,7 @@ "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "12521571430439399703" + "templateHash": "11187525182242144731" } }, "parameters": { @@ -314,7 +314,7 @@ "properties": { "customSubDomainName": "[format('aoai-finops-{0}', parameters('resourceToken'))]", "publicNetworkAccess": "Enabled", - "disableLocalAuth": false + "disableLocalAuth": true } }, { diff --git a/infra/main.bicep b/infra/main.bicep index d7bebe0..fdc0b6c 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -18,14 +18,14 @@ param aoaiLocation string = 'swedencentral' @description('App Service Plan SKU. B1 (~$13/mo) is the recommended evaluation default; P0V3 matches production.') param appServicePlanSku string = 'B1' -@description('Azure OpenAI model name to deploy (must be available in `aoaiLocation`).') -param aoaiModelName string = 'gpt-4o' +@description('Azure OpenAI model name to deploy (must be available in `aoaiLocation`). The agent requires a reasoning model — `ReasoningEffort=xhigh` is set in code (see CopilotSessionFactory.cs). gpt-4o / gpt-4 will not work.') +param aoaiModelName string = 'gpt-5.4' @description('Azure OpenAI model version (use the latest GA version for the chosen model).') -param aoaiModelVersion string = '2024-11-20' +param aoaiModelVersion string = '2025-08-07' @description('Azure OpenAI deployment name surfaced as `AzureOpenAI__DeploymentName` to the app.') -param aoaiDeploymentName string = 'gpt-4o' +param aoaiDeploymentName string = 'gpt-5.4' @description('Azure OpenAI model deployment capacity (TPM in thousands).') param aoaiModelCapacity int = 30 diff --git a/infra/main.json b/infra/main.json index 0f59828..e6db492 100644 --- a/infra/main.json +++ b/infra/main.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "17194863635066490569" + "templateHash": "11102783782836268633" } }, "parameters": { @@ -52,21 +52,21 @@ }, "aoaiModelName": { "type": "string", - "defaultValue": "gpt-4o", + "defaultValue": "gpt-5.4", "metadata": { - "description": "Azure OpenAI model name to deploy (must be available in `aoaiLocation`)." + "description": "Azure OpenAI model name to deploy (must be available in `aoaiLocation`). The agent requires a reasoning model — `ReasoningEffort=xhigh` is set in code (see CopilotSessionFactory.cs). gpt-4o / gpt-4 will not work." } }, "aoaiModelVersion": { "type": "string", - "defaultValue": "2024-11-20", + "defaultValue": "2025-08-07", "metadata": { "description": "Azure OpenAI model version (use the latest GA version for the chosen model)." } }, "aoaiDeploymentName": { "type": "string", - "defaultValue": "gpt-4o", + "defaultValue": "gpt-5.4", "metadata": { "description": "Azure OpenAI deployment name surfaced as `AzureOpenAI__DeploymentName` to the app." } @@ -180,7 +180,7 @@ "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "8223718829157142936" + "templateHash": "18137422051515054256" } }, "parameters": { @@ -435,7 +435,7 @@ "_generator": { "name": "bicep", "version": "0.41.2.15936", - "templateHash": "12521571430439399703" + "templateHash": "11187525182242144731" } }, "parameters": { @@ -489,7 +489,7 @@ "properties": { "customSubDomainName": "[format('aoai-finops-{0}', parameters('resourceToken'))]", "publicNetworkAccess": "Enabled", - "disableLocalAuth": false + "disableLocalAuth": true } }, { diff --git a/infra/main.parameters.json b/infra/main.parameters.json index 0a1249f..7a4bf5e 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -15,13 +15,13 @@ "value": "${APP_SERVICE_PLAN_SKU=B1}" }, "aoaiModelName": { - "value": "${AZURE_OPENAI_MODEL_NAME=gpt-4o}" + "value": "${AZURE_OPENAI_MODEL_NAME=gpt-5.4}" }, "aoaiModelVersion": { - "value": "${AZURE_OPENAI_MODEL_VERSION=2024-11-20}" + "value": "${AZURE_OPENAI_MODEL_VERSION=2025-08-07}" }, "aoaiDeploymentName": { - "value": "${AZURE_OPENAI_DEPLOYMENT_NAME=gpt-4o}" + "value": "${AZURE_OPENAI_DEPLOYMENT_NAME=gpt-5.4}" }, "aoaiModelCapacity": { "value": "${AZURE_OPENAI_MODEL_CAPACITY=30}" diff --git a/infra/scripts/preprovision.ps1 b/infra/scripts/preprovision.ps1 index f9a7ae3..529ecfe 100644 --- a/infra/scripts/preprovision.ps1 +++ b/infra/scripts/preprovision.ps1 @@ -34,9 +34,32 @@ if ($signedInUser) { # ── 2. Entra app registration ── $envValues = azd env get-values | ConvertFrom-StringData $existingAppId = $envValues['AZURE_ENTRA_APP_ID'] +$existingSecret = $envValues['AZURE_ENTRA_CLIENT_SECRET'] if ($existingAppId -and $existingAppId.Trim('"').Length -gt 0) { - Write-Host " Reusing existing Entra app: $($existingAppId.Trim('"'))" -ForegroundColor Green + $appId = $existingAppId.Trim('"') + $secret = if ($existingSecret) { $existingSecret.Trim('"') } else { '' } + + # Validate the secret is also provided — Bicep would otherwise pass an empty + # string into Microsoft__ClientSecret and OAuth would silently fall back to + # anonymous mode at runtime. + if ([string]::IsNullOrWhiteSpace($secret)) { + Write-Host " AZURE_ENTRA_APP_ID is set but AZURE_ENTRA_CLIENT_SECRET is missing." -ForegroundColor Red + Write-Host " Run: azd env set AZURE_ENTRA_CLIENT_SECRET ''" -ForegroundColor Yellow + exit 1 + } + + # Verify the app actually exists in this tenant and capture its objectId so + # postprovision.ps1 can patch the App Service hostname into the redirect URIs. + Write-Host " Reusing existing Entra app: $appId" -ForegroundColor Green + $existingObjectId = az ad app show --id $appId --query id -o tsv 2>$null + if ($LASTEXITCODE -ne 0 -or -not $existingObjectId) { + Write-Host " App registration $appId not found in tenant $($account.tenantId)." -ForegroundColor Red + Write-Host " Either fix AZURE_ENTRA_APP_ID or unset it (azd env set AZURE_ENTRA_APP_ID '') to create a new one." -ForegroundColor Yellow + exit 1 + } + azd env set AZURE_ENTRA_OBJECT_ID $existingObjectId | Out-Null + Write-Host " Object ID: $existingObjectId (cached for postprovision redirect-URI patch)" -ForegroundColor Gray } else { Write-Host " Creating Entra ID app registration (multi-tenant, 5 consent tiers)..." -ForegroundColor Yellow From 360174a34594540070b30562194619f007ab9716 Mon Sep 17 00:00:00 2001 From: abnaraya Date: Wed, 20 May 2026 13:44:26 +0200 Subject: [PATCH 03/15] untested - azd changes --- .../prompts/sync-upstream-code-safe.prompt.md | 41 +++++++++++++++++++ infra/scripts/preprovision.ps1 | 23 +++++++---- 2 files changed, 57 insertions(+), 7 deletions(-) create mode 100644 .github/prompts/sync-upstream-code-safe.prompt.md diff --git a/.github/prompts/sync-upstream-code-safe.prompt.md b/.github/prompts/sync-upstream-code-safe.prompt.md new file mode 100644 index 0000000..2125e94 --- /dev/null +++ b/.github/prompts/sync-upstream-code-safe.prompt.md @@ -0,0 +1,41 @@ +Safely sync local main with upstream/main while preserving local changes (both committed and uncommitted). + +## Primary Rules (Safety — Non-negotiable) + +1. **Never destroy local changes** — Always preserve uncommitted or unpushed work +2. **Always preview before executing** — Show diffs/logs before running commands +3. **Fail safely** — If uncertain, ask the user rather than proceeding + +## Secondary Rules (Workflow — Follow when possible) + +1. Prefer `git fetch` + `git merge` over `git pull` +2. Use `--no-ff` to preserve branch history +3. Stash untracked files only with explicit user confirmation - require the user to type 'yes', no ther ways are accepted. + +## Tertiary Rules (Optimization — Apply if no conflicts with above) + +1. Automate repetitive steps +2. Suggest cleanup of merge artifacts +3. Provide rollback commands + +## Conflict Resolution + +If rules from different tiers conflict, use the following guidelines to prioritize: +- **Always prefer Primary over Secondary or Tertiary** +- When two Secondary rules conflict, ask the user to decide +- Never skip a Primary rule to optimize for Secondary/Tertiary + +## Edge Cases (Decision Tree) + +| Scenario | Primary Rule | Action | +|----------|--------------|--------| +| Uncommitted changes + upstream conflicts | Preserve local | Stash, sync, pop | +| Force push required | Never destroy | Refuse + explain alternatives | +| Detached HEAD state | Preview first | Show current state, ask to proceed | +| Upstream branch missing| Notify the user and suggest creating a new upstream branch or reconfiguring the remote| + +Final report format: +- What changed (branch alignment ahead/behind counts). +- Whether uncommitted local changes were preserved. +- Whether push is required next. +- Exact next command(s), minimal and safe. diff --git a/infra/scripts/preprovision.ps1 b/infra/scripts/preprovision.ps1 index 529ecfe..db9ba93 100644 --- a/infra/scripts/preprovision.ps1 +++ b/infra/scripts/preprovision.ps1 @@ -32,13 +32,23 @@ if ($signedInUser) { } # ── 2. Entra app registration ── -$envValues = azd env get-values | ConvertFrom-StringData -$existingAppId = $envValues['AZURE_ENTRA_APP_ID'] -$existingSecret = $envValues['AZURE_ENTRA_CLIENT_SECRET'] +function Get-AzdEnvValue { + param([string]$Key) + $val = azd env get-value $Key 2>$null + if ($LASTEXITCODE -ne 0) { return '' } + if ($null -eq $val) { return '' } + return ([string]$val).Trim().Trim('"') +} + +$existingAppId = Get-AzdEnvValue 'AZURE_ENTRA_APP_ID' +$existingSecret = Get-AzdEnvValue 'AZURE_ENTRA_CLIENT_SECRET' +$envName = Get-AzdEnvValue 'AZURE_ENV_NAME' +if ([string]::IsNullOrWhiteSpace($envName)) { $envName = $env:AZURE_ENV_NAME } +if ([string]::IsNullOrWhiteSpace($envName)) { $envName = 'finops-agent' } -if ($existingAppId -and $existingAppId.Trim('"').Length -gt 0) { - $appId = $existingAppId.Trim('"') - $secret = if ($existingSecret) { $existingSecret.Trim('"') } else { '' } +if (-not [string]::IsNullOrWhiteSpace($existingAppId)) { + $appId = $existingAppId + $secret = $existingSecret # Validate the secret is also provided — Bicep would otherwise pass an empty # string into Microsoft__ClientSecret and OAuth would silently fall back to @@ -64,7 +74,6 @@ if ($existingAppId -and $existingAppId.Trim('"').Length -gt 0) { Write-Host " Creating Entra ID app registration (multi-tenant, 5 consent tiers)..." -ForegroundColor Yellow $entraScript = Join-Path $repoRoot 'src/Dashboard/setup-entra-app.ps1' - $envName = $envValues['AZURE_ENV_NAME'].Trim('"') $appName = "Azure FinOps Agent ($envName)" # Production redirect URI is unknown until Bicep runs. Register localhost now; From d6aeb6de17377f8795f81d768b0c166cc7d41495 Mon Sep 17 00:00:00 2001 From: abnaraya Date: Wed, 20 May 2026 13:59:02 +0200 Subject: [PATCH 04/15] docs: add permissions requirements for `azd up` deployment --- .../prompts/sync-upstream-code-safe.prompt.md | 41 -------------- README.md | 4 ++ docs/azd-up-permissions.md | 53 +++++++++++++++++++ 3 files changed, 57 insertions(+), 41 deletions(-) delete mode 100644 .github/prompts/sync-upstream-code-safe.prompt.md create mode 100644 docs/azd-up-permissions.md diff --git a/.github/prompts/sync-upstream-code-safe.prompt.md b/.github/prompts/sync-upstream-code-safe.prompt.md deleted file mode 100644 index 2125e94..0000000 --- a/.github/prompts/sync-upstream-code-safe.prompt.md +++ /dev/null @@ -1,41 +0,0 @@ -Safely sync local main with upstream/main while preserving local changes (both committed and uncommitted). - -## Primary Rules (Safety — Non-negotiable) - -1. **Never destroy local changes** — Always preserve uncommitted or unpushed work -2. **Always preview before executing** — Show diffs/logs before running commands -3. **Fail safely** — If uncertain, ask the user rather than proceeding - -## Secondary Rules (Workflow — Follow when possible) - -1. Prefer `git fetch` + `git merge` over `git pull` -2. Use `--no-ff` to preserve branch history -3. Stash untracked files only with explicit user confirmation - require the user to type 'yes', no ther ways are accepted. - -## Tertiary Rules (Optimization — Apply if no conflicts with above) - -1. Automate repetitive steps -2. Suggest cleanup of merge artifacts -3. Provide rollback commands - -## Conflict Resolution - -If rules from different tiers conflict, use the following guidelines to prioritize: -- **Always prefer Primary over Secondary or Tertiary** -- When two Secondary rules conflict, ask the user to decide -- Never skip a Primary rule to optimize for Secondary/Tertiary - -## Edge Cases (Decision Tree) - -| Scenario | Primary Rule | Action | -|----------|--------------|--------| -| Uncommitted changes + upstream conflicts | Preserve local | Stash, sync, pop | -| Force push required | Never destroy | Refuse + explain alternatives | -| Detached HEAD state | Preview first | Show current state, ask to proceed | -| Upstream branch missing| Notify the user and suggest creating a new upstream branch or reconfiguring the remote| - -Final report format: -- What changed (branch alignment ahead/behind counts). -- Whether uncommitted local changes were preserved. -- Whether push is required next. -- Exact next command(s), minimal and safe. diff --git a/README.md b/README.md index 383d44e..4579e7d 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,10 @@ Override defaults via `azd env set` before running `azd up`: Tear down with `azd down --purge` (purge is required because Cognitive Services soft-deletes by default). +### Required permissions + +`azd up` needs rights on the Azure subscription **and** the Microsoft Entra tenant. The easiest combination is **Owner** on the subscription plus the ability to **create app registrations** in the tenant. For the least-privilege breakdown (Contributor + User Access Administrator, Application Administrator role, resource providers, quota), see [docs/azd-up-permissions.md](docs/azd-up-permissions.md). + ## Running Locally ### Prerequisites diff --git a/docs/azd-up-permissions.md b/docs/azd-up-permissions.md new file mode 100644 index 0000000..a4e59cd --- /dev/null +++ b/docs/azd-up-permissions.md @@ -0,0 +1,53 @@ +# Permissions required to run `azd up` + +To deploy this repo with `azd up`, the signed-in identity needs permissions on three planes: the Azure subscription, the Microsoft Entra tenant, and (implicitly) the resource providers used by the Bicep templates. + +## 1. Azure subscription + +The Bicep in [infra/main.bicep](../infra/main.bicep) targets **subscription scope** (it creates the resource group) and assigns RBAC roles to the Web App's managed identity, so the deployer needs both resource-management rights and role-assignment rights. + +**Recommended:** + +- **Owner** on the target subscription — satisfies everything below in one role. + +**Minimum (least privilege):** + +- **Contributor** on the subscription — to create the RG and all resources (ACR, App Service Plan + Web App, Azure OpenAI / Cognitive Services account + model deployment, Log Analytics workspace, Application Insights), **plus** +- **User Access Administrator** (or **Role Based Access Control Administrator**) on the subscription — required because [infra/modules/roles.bicep](../infra/modules/roles.bicep) and [infra/modules/roles-aoai.bicep](../infra/modules/roles-aoai.bicep) create role assignments (`AcrPull` on ACR and `Cognitive Services User` on the AOAI account) for the Web App's system-assigned managed identity. + +You also need available **quota** for: + +- Azure OpenAI in `aoaiLocation` (default `swedencentral`, 30K TPM for `gpt-5.4`). +- The chosen App Service Plan SKU in `location` (default `B1`). + +## 2. Microsoft Entra ID (tenant) + +The preprovision hook ([infra/scripts/preprovision.ps1](../infra/scripts/preprovision.ps1)) calls [src/Dashboard/setup-entra-app.ps1](../src/Dashboard/setup-entra-app.ps1) to create a multi-tenant app registration and client secret. That requires one of: + +- The **Application Administrator** or **Cloud Application Administrator** directory role, **or** +- The tenant setting **"Users can register applications" = Yes** (default in many tenants), in which case any user can register apps. + +If your tenant blocks app registration and you don't have those roles, pre-create the app yourself and seed the values before running `azd up`: + +```powershell +azd env set AZURE_ENTRA_APP_ID '' +azd env set AZURE_ENTRA_CLIENT_SECRET '' +``` + +The preprovision hook detects these values and skips Entra creation. + +## 3. Resource providers + +The first `azd up` in a fresh subscription will register the following providers (Contributor is sufficient): + +- `Microsoft.ContainerRegistry` +- `Microsoft.Web` +- `Microsoft.CognitiveServices` +- `Microsoft.OperationalInsights` +- `Microsoft.Insights` +- `Microsoft.ManagedIdentity` +- `Microsoft.Authorization` + +## TL;DR + +Easiest working combination: **Owner on the subscription** + ability to **create app registrations in the tenant**. Everything else (image build via `az acr build` in the postdeploy hook, redirect-URI patching, role assignments) inherits from those. From 76ee63b1460a31fbf7ceb681f883b91c7bd75714 Mon Sep 17 00:00:00 2001 From: Ali Reza Farahnak Date: Tue, 30 Jun 2026 21:46:54 +0200 Subject: [PATCH 05/15] chore: tidy .gitignore and add social-preview upload script Ignore compiled Bicep ARM output (keep *.parameters.json) and untrack the 3 stray artifacts (main.json, main-resources.json, modules/appservice.json). Ignore .pw-profile/ (Playwright profile w/ cookies), *.lscache, and .pw-social-preview-result.png. Add upload-social-preview.mjs maintainer utility. --- .gitignore | 10 + infra/main-resources.json | 761 ------------------------- infra/main.json | 1005 --------------------------------- infra/modules/appservice.json | 157 ----- upload-social-preview.mjs | 64 +++ 5 files changed, 74 insertions(+), 1923 deletions(-) delete mode 100644 infra/main-resources.json delete mode 100644 infra/main.json delete mode 100644 infra/modules/appservice.json create mode 100644 upload-social-preview.mjs diff --git a/.gitignore b/.gitignore index dd77140..dc3148c 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.suo *.user *.userosscache +*.lscache *.sln.docstates [Dd]ebug/ [Rr]elease/ @@ -29,6 +30,10 @@ build/ ## Playwright MCP .playwright-mcp/ +## Playwright persistent browser profile (holds session cookies / sign-in state — never commit) +.pw-profile/ +## Generated by upload-social-preview.mjs on each run +.pw-social-preview-result.png ## OS files .DS_Store @@ -72,6 +77,11 @@ secrets.json ## azd local environment state (per-developer; contains secrets like AZURE_ENTRA_CLIENT_SECRET) .azure/ +## Compiled Bicep ARM output — azd compiles .bicep on the fly, so these are +## build artifacts that drift from source. Keep *.parameters.json (azd needs it). +infra/**/*.json +!infra/**/*.parameters.json + .copilot/skills/agent-collaboration/SKILL.md .gitignore .copilot/skills/squad-conventions/SKILL.md diff --git a/infra/main-resources.json b/infra/main-resources.json deleted file mode 100644 index af8f747..0000000 --- a/infra/main-resources.json +++ /dev/null @@ -1,761 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "18137422051515054256" - } - }, - "parameters": { - "location": { - "type": "string" - }, - "aoaiLocation": { - "type": "string" - }, - "resourceToken": { - "type": "string" - }, - "tags": { - "type": "object" - }, - "appServicePlanSku": { - "type": "string" - }, - "aoaiModelName": { - "type": "string" - }, - "aoaiModelVersion": { - "type": "string" - }, - "aoaiDeploymentName": { - "type": "string" - }, - "aoaiModelCapacity": { - "type": "int" - }, - "existingAoaiResourceId": { - "type": "string" - }, - "entraAppId": { - "type": "string" - }, - "entraClientSecret": { - "type": "securestring" - }, - "entraTenantId": { - "type": "string" - } - }, - "variables": { - "containerImageName": "finops-agent:latest" - }, - "resources": [ - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "monitoring", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "location": { - "value": "[parameters('location')]" - }, - "resourceToken": { - "value": "[parameters('resourceToken')]" - }, - "tags": { - "value": "[parameters('tags')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "4468647702531648630" - } - }, - "parameters": { - "location": { - "type": "string" - }, - "resourceToken": { - "type": "string" - }, - "tags": { - "type": "object" - } - }, - "resources": [ - { - "type": "Microsoft.OperationalInsights/workspaces", - "apiVersion": "2025-07-01", - "name": "[format('log-finops-{0}', parameters('resourceToken'))]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "properties": { - "sku": { - "name": "PerGB2018" - }, - "retentionInDays": 30, - "features": { - "enableLogAccessUsingOnlyResourcePermissions": true - } - } - }, - { - "type": "Microsoft.Insights/components", - "apiVersion": "2020-02-02", - "name": "[format('appi-finops-{0}', parameters('resourceToken'))]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "kind": "web", - "properties": { - "Application_Type": "web", - "WorkspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', format('log-finops-{0}', parameters('resourceToken')))]", - "IngestionMode": "LogAnalytics", - "publicNetworkAccessForIngestion": "Enabled", - "publicNetworkAccessForQuery": "Enabled" - }, - "dependsOn": [ - "[resourceId('Microsoft.OperationalInsights/workspaces', format('log-finops-{0}', parameters('resourceToken')))]" - ] - } - ], - "outputs": { - "logAnalyticsWorkspaceId": { - "type": "string", - "value": "[resourceId('Microsoft.OperationalInsights/workspaces', format('log-finops-{0}', parameters('resourceToken')))]" - }, - "appInsightsConnectionString": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Insights/components', format('appi-finops-{0}', parameters('resourceToken'))), '2020-02-02').ConnectionString]" - } - } - } - } - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "acr", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "location": { - "value": "[parameters('location')]" - }, - "resourceToken": { - "value": "[parameters('resourceToken')]" - }, - "tags": { - "value": "[parameters('tags')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "13570533503122721445" - } - }, - "parameters": { - "location": { - "type": "string" - }, - "resourceToken": { - "type": "string" - }, - "tags": { - "type": "object" - } - }, - "resources": [ - { - "type": "Microsoft.ContainerRegistry/registries", - "apiVersion": "2024-11-01-preview", - "name": "[format('crfinops{0}', parameters('resourceToken'))]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "sku": { - "name": "Basic" - }, - "properties": { - "adminUserEnabled": false, - "anonymousPullEnabled": false, - "publicNetworkAccess": "Enabled" - } - } - ], - "outputs": { - "name": { - "type": "string", - "value": "[format('crfinops{0}', parameters('resourceToken'))]" - }, - "loginServer": { - "type": "string", - "value": "[reference(resourceId('Microsoft.ContainerRegistry/registries', format('crfinops{0}', parameters('resourceToken'))), '2024-11-01-preview').loginServer]" - }, - "id": { - "type": "string", - "value": "[resourceId('Microsoft.ContainerRegistry/registries', format('crfinops{0}', parameters('resourceToken')))]" - } - } - } - } - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "aoai", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "aoaiLocation": { - "value": "[parameters('aoaiLocation')]" - }, - "resourceToken": { - "value": "[parameters('resourceToken')]" - }, - "tags": { - "value": "[parameters('tags')]" - }, - "modelName": { - "value": "[parameters('aoaiModelName')]" - }, - "modelVersion": { - "value": "[parameters('aoaiModelVersion')]" - }, - "deploymentName": { - "value": "[parameters('aoaiDeploymentName')]" - }, - "modelCapacity": { - "value": "[parameters('aoaiModelCapacity')]" - }, - "existingAoaiResourceId": { - "value": "[parameters('existingAoaiResourceId')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "11187525182242144731" - } - }, - "parameters": { - "aoaiLocation": { - "type": "string" - }, - "resourceToken": { - "type": "string" - }, - "tags": { - "type": "object" - }, - "modelName": { - "type": "string" - }, - "modelVersion": { - "type": "string" - }, - "deploymentName": { - "type": "string" - }, - "modelCapacity": { - "type": "int" - }, - "existingAoaiResourceId": { - "type": "string" - } - }, - "variables": { - "useExisting": "[not(empty(parameters('existingAoaiResourceId')))]", - "existingSegments": "[split(parameters('existingAoaiResourceId'), '/')]", - "existingSubId": "[if(variables('useExisting'), variables('existingSegments')[2], subscription().subscriptionId)]", - "existingRg": "[if(variables('useExisting'), variables('existingSegments')[4], resourceGroup().name)]", - "existingName": "[if(variables('useExisting'), variables('existingSegments')[8], '')]" - }, - "resources": [ - { - "condition": "[not(variables('useExisting'))]", - "type": "Microsoft.CognitiveServices/accounts", - "apiVersion": "2025-04-01-preview", - "name": "[format('aoai-finops-{0}', parameters('resourceToken'))]", - "location": "[parameters('aoaiLocation')]", - "tags": "[parameters('tags')]", - "kind": "OpenAI", - "sku": { - "name": "S0" - }, - "identity": { - "type": "SystemAssigned" - }, - "properties": { - "customSubDomainName": "[format('aoai-finops-{0}', parameters('resourceToken'))]", - "publicNetworkAccess": "Enabled", - "disableLocalAuth": true - } - }, - { - "condition": "[not(variables('useExisting'))]", - "type": "Microsoft.CognitiveServices/accounts/deployments", - "apiVersion": "2025-04-01-preview", - "name": "[format('{0}/{1}', format('aoai-finops-{0}', parameters('resourceToken')), parameters('deploymentName'))]", - "sku": { - "name": "GlobalStandard", - "capacity": "[parameters('modelCapacity')]" - }, - "properties": { - "model": { - "format": "OpenAI", - "name": "[parameters('modelName')]", - "version": "[parameters('modelVersion')]" - } - }, - "dependsOn": [ - "[resourceId('Microsoft.CognitiveServices/accounts', format('aoai-finops-{0}', parameters('resourceToken')))]" - ] - } - ], - "outputs": { - "endpoint": { - "type": "string", - "value": "[if(variables('useExisting'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('existingSubId'), variables('existingRg')), 'Microsoft.CognitiveServices/accounts', variables('existingName')), '2025-04-01-preview').endpoint, reference(resourceId('Microsoft.CognitiveServices/accounts', format('aoai-finops-{0}', parameters('resourceToken'))), '2025-04-01-preview').endpoint)]" - }, - "accountName": { - "type": "string", - "value": "[if(variables('useExisting'), variables('existingName'), format('aoai-finops-{0}', parameters('resourceToken')))]" - }, - "deploymentName": { - "type": "string", - "value": "[parameters('deploymentName')]" - }, - "resourceGroup": { - "type": "string", - "value": "[if(variables('useExisting'), variables('existingRg'), resourceGroup().name)]" - }, - "subscriptionId": { - "type": "string", - "value": "[if(variables('useExisting'), variables('existingSubId'), subscription().subscriptionId)]" - } - } - } - } - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "appservice", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "location": { - "value": "[parameters('location')]" - }, - "resourceToken": { - "value": "[parameters('resourceToken')]" - }, - "tags": { - "value": "[parameters('tags')]" - }, - "appServicePlanSku": { - "value": "[parameters('appServicePlanSku')]" - }, - "acrLoginServer": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'acr'), '2025-04-01').outputs.loginServer.value]" - }, - "containerImageName": { - "value": "[variables('containerImageName')]" - }, - "appInsightsConnectionString": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.appInsightsConnectionString.value]" - }, - "aoaiEndpoint": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'aoai'), '2025-04-01').outputs.endpoint.value]" - }, - "aoaiDeploymentName": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'aoai'), '2025-04-01').outputs.deploymentName.value]" - }, - "entraAppId": { - "value": "[parameters('entraAppId')]" - }, - "entraClientSecret": { - "value": "[parameters('entraClientSecret')]" - }, - "entraTenantId": { - "value": "[parameters('entraTenantId')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "10279812525912434775" - } - }, - "parameters": { - "location": { - "type": "string" - }, - "resourceToken": { - "type": "string" - }, - "tags": { - "type": "object" - }, - "appServicePlanSku": { - "type": "string" - }, - "acrLoginServer": { - "type": "string" - }, - "containerImageName": { - "type": "string" - }, - "appInsightsConnectionString": { - "type": "securestring" - }, - "aoaiEndpoint": { - "type": "string" - }, - "aoaiDeploymentName": { - "type": "string" - }, - "entraAppId": { - "type": "string" - }, - "entraClientSecret": { - "type": "securestring" - }, - "entraTenantId": { - "type": "string" - } - }, - "variables": { - "planTier": "[if(startsWith(parameters('appServicePlanSku'), 'B'), 'Basic', if(startsWith(parameters('appServicePlanSku'), 'S'), 'Standard', 'PremiumV3'))]" - }, - "resources": [ - { - "type": "Microsoft.Web/serverfarms", - "apiVersion": "2024-04-01", - "name": "[format('plan-finops-{0}', parameters('resourceToken'))]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "kind": "linux", - "sku": { - "name": "[parameters('appServicePlanSku')]", - "tier": "[variables('planTier')]" - }, - "properties": { - "reserved": true - } - }, - { - "type": "Microsoft.Web/sites", - "apiVersion": "2024-04-01", - "name": "[format('app-finops-{0}', parameters('resourceToken'))]", - "location": "[parameters('location')]", - "tags": "[union(parameters('tags'), createObject('azd-service-name', 'web'))]", - "kind": "app,linux,container", - "identity": { - "type": "SystemAssigned" - }, - "properties": { - "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', format('plan-finops-{0}', parameters('resourceToken')))]", - "httpsOnly": true, - "publicNetworkAccess": "Enabled", - "siteConfig": { - "linuxFxVersion": "[format('DOCKER|{0}/{1}', parameters('acrLoginServer'), parameters('containerImageName'))]", - "acrUseManagedIdentityCreds": true, - "alwaysOn": "[and(not(equals(parameters('appServicePlanSku'), 'F1')), not(startsWith(parameters('appServicePlanSku'), 'B')))]", - "http20Enabled": true, - "ftpsState": "Disabled", - "minTlsVersion": "1.2", - "healthCheckPath": "/api/version", - "appSettings": [ - { - "name": "WEBSITES_PORT", - "value": "8080" - }, - { - "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE", - "value": "false" - }, - { - "name": "DOCKER_REGISTRY_SERVER_URL", - "value": "[format('https://{0}', parameters('acrLoginServer'))]" - }, - { - "name": "AzureOpenAI__Endpoint", - "value": "[parameters('aoaiEndpoint')]" - }, - { - "name": "AzureOpenAI__DeploymentName", - "value": "[parameters('aoaiDeploymentName')]" - }, - { - "name": "Microsoft__ClientId", - "value": "[parameters('entraAppId')]" - }, - { - "name": "Microsoft__ClientSecret", - "value": "[parameters('entraClientSecret')]" - }, - { - "name": "Microsoft__TenantId", - "value": "[parameters('entraTenantId')]" - }, - { - "name": "ApplicationInsights__ConnectionString", - "value": "[parameters('appInsightsConnectionString')]" - }, - { - "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", - "value": "[parameters('appInsightsConnectionString')]" - }, - { - "name": "ASPNETCORE_ENVIRONMENT", - "value": "Production" - } - ] - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms', format('plan-finops-{0}', parameters('resourceToken')))]" - ] - } - ], - "outputs": { - "name": { - "type": "string", - "value": "[format('app-finops-{0}', parameters('resourceToken'))]" - }, - "hostname": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Web/sites', format('app-finops-{0}', parameters('resourceToken'))), '2024-04-01').defaultHostName]" - }, - "principalId": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Web/sites', format('app-finops-{0}', parameters('resourceToken'))), '2024-04-01', 'full').identity.principalId]" - } - } - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'acr')]", - "[resourceId('Microsoft.Resources/deployments', 'aoai')]", - "[resourceId('Microsoft.Resources/deployments', 'monitoring')]" - ] - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "roles", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "webAppPrincipalId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'appservice'), '2025-04-01').outputs.principalId.value]" - }, - "acrName": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'acr'), '2025-04-01').outputs.name.value]" - }, - "aoaiName": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'aoai'), '2025-04-01').outputs.accountName.value]" - }, - "aoaiResourceGroup": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'aoai'), '2025-04-01').outputs.resourceGroup.value]" - }, - "aoaiSubscriptionId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'aoai'), '2025-04-01').outputs.subscriptionId.value]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "4472192890205802557" - } - }, - "parameters": { - "webAppPrincipalId": { - "type": "string" - }, - "acrName": { - "type": "string" - }, - "aoaiName": { - "type": "string" - }, - "aoaiResourceGroup": { - "type": "string" - }, - "aoaiSubscriptionId": { - "type": "string" - } - }, - "variables": { - "acrPullRoleId": "7f951dda-4ed3-4680-a7ca-43fe172d538d", - "cognitiveServicesUserRoleId": "a97b65f3-24c7-4388-baec-2e87135dc908" - }, - "resources": [ - { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]", - "name": "[guid(resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName')), parameters('webAppPrincipalId'), variables('acrPullRoleId'))]", - "properties": { - "principalId": "[parameters('webAppPrincipalId')]", - "principalType": "ServicePrincipal", - "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('acrPullRoleId'))]" - } - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "aoai-role", - "subscriptionId": "[parameters('aoaiSubscriptionId')]", - "resourceGroup": "[parameters('aoaiResourceGroup')]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "aoaiName": { - "value": "[parameters('aoaiName')]" - }, - "webAppPrincipalId": { - "value": "[parameters('webAppPrincipalId')]" - }, - "cognitiveServicesUserRoleId": { - "value": "[variables('cognitiveServicesUserRoleId')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "16264163075713442013" - } - }, - "parameters": { - "aoaiName": { - "type": "string" - }, - "webAppPrincipalId": { - "type": "string" - }, - "cognitiveServicesUserRoleId": { - "type": "string" - } - }, - "resources": [ - { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('aoaiName'))]", - "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('aoaiName')), parameters('webAppPrincipalId'), parameters('cognitiveServicesUserRoleId'))]", - "properties": { - "principalId": "[parameters('webAppPrincipalId')]", - "principalType": "ServicePrincipal", - "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', parameters('cognitiveServicesUserRoleId'))]" - } - } - ] - } - } - } - ] - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'acr')]", - "[resourceId('Microsoft.Resources/deployments', 'aoai')]", - "[resourceId('Microsoft.Resources/deployments', 'appservice')]" - ] - } - ], - "outputs": { - "acrName": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'acr'), '2025-04-01').outputs.name.value]" - }, - "acrLoginServer": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'acr'), '2025-04-01').outputs.loginServer.value]" - }, - "containerImageName": { - "type": "string", - "value": "[variables('containerImageName')]" - }, - "webAppName": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'appservice'), '2025-04-01').outputs.name.value]" - }, - "webAppHostname": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'appservice'), '2025-04-01').outputs.hostname.value]" - }, - "webAppUrl": { - "type": "string", - "value": "[format('https://{0}', reference(resourceId('Microsoft.Resources/deployments', 'appservice'), '2025-04-01').outputs.hostname.value)]" - }, - "webAppPrincipalId": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'appservice'), '2025-04-01').outputs.principalId.value]" - }, - "aoaiEndpoint": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'aoai'), '2025-04-01').outputs.endpoint.value]" - }, - "aoaiDeploymentName": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'aoai'), '2025-04-01').outputs.deploymentName.value]" - }, - "appInsightsConnectionString": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.appInsightsConnectionString.value]" - }, - "logAnalyticsWorkspaceId": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.logAnalyticsWorkspaceId.value]" - } - } -} \ No newline at end of file diff --git a/infra/main.json b/infra/main.json deleted file mode 100644 index e6db492..0000000 --- a/infra/main.json +++ /dev/null @@ -1,1005 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2018-05-01/subscriptionDeploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "11102783782836268633" - } - }, - "parameters": { - "environmentName": { - "type": "string", - "minLength": 1, - "maxLength": 64, - "metadata": { - "description": "Name of the azd environment. Used to derive resource names and tags." - } - }, - "location": { - "type": "string", - "minLength": 1, - "metadata": { - "description": "Primary Azure region for the resource group and most resources (e.g. swedencentral, eastus2)." - } - }, - "aoaiLocation": { - "type": "string", - "defaultValue": "swedencentral", - "metadata": { - "description": "Azure region for the Azure OpenAI account. Often differs from `location` because model availability is region-restricted. Default swedencentral has broad model coverage." - } - }, - "appServicePlanSku": { - "type": "string", - "defaultValue": "B1", - "allowedValues": [ - "B1", - "B2", - "B3", - "S1", - "S2", - "S3", - "P0V3", - "P1V3", - "P2V3", - "P3V3" - ], - "metadata": { - "description": "App Service Plan SKU. B1 (~$13/mo) is the recommended evaluation default; P0V3 matches production." - } - }, - "aoaiModelName": { - "type": "string", - "defaultValue": "gpt-5.4", - "metadata": { - "description": "Azure OpenAI model name to deploy (must be available in `aoaiLocation`). The agent requires a reasoning model — `ReasoningEffort=xhigh` is set in code (see CopilotSessionFactory.cs). gpt-4o / gpt-4 will not work." - } - }, - "aoaiModelVersion": { - "type": "string", - "defaultValue": "2025-08-07", - "metadata": { - "description": "Azure OpenAI model version (use the latest GA version for the chosen model)." - } - }, - "aoaiDeploymentName": { - "type": "string", - "defaultValue": "gpt-5.4", - "metadata": { - "description": "Azure OpenAI deployment name surfaced as `AzureOpenAI__DeploymentName` to the app." - } - }, - "aoaiModelCapacity": { - "type": "int", - "defaultValue": 30, - "metadata": { - "description": "Azure OpenAI model deployment capacity (TPM in thousands)." - } - }, - "existingAoaiResourceId": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Optional resource ID of an existing Azure OpenAI account to reuse instead of creating a new one. When set, `aoaiLocation`/`aoaiModelName`/`aoaiModelVersion` are ignored — the deployment must already exist on the existing account." - } - }, - "entraAppId": { - "type": "string", - "defaultValue": "", - "metadata": { - "description": "Entra ID multi-tenant app registration client ID. Created automatically by the preprovision hook if empty." - } - }, - "entraClientSecret": { - "type": "securestring", - "defaultValue": "", - "metadata": { - "description": "Entra ID app registration client secret. Created automatically by the preprovision hook if empty." - } - }, - "entraTenantId": { - "type": "string", - "defaultValue": "common", - "metadata": { - "description": "Entra tenant ID for OAuth — `common` for multi-tenant. Leave default unless restricting to a single tenant." - } - } - }, - "variables": { - "tags": { - "azd-env-name": "[parameters('environmentName')]", - "application": "azure-finops-agent" - }, - "resourceToken": "[toLower(uniqueString(subscription().id, parameters('environmentName'), parameters('location')))]" - }, - "resources": [ - { - "type": "Microsoft.Resources/resourceGroups", - "apiVersion": "2023-07-01", - "name": "[format('rg-{0}', parameters('environmentName'))]", - "location": "[parameters('location')]", - "tags": "[variables('tags')]" - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "finops-resources", - "resourceGroup": "[format('rg-{0}', parameters('environmentName'))]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "location": { - "value": "[parameters('location')]" - }, - "aoaiLocation": { - "value": "[parameters('aoaiLocation')]" - }, - "resourceToken": { - "value": "[variables('resourceToken')]" - }, - "tags": { - "value": "[variables('tags')]" - }, - "appServicePlanSku": { - "value": "[parameters('appServicePlanSku')]" - }, - "aoaiModelName": { - "value": "[parameters('aoaiModelName')]" - }, - "aoaiModelVersion": { - "value": "[parameters('aoaiModelVersion')]" - }, - "aoaiDeploymentName": { - "value": "[parameters('aoaiDeploymentName')]" - }, - "aoaiModelCapacity": { - "value": "[parameters('aoaiModelCapacity')]" - }, - "existingAoaiResourceId": { - "value": "[parameters('existingAoaiResourceId')]" - }, - "entraAppId": { - "value": "[parameters('entraAppId')]" - }, - "entraClientSecret": { - "value": "[parameters('entraClientSecret')]" - }, - "entraTenantId": { - "value": "[parameters('entraTenantId')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "18137422051515054256" - } - }, - "parameters": { - "location": { - "type": "string" - }, - "aoaiLocation": { - "type": "string" - }, - "resourceToken": { - "type": "string" - }, - "tags": { - "type": "object" - }, - "appServicePlanSku": { - "type": "string" - }, - "aoaiModelName": { - "type": "string" - }, - "aoaiModelVersion": { - "type": "string" - }, - "aoaiDeploymentName": { - "type": "string" - }, - "aoaiModelCapacity": { - "type": "int" - }, - "existingAoaiResourceId": { - "type": "string" - }, - "entraAppId": { - "type": "string" - }, - "entraClientSecret": { - "type": "securestring" - }, - "entraTenantId": { - "type": "string" - } - }, - "variables": { - "containerImageName": "finops-agent:latest" - }, - "resources": [ - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "monitoring", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "location": { - "value": "[parameters('location')]" - }, - "resourceToken": { - "value": "[parameters('resourceToken')]" - }, - "tags": { - "value": "[parameters('tags')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "4468647702531648630" - } - }, - "parameters": { - "location": { - "type": "string" - }, - "resourceToken": { - "type": "string" - }, - "tags": { - "type": "object" - } - }, - "resources": [ - { - "type": "Microsoft.OperationalInsights/workspaces", - "apiVersion": "2025-07-01", - "name": "[format('log-finops-{0}', parameters('resourceToken'))]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "properties": { - "sku": { - "name": "PerGB2018" - }, - "retentionInDays": 30, - "features": { - "enableLogAccessUsingOnlyResourcePermissions": true - } - } - }, - { - "type": "Microsoft.Insights/components", - "apiVersion": "2020-02-02", - "name": "[format('appi-finops-{0}', parameters('resourceToken'))]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "kind": "web", - "properties": { - "Application_Type": "web", - "WorkspaceResourceId": "[resourceId('Microsoft.OperationalInsights/workspaces', format('log-finops-{0}', parameters('resourceToken')))]", - "IngestionMode": "LogAnalytics", - "publicNetworkAccessForIngestion": "Enabled", - "publicNetworkAccessForQuery": "Enabled" - }, - "dependsOn": [ - "[resourceId('Microsoft.OperationalInsights/workspaces', format('log-finops-{0}', parameters('resourceToken')))]" - ] - } - ], - "outputs": { - "logAnalyticsWorkspaceId": { - "type": "string", - "value": "[resourceId('Microsoft.OperationalInsights/workspaces', format('log-finops-{0}', parameters('resourceToken')))]" - }, - "appInsightsConnectionString": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Insights/components', format('appi-finops-{0}', parameters('resourceToken'))), '2020-02-02').ConnectionString]" - } - } - } - } - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "acr", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "location": { - "value": "[parameters('location')]" - }, - "resourceToken": { - "value": "[parameters('resourceToken')]" - }, - "tags": { - "value": "[parameters('tags')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "13570533503122721445" - } - }, - "parameters": { - "location": { - "type": "string" - }, - "resourceToken": { - "type": "string" - }, - "tags": { - "type": "object" - } - }, - "resources": [ - { - "type": "Microsoft.ContainerRegistry/registries", - "apiVersion": "2024-11-01-preview", - "name": "[format('crfinops{0}', parameters('resourceToken'))]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "sku": { - "name": "Basic" - }, - "properties": { - "adminUserEnabled": false, - "anonymousPullEnabled": false, - "publicNetworkAccess": "Enabled" - } - } - ], - "outputs": { - "name": { - "type": "string", - "value": "[format('crfinops{0}', parameters('resourceToken'))]" - }, - "loginServer": { - "type": "string", - "value": "[reference(resourceId('Microsoft.ContainerRegistry/registries', format('crfinops{0}', parameters('resourceToken'))), '2024-11-01-preview').loginServer]" - }, - "id": { - "type": "string", - "value": "[resourceId('Microsoft.ContainerRegistry/registries', format('crfinops{0}', parameters('resourceToken')))]" - } - } - } - } - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "aoai", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "aoaiLocation": { - "value": "[parameters('aoaiLocation')]" - }, - "resourceToken": { - "value": "[parameters('resourceToken')]" - }, - "tags": { - "value": "[parameters('tags')]" - }, - "modelName": { - "value": "[parameters('aoaiModelName')]" - }, - "modelVersion": { - "value": "[parameters('aoaiModelVersion')]" - }, - "deploymentName": { - "value": "[parameters('aoaiDeploymentName')]" - }, - "modelCapacity": { - "value": "[parameters('aoaiModelCapacity')]" - }, - "existingAoaiResourceId": { - "value": "[parameters('existingAoaiResourceId')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "11187525182242144731" - } - }, - "parameters": { - "aoaiLocation": { - "type": "string" - }, - "resourceToken": { - "type": "string" - }, - "tags": { - "type": "object" - }, - "modelName": { - "type": "string" - }, - "modelVersion": { - "type": "string" - }, - "deploymentName": { - "type": "string" - }, - "modelCapacity": { - "type": "int" - }, - "existingAoaiResourceId": { - "type": "string" - } - }, - "variables": { - "useExisting": "[not(empty(parameters('existingAoaiResourceId')))]", - "existingSegments": "[split(parameters('existingAoaiResourceId'), '/')]", - "existingSubId": "[if(variables('useExisting'), variables('existingSegments')[2], subscription().subscriptionId)]", - "existingRg": "[if(variables('useExisting'), variables('existingSegments')[4], resourceGroup().name)]", - "existingName": "[if(variables('useExisting'), variables('existingSegments')[8], '')]" - }, - "resources": [ - { - "condition": "[not(variables('useExisting'))]", - "type": "Microsoft.CognitiveServices/accounts", - "apiVersion": "2025-04-01-preview", - "name": "[format('aoai-finops-{0}', parameters('resourceToken'))]", - "location": "[parameters('aoaiLocation')]", - "tags": "[parameters('tags')]", - "kind": "OpenAI", - "sku": { - "name": "S0" - }, - "identity": { - "type": "SystemAssigned" - }, - "properties": { - "customSubDomainName": "[format('aoai-finops-{0}', parameters('resourceToken'))]", - "publicNetworkAccess": "Enabled", - "disableLocalAuth": true - } - }, - { - "condition": "[not(variables('useExisting'))]", - "type": "Microsoft.CognitiveServices/accounts/deployments", - "apiVersion": "2025-04-01-preview", - "name": "[format('{0}/{1}', format('aoai-finops-{0}', parameters('resourceToken')), parameters('deploymentName'))]", - "sku": { - "name": "GlobalStandard", - "capacity": "[parameters('modelCapacity')]" - }, - "properties": { - "model": { - "format": "OpenAI", - "name": "[parameters('modelName')]", - "version": "[parameters('modelVersion')]" - } - }, - "dependsOn": [ - "[resourceId('Microsoft.CognitiveServices/accounts', format('aoai-finops-{0}', parameters('resourceToken')))]" - ] - } - ], - "outputs": { - "endpoint": { - "type": "string", - "value": "[if(variables('useExisting'), reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', variables('existingSubId'), variables('existingRg')), 'Microsoft.CognitiveServices/accounts', variables('existingName')), '2025-04-01-preview').endpoint, reference(resourceId('Microsoft.CognitiveServices/accounts', format('aoai-finops-{0}', parameters('resourceToken'))), '2025-04-01-preview').endpoint)]" - }, - "accountName": { - "type": "string", - "value": "[if(variables('useExisting'), variables('existingName'), format('aoai-finops-{0}', parameters('resourceToken')))]" - }, - "deploymentName": { - "type": "string", - "value": "[parameters('deploymentName')]" - }, - "resourceGroup": { - "type": "string", - "value": "[if(variables('useExisting'), variables('existingRg'), resourceGroup().name)]" - }, - "subscriptionId": { - "type": "string", - "value": "[if(variables('useExisting'), variables('existingSubId'), subscription().subscriptionId)]" - } - } - } - } - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "appservice", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "location": { - "value": "[parameters('location')]" - }, - "resourceToken": { - "value": "[parameters('resourceToken')]" - }, - "tags": { - "value": "[parameters('tags')]" - }, - "appServicePlanSku": { - "value": "[parameters('appServicePlanSku')]" - }, - "acrLoginServer": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'acr'), '2025-04-01').outputs.loginServer.value]" - }, - "containerImageName": { - "value": "[variables('containerImageName')]" - }, - "appInsightsConnectionString": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.appInsightsConnectionString.value]" - }, - "aoaiEndpoint": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'aoai'), '2025-04-01').outputs.endpoint.value]" - }, - "aoaiDeploymentName": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'aoai'), '2025-04-01').outputs.deploymentName.value]" - }, - "entraAppId": { - "value": "[parameters('entraAppId')]" - }, - "entraClientSecret": { - "value": "[parameters('entraClientSecret')]" - }, - "entraTenantId": { - "value": "[parameters('entraTenantId')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "10279812525912434775" - } - }, - "parameters": { - "location": { - "type": "string" - }, - "resourceToken": { - "type": "string" - }, - "tags": { - "type": "object" - }, - "appServicePlanSku": { - "type": "string" - }, - "acrLoginServer": { - "type": "string" - }, - "containerImageName": { - "type": "string" - }, - "appInsightsConnectionString": { - "type": "securestring" - }, - "aoaiEndpoint": { - "type": "string" - }, - "aoaiDeploymentName": { - "type": "string" - }, - "entraAppId": { - "type": "string" - }, - "entraClientSecret": { - "type": "securestring" - }, - "entraTenantId": { - "type": "string" - } - }, - "variables": { - "planTier": "[if(startsWith(parameters('appServicePlanSku'), 'B'), 'Basic', if(startsWith(parameters('appServicePlanSku'), 'S'), 'Standard', 'PremiumV3'))]" - }, - "resources": [ - { - "type": "Microsoft.Web/serverfarms", - "apiVersion": "2024-04-01", - "name": "[format('plan-finops-{0}', parameters('resourceToken'))]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "kind": "linux", - "sku": { - "name": "[parameters('appServicePlanSku')]", - "tier": "[variables('planTier')]" - }, - "properties": { - "reserved": true - } - }, - { - "type": "Microsoft.Web/sites", - "apiVersion": "2024-04-01", - "name": "[format('app-finops-{0}', parameters('resourceToken'))]", - "location": "[parameters('location')]", - "tags": "[union(parameters('tags'), createObject('azd-service-name', 'web'))]", - "kind": "app,linux,container", - "identity": { - "type": "SystemAssigned" - }, - "properties": { - "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', format('plan-finops-{0}', parameters('resourceToken')))]", - "httpsOnly": true, - "publicNetworkAccess": "Enabled", - "siteConfig": { - "linuxFxVersion": "[format('DOCKER|{0}/{1}', parameters('acrLoginServer'), parameters('containerImageName'))]", - "acrUseManagedIdentityCreds": true, - "alwaysOn": "[and(not(equals(parameters('appServicePlanSku'), 'F1')), not(startsWith(parameters('appServicePlanSku'), 'B')))]", - "http20Enabled": true, - "ftpsState": "Disabled", - "minTlsVersion": "1.2", - "healthCheckPath": "/api/version", - "appSettings": [ - { - "name": "WEBSITES_PORT", - "value": "8080" - }, - { - "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE", - "value": "false" - }, - { - "name": "DOCKER_REGISTRY_SERVER_URL", - "value": "[format('https://{0}', parameters('acrLoginServer'))]" - }, - { - "name": "AzureOpenAI__Endpoint", - "value": "[parameters('aoaiEndpoint')]" - }, - { - "name": "AzureOpenAI__DeploymentName", - "value": "[parameters('aoaiDeploymentName')]" - }, - { - "name": "Microsoft__ClientId", - "value": "[parameters('entraAppId')]" - }, - { - "name": "Microsoft__ClientSecret", - "value": "[parameters('entraClientSecret')]" - }, - { - "name": "Microsoft__TenantId", - "value": "[parameters('entraTenantId')]" - }, - { - "name": "ApplicationInsights__ConnectionString", - "value": "[parameters('appInsightsConnectionString')]" - }, - { - "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", - "value": "[parameters('appInsightsConnectionString')]" - }, - { - "name": "ASPNETCORE_ENVIRONMENT", - "value": "Production" - } - ] - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms', format('plan-finops-{0}', parameters('resourceToken')))]" - ] - } - ], - "outputs": { - "name": { - "type": "string", - "value": "[format('app-finops-{0}', parameters('resourceToken'))]" - }, - "hostname": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Web/sites', format('app-finops-{0}', parameters('resourceToken'))), '2024-04-01').defaultHostName]" - }, - "principalId": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Web/sites', format('app-finops-{0}', parameters('resourceToken'))), '2024-04-01', 'full').identity.principalId]" - } - } - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'acr')]", - "[resourceId('Microsoft.Resources/deployments', 'aoai')]", - "[resourceId('Microsoft.Resources/deployments', 'monitoring')]" - ] - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "roles", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "webAppPrincipalId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'appservice'), '2025-04-01').outputs.principalId.value]" - }, - "acrName": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'acr'), '2025-04-01').outputs.name.value]" - }, - "aoaiName": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'aoai'), '2025-04-01').outputs.accountName.value]" - }, - "aoaiResourceGroup": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'aoai'), '2025-04-01').outputs.resourceGroup.value]" - }, - "aoaiSubscriptionId": { - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'aoai'), '2025-04-01').outputs.subscriptionId.value]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "4472192890205802557" - } - }, - "parameters": { - "webAppPrincipalId": { - "type": "string" - }, - "acrName": { - "type": "string" - }, - "aoaiName": { - "type": "string" - }, - "aoaiResourceGroup": { - "type": "string" - }, - "aoaiSubscriptionId": { - "type": "string" - } - }, - "variables": { - "acrPullRoleId": "7f951dda-4ed3-4680-a7ca-43fe172d538d", - "cognitiveServicesUserRoleId": "a97b65f3-24c7-4388-baec-2e87135dc908" - }, - "resources": [ - { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName'))]", - "name": "[guid(resourceId('Microsoft.ContainerRegistry/registries', parameters('acrName')), parameters('webAppPrincipalId'), variables('acrPullRoleId'))]", - "properties": { - "principalId": "[parameters('webAppPrincipalId')]", - "principalType": "ServicePrincipal", - "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', variables('acrPullRoleId'))]" - } - }, - { - "type": "Microsoft.Resources/deployments", - "apiVersion": "2025-04-01", - "name": "aoai-role", - "subscriptionId": "[parameters('aoaiSubscriptionId')]", - "resourceGroup": "[parameters('aoaiResourceGroup')]", - "properties": { - "expressionEvaluationOptions": { - "scope": "inner" - }, - "mode": "Incremental", - "parameters": { - "aoaiName": { - "value": "[parameters('aoaiName')]" - }, - "webAppPrincipalId": { - "value": "[parameters('webAppPrincipalId')]" - }, - "cognitiveServicesUserRoleId": { - "value": "[variables('cognitiveServicesUserRoleId')]" - } - }, - "template": { - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "16264163075713442013" - } - }, - "parameters": { - "aoaiName": { - "type": "string" - }, - "webAppPrincipalId": { - "type": "string" - }, - "cognitiveServicesUserRoleId": { - "type": "string" - } - }, - "resources": [ - { - "type": "Microsoft.Authorization/roleAssignments", - "apiVersion": "2022-04-01", - "scope": "[resourceId('Microsoft.CognitiveServices/accounts', parameters('aoaiName'))]", - "name": "[guid(resourceId('Microsoft.CognitiveServices/accounts', parameters('aoaiName')), parameters('webAppPrincipalId'), parameters('cognitiveServicesUserRoleId'))]", - "properties": { - "principalId": "[parameters('webAppPrincipalId')]", - "principalType": "ServicePrincipal", - "roleDefinitionId": "[subscriptionResourceId('Microsoft.Authorization/roleDefinitions', parameters('cognitiveServicesUserRoleId'))]" - } - } - ] - } - } - } - ] - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Resources/deployments', 'acr')]", - "[resourceId('Microsoft.Resources/deployments', 'aoai')]", - "[resourceId('Microsoft.Resources/deployments', 'appservice')]" - ] - } - ], - "outputs": { - "acrName": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'acr'), '2025-04-01').outputs.name.value]" - }, - "acrLoginServer": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'acr'), '2025-04-01').outputs.loginServer.value]" - }, - "containerImageName": { - "type": "string", - "value": "[variables('containerImageName')]" - }, - "webAppName": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'appservice'), '2025-04-01').outputs.name.value]" - }, - "webAppHostname": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'appservice'), '2025-04-01').outputs.hostname.value]" - }, - "webAppUrl": { - "type": "string", - "value": "[format('https://{0}', reference(resourceId('Microsoft.Resources/deployments', 'appservice'), '2025-04-01').outputs.hostname.value)]" - }, - "webAppPrincipalId": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'appservice'), '2025-04-01').outputs.principalId.value]" - }, - "aoaiEndpoint": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'aoai'), '2025-04-01').outputs.endpoint.value]" - }, - "aoaiDeploymentName": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'aoai'), '2025-04-01').outputs.deploymentName.value]" - }, - "appInsightsConnectionString": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.appInsightsConnectionString.value]" - }, - "logAnalyticsWorkspaceId": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Resources/deployments', 'monitoring'), '2025-04-01').outputs.logAnalyticsWorkspaceId.value]" - } - } - } - }, - "dependsOn": [ - "[subscriptionResourceId('Microsoft.Resources/resourceGroups', format('rg-{0}', parameters('environmentName')))]" - ] - } - ], - "outputs": { - "AZURE_LOCATION": { - "type": "string", - "value": "[parameters('location')]" - }, - "AZURE_RESOURCE_GROUP": { - "type": "string", - "value": "[format('rg-{0}', parameters('environmentName'))]" - }, - "AZURE_TENANT_ID": { - "type": "string", - "value": "[subscription().tenantId]" - }, - "AZURE_SUBSCRIPTION_ID": { - "type": "string", - "value": "[subscription().subscriptionId]" - }, - "AZURE_CONTAINER_REGISTRY_NAME": { - "type": "string", - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'finops-resources'), '2025-04-01').outputs.acrName.value]" - }, - "AZURE_CONTAINER_REGISTRY_LOGIN_SERVER": { - "type": "string", - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'finops-resources'), '2025-04-01').outputs.acrLoginServer.value]" - }, - "AZURE_CONTAINER_REGISTRY_IMAGE": { - "type": "string", - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'finops-resources'), '2025-04-01').outputs.containerImageName.value]" - }, - "WEB_APP_NAME": { - "type": "string", - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'finops-resources'), '2025-04-01').outputs.webAppName.value]" - }, - "WEB_APP_HOSTNAME": { - "type": "string", - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'finops-resources'), '2025-04-01').outputs.webAppHostname.value]" - }, - "WEB_APP_URL": { - "type": "string", - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'finops-resources'), '2025-04-01').outputs.webAppUrl.value]" - }, - "WEB_APP_PRINCIPAL_ID": { - "type": "string", - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'finops-resources'), '2025-04-01').outputs.webAppPrincipalId.value]" - }, - "AZURE_OPENAI_ENDPOINT": { - "type": "string", - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'finops-resources'), '2025-04-01').outputs.aoaiEndpoint.value]" - }, - "AZURE_OPENAI_DEPLOYMENT_NAME": { - "type": "string", - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'finops-resources'), '2025-04-01').outputs.aoaiDeploymentName.value]" - }, - "APPLICATIONINSIGHTS_CONNECTION_STRING": { - "type": "string", - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'finops-resources'), '2025-04-01').outputs.appInsightsConnectionString.value]" - }, - "AZURE_LOG_ANALYTICS_WORKSPACE_ID": { - "type": "string", - "value": "[reference(extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', subscription().subscriptionId, format('rg-{0}', parameters('environmentName'))), 'Microsoft.Resources/deployments', 'finops-resources'), '2025-04-01').outputs.logAnalyticsWorkspaceId.value]" - } - } -} \ No newline at end of file diff --git a/infra/modules/appservice.json b/infra/modules/appservice.json deleted file mode 100644 index 5f2eb02..0000000 --- a/infra/modules/appservice.json +++ /dev/null @@ -1,157 +0,0 @@ -{ - "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", - "contentVersion": "1.0.0.0", - "metadata": { - "_generator": { - "name": "bicep", - "version": "0.41.2.15936", - "templateHash": "10279812525912434775" - } - }, - "parameters": { - "location": { - "type": "string" - }, - "resourceToken": { - "type": "string" - }, - "tags": { - "type": "object" - }, - "appServicePlanSku": { - "type": "string" - }, - "acrLoginServer": { - "type": "string" - }, - "containerImageName": { - "type": "string" - }, - "appInsightsConnectionString": { - "type": "securestring" - }, - "aoaiEndpoint": { - "type": "string" - }, - "aoaiDeploymentName": { - "type": "string" - }, - "entraAppId": { - "type": "string" - }, - "entraClientSecret": { - "type": "securestring" - }, - "entraTenantId": { - "type": "string" - } - }, - "variables": { - "planTier": "[if(startsWith(parameters('appServicePlanSku'), 'B'), 'Basic', if(startsWith(parameters('appServicePlanSku'), 'S'), 'Standard', 'PremiumV3'))]" - }, - "resources": [ - { - "type": "Microsoft.Web/serverfarms", - "apiVersion": "2024-04-01", - "name": "[format('plan-finops-{0}', parameters('resourceToken'))]", - "location": "[parameters('location')]", - "tags": "[parameters('tags')]", - "kind": "linux", - "sku": { - "name": "[parameters('appServicePlanSku')]", - "tier": "[variables('planTier')]" - }, - "properties": { - "reserved": true - } - }, - { - "type": "Microsoft.Web/sites", - "apiVersion": "2024-04-01", - "name": "[format('app-finops-{0}', parameters('resourceToken'))]", - "location": "[parameters('location')]", - "tags": "[union(parameters('tags'), createObject('azd-service-name', 'web'))]", - "kind": "app,linux,container", - "identity": { - "type": "SystemAssigned" - }, - "properties": { - "serverFarmId": "[resourceId('Microsoft.Web/serverfarms', format('plan-finops-{0}', parameters('resourceToken')))]", - "httpsOnly": true, - "publicNetworkAccess": "Enabled", - "siteConfig": { - "linuxFxVersion": "[format('DOCKER|{0}/{1}', parameters('acrLoginServer'), parameters('containerImageName'))]", - "acrUseManagedIdentityCreds": true, - "alwaysOn": "[and(not(equals(parameters('appServicePlanSku'), 'F1')), not(startsWith(parameters('appServicePlanSku'), 'B')))]", - "http20Enabled": true, - "ftpsState": "Disabled", - "minTlsVersion": "1.2", - "healthCheckPath": "/api/version", - "appSettings": [ - { - "name": "WEBSITES_PORT", - "value": "8080" - }, - { - "name": "WEBSITES_ENABLE_APP_SERVICE_STORAGE", - "value": "false" - }, - { - "name": "DOCKER_REGISTRY_SERVER_URL", - "value": "[format('https://{0}', parameters('acrLoginServer'))]" - }, - { - "name": "AzureOpenAI__Endpoint", - "value": "[parameters('aoaiEndpoint')]" - }, - { - "name": "AzureOpenAI__DeploymentName", - "value": "[parameters('aoaiDeploymentName')]" - }, - { - "name": "Microsoft__ClientId", - "value": "[parameters('entraAppId')]" - }, - { - "name": "Microsoft__ClientSecret", - "value": "[parameters('entraClientSecret')]" - }, - { - "name": "Microsoft__TenantId", - "value": "[parameters('entraTenantId')]" - }, - { - "name": "ApplicationInsights__ConnectionString", - "value": "[parameters('appInsightsConnectionString')]" - }, - { - "name": "APPLICATIONINSIGHTS_CONNECTION_STRING", - "value": "[parameters('appInsightsConnectionString')]" - }, - { - "name": "ASPNETCORE_ENVIRONMENT", - "value": "Production" - } - ] - } - }, - "dependsOn": [ - "[resourceId('Microsoft.Web/serverfarms', format('plan-finops-{0}', parameters('resourceToken')))]" - ] - } - ], - "outputs": { - "name": { - "type": "string", - "value": "[format('app-finops-{0}', parameters('resourceToken'))]" - }, - "hostname": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Web/sites', format('app-finops-{0}', parameters('resourceToken'))), '2024-04-01').defaultHostName]" - }, - "principalId": { - "type": "string", - "value": "[reference(resourceId('Microsoft.Web/sites', format('app-finops-{0}', parameters('resourceToken'))), '2024-04-01', 'full').identity.principalId]" - } - } -} \ No newline at end of file diff --git a/upload-social-preview.mjs b/upload-social-preview.mjs new file mode 100644 index 0000000..af3e2fc --- /dev/null +++ b/upload-social-preview.mjs @@ -0,0 +1,64 @@ +// Upload social preview image to GitHub repo settings via headed Playwright. +// Run: node upload-social-preview.mjs +// Persists sign-in to .pw-profile/ so subsequent runs skip the login step. + +import { chromium } from 'playwright'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { existsSync } from 'node:fs'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const PROFILE_DIR = path.join(__dirname, '.pw-profile'); +const REPO_SETTINGS = 'https://github.com/Azure-Samples/azure-finops-agent/settings'; +const IMAGE = path.resolve(__dirname, 'src/Dashboard/frontend/public/og-image.png'); + +if (!existsSync(IMAGE)) { + console.error('Image not found:', IMAGE); + process.exit(1); +} + +const ctx = await chromium.launchPersistentContext(PROFILE_DIR, { + headless: false, + viewport: { width: 1400, height: 900 }, +}); +const page = ctx.pages()[0] ?? await ctx.newPage(); + +console.log('Opening', REPO_SETTINGS); +await page.goto(REPO_SETTINGS, { waitUntil: 'domcontentloaded' }); + +// If redirected to /login, wait for the user to sign in (up to 3 min). +if (page.url().includes('/login')) { + console.log('\n=== Sign in to GitHub in the open browser window. ==='); + console.log('Waiting up to 3 minutes for you to land on the settings page...\n'); + await page.waitForURL(/\/Azure-Samples\/azure-finops-agent\/settings/, { timeout: 180_000 }); + console.log('Detected settings page.'); +} + +// Scroll to the Social preview section (bottom of /settings). +console.log('Locating Social preview section...'); +const heading = page.getByRole('heading', { name: /social preview/i }); +await heading.waitFor({ state: 'visible', timeout: 30_000 }); +await heading.scrollIntoViewIfNeeded(); + +// Click "Edit" (or "Upload an image..." if no image is set yet). +const editBtn = page.getByRole('button', { name: /^edit$/i }).first(); +const uploadBtn = page.getByRole('button', { name: /upload an image/i }).first(); + +if (await editBtn.isVisible().catch(() => false)) { + console.log('Clicking Edit...'); + await editBtn.click(); +} + +// The file picker is wired to a hidden . Set it directly. +console.log('Uploading', IMAGE); +const fileInput = page.locator('input[type="file"]').first(); +await fileInput.setInputFiles(IMAGE); + +// GitHub auto-saves the upload; give it a moment, then re-read. +console.log('Waiting for save...'); +await page.waitForTimeout(4000); + +await page.screenshot({ path: path.join(__dirname, '.pw-social-preview-result.png'), fullPage: false }); +console.log('Done. Screenshot saved to .pw-social-preview-result.png'); + +await ctx.close(); From 548053fad8d81f775f5060683a9aefd077ac591b Mon Sep 17 00:00:00 2001 From: Ali Reza Farahnak Date: Tue, 30 Jun 2026 22:18:47 +0200 Subject: [PATCH 06/15] fix(infra): correct gpt-5.4 model version + restrict to supported regions gpt-5.4 is version 2026-03-05, not 2025-08-07 (that belongs to the original gpt-5) - the old pin would fail the AOAI model deployment. Also constrain location and aoaiLocation to the 29 regions where gpt-5.4 Global Standard is available (verified via az model catalog + Foundry region-availability docs) so azd up succeeds for any customer. --- README.md | 6 ++-- infra/main.bicep | 71 +++++++++++++++++++++++++++++++++++--- infra/main.parameters.json | 2 +- 3 files changed, 70 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 4579e7d..5f0d318 100644 --- a/README.md +++ b/README.md @@ -50,10 +50,10 @@ Override defaults via `azd env set` before running `azd up`: | Variable | Default | Notes | |---|---|---| -| `AZURE_LOCATION` | _(prompted)_ | Region for the resource group and most resources | -| `AZURE_OPENAI_LOCATION` | `swedencentral` | AOAI region (model availability is region-restricted) | +| `AZURE_LOCATION` | _(prompted)_ | Region for the resource group and most resources. Restricted to the 29 regions where the full stack (incl. gpt-5.4 Global Standard) is available | +| `AZURE_OPENAI_LOCATION` | `swedencentral` | AOAI region. Restricted to the 29 regions offering gpt-5.4 Global Standard | | `APP_SERVICE_PLAN_SKU` | `B1` | Use `P0V3` for production-grade (~$77/mo vs ~$13/mo) | -| `AZURE_OPENAI_MODEL_NAME` / `_VERSION` | `gpt-5.4` / `2025-08-07` | **Reasoning model required** — the agent sets `ReasoningEffort=xhigh` in code. gpt-4o / gpt-4 will not work. | +| `AZURE_OPENAI_MODEL_NAME` / `_VERSION` | `gpt-5.4` / `2026-03-05` | **Reasoning model required** — the agent sets `ReasoningEffort=xhigh` in code. gpt-4o / gpt-4 will not work. | | `AZURE_OPENAI_DEPLOYMENT_NAME` | `gpt-5.4` | Surfaced as `AzureOpenAI__DeploymentName` to the app | | `EXISTING_AOAI_RESOURCE_ID` | _(empty)_ | Full resource ID to reuse an existing AOAI account instead of creating one | | `AZURE_ENTRA_APP_ID` / `AZURE_ENTRA_CLIENT_SECRET` | _(empty)_ | Reuse an existing Entra app instead of creating one | diff --git a/infra/main.bicep b/infra/main.bicep index fdc0b6c..751c5cd 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -7,11 +7,72 @@ targetScope = 'subscription' @description('Name of the azd environment. Used to derive resource names and tags.') param environmentName string -@minLength(1) -@description('Primary Azure region for the resource group and most resources (e.g. swedencentral, eastus2).') +@allowed([ + 'australiaeast' + 'brazilsouth' + 'canadacentral' + 'canadaeast' + 'centralus' + 'eastus' + 'eastus2' + 'francecentral' + 'germanywestcentral' + 'italynorth' + 'japaneast' + 'koreacentral' + 'northcentralus' + 'northeurope' + 'norwayeast' + 'polandcentral' + 'southafricanorth' + 'southcentralus' + 'southeastasia' + 'southindia' + 'spaincentral' + 'swedencentral' + 'switzerlandnorth' + 'switzerlandwest' + 'uaenorth' + 'uksouth' + 'westeurope' + 'westus' + 'westus3' +]) +@description('Primary Azure region for the resource group and all non-AOAI resources. Restricted to regions where the full stack (App Service, ACR, Log Analytics/App Insights, and gpt-5.4 Global Standard) is available, so `azd up` succeeds for any customer.') param location string -@description('Azure region for the Azure OpenAI account. Often differs from `location` because model availability is region-restricted. Default swedencentral has broad model coverage.') +@allowed([ + 'australiaeast' + 'brazilsouth' + 'canadacentral' + 'canadaeast' + 'centralus' + 'eastus' + 'eastus2' + 'francecentral' + 'germanywestcentral' + 'italynorth' + 'japaneast' + 'koreacentral' + 'northcentralus' + 'northeurope' + 'norwayeast' + 'polandcentral' + 'southafricanorth' + 'southcentralus' + 'southeastasia' + 'southindia' + 'spaincentral' + 'swedencentral' + 'switzerlandnorth' + 'switzerlandwest' + 'uaenorth' + 'uksouth' + 'westeurope' + 'westus' + 'westus3' +]) +@description('Azure region for the Azure OpenAI account. Restricted to regions where gpt-5.4 Global Standard (version 2026-03-05) is available — verified against the Foundry model region-availability matrix. May differ from `location`. Default: swedencentral.') param aoaiLocation string = 'swedencentral' @allowed([ 'B1', 'B2', 'B3', 'S1', 'S2', 'S3', 'P0V3', 'P1V3', 'P2V3', 'P3V3' ]) @@ -21,8 +82,8 @@ param appServicePlanSku string = 'B1' @description('Azure OpenAI model name to deploy (must be available in `aoaiLocation`). The agent requires a reasoning model — `ReasoningEffort=xhigh` is set in code (see CopilotSessionFactory.cs). gpt-4o / gpt-4 will not work.') param aoaiModelName string = 'gpt-5.4' -@description('Azure OpenAI model version (use the latest GA version for the chosen model).') -param aoaiModelVersion string = '2025-08-07' +@description('Azure OpenAI model version. Must match the model name: gpt-5.4 = 2026-03-05 (note: 2025-08-07 is the original gpt-5 version, NOT gpt-5.4).') +param aoaiModelVersion string = '2026-03-05' @description('Azure OpenAI deployment name surfaced as `AzureOpenAI__DeploymentName` to the app.') param aoaiDeploymentName string = 'gpt-5.4' diff --git a/infra/main.parameters.json b/infra/main.parameters.json index 7a4bf5e..83cd47c 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -18,7 +18,7 @@ "value": "${AZURE_OPENAI_MODEL_NAME=gpt-5.4}" }, "aoaiModelVersion": { - "value": "${AZURE_OPENAI_MODEL_VERSION=2025-08-07}" + "value": "${AZURE_OPENAI_MODEL_VERSION=2026-03-05}" }, "aoaiDeploymentName": { "value": "${AZURE_OPENAI_DEPLOYMENT_NAME=gpt-5.4}" From 700289ca278461a09c5db37b0c9299be9c8ee608 Mon Sep 17 00:00:00 2001 From: Ali Reza Farahnak Date: Tue, 30 Jun 2026 22:18:57 +0200 Subject: [PATCH 07/15] feat(infra): migrate AOAI to Azure AI Foundry (AIServices) with managed-identity auth Switch aoai.bicep from classic kind:OpenAI to kind:AIServices + allowProjectManagement (API 2026-03-01), matching the live finops-agent-ai resource. Grant the Web App MI 'Cognitive Services OpenAI User' (5e0bd9bd) instead of 'Cognitive Services User'. Keep disableLocalAuth:true - managed identity only, no API keys. Endpoint output (properties.endpoint) unchanged; prod confirms Copilot SDK BYOK works with the cognitiveservices.azure.com host. Docs updated. --- .github/copilot-instructions.md | 2 +- README.md | 4 ++-- infra/modules/aoai.bicep | 19 +++++++++++-------- infra/modules/roles-aoai.bicep | 14 +++++++------- infra/modules/roles.bicep | 12 +++++++----- 5 files changed, 28 insertions(+), 23 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index c63d7bb..5ee5c7f 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -274,7 +274,7 @@ The production app runs as a Docker container on Azure App Service, fed from Azu ### Customer-facing `azd up` path -For external users deploying into their own subscription, the repo ships with `azure.yaml` + `infra/` (Bicep) + `infra/scripts/` (PowerShell hooks). A single `azd up` provisions everything: RG, Log Analytics + App Insights, ACR (admin disabled, MI-based pull), Azure OpenAI + model deployment, App Service Plan + Web App with system-assigned MI, role assignments (`AcrPull`, `Cognitive Services User`), and the multi-tenant Entra app registration (via `setup-entra-app.ps1 -OutputJson` invoked from the preprovision hook). The postdeploy hook runs `az acr build` so users do not need a local Docker daemon. See README "Deploy to Azure (`azd up`)" for the full UX. **Note:** the production owner deployment (`crfinopsagent` / `finops-agent-container`) predates this and is unaffected — `azd up` always creates new resources under the chosen `azd env` name. +For external users deploying into their own subscription, the repo ships with `azure.yaml` + `infra/` (Bicep) + `infra/scripts/` (PowerShell hooks). A single `azd up` provisions everything: RG, Log Analytics + App Insights, ACR (admin disabled, MI-based pull), Azure AI Foundry (`AIServices`) account + model deployment, App Service Plan + Web App with system-assigned MI, role assignments (`AcrPull`, `Cognitive Services OpenAI User`), and the multi-tenant Entra app registration (via `setup-entra-app.ps1 -OutputJson` invoked from the preprovision hook). The postdeploy hook runs `az acr build` so users do not need a local Docker daemon. See README "Deploy to Azure (`azd up`)" for the full UX. **Note:** the production owner deployment (`crfinopsagent` / `finops-agent-container`) predates this and is unaffected — `azd up` always creates new resources under the chosen `azd env` name. ## Code Conventions diff --git a/README.md b/README.md index 5f0d318..f23e49f 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,8 @@ azd up # provision + build image + deploy What `azd up` does: -1. Creates a resource group, Log Analytics + Application Insights, Azure Container Registry (Basic, admin disabled), Azure OpenAI account + model deployment, Linux App Service Plan, and the containerised Web App with system-assigned managed identity. -2. Grants the Web App's MI `AcrPull` on the registry and `Cognitive Services User` on Azure OpenAI (BYOK). +1. Creates a resource group, Log Analytics + Application Insights, Azure Container Registry (Basic, admin disabled), Azure AI Foundry (`AIServices`) account + `gpt-5.4` model deployment, Linux App Service Plan, and the containerised Web App with system-assigned managed identity. +2. Grants the Web App's MI `AcrPull` on the registry and `Cognitive Services OpenAI User` on the Foundry account (BYOK via managed identity — no API keys). 3. Creates a multi-tenant Microsoft Entra ID app registration with the 5 incremental-consent permission tiers (ARM, Microsoft Graph, Log Analytics, Azure Storage) — see [setup-entra-app.ps1](src/Dashboard/setup-entra-app.ps1). 4. Builds the Docker image server-side via `az acr build` (no local Docker daemon required) and restarts the Web App. diff --git a/infra/modules/aoai.bicep b/infra/modules/aoai.bicep index be7436b..aaa43ff 100644 --- a/infra/modules/aoai.bicep +++ b/infra/modules/aoai.bicep @@ -1,7 +1,7 @@ -// Azure OpenAI account + model deployment. Conditionally created — when -// `existingAoaiResourceId` is provided, the module skips creation and reads -// the endpoint/name from the existing account so the Web App MI gets the role -// grant on whichever account is targeted. +// Azure AI Foundry (Cognitive Services, kind=AIServices) account + model deployment. +// Conditionally created — when `existingAoaiResourceId` is provided, the module +// skips creation and reads the endpoint/name from the existing account so the +// Web App MI gets the role grant on whichever account is targeted. param aoaiLocation string param resourceToken string param tags object @@ -20,11 +20,11 @@ var existingSubId = useExisting ? existingSegments[2] : subscription().subscript var existingRg = useExisting ? existingSegments[4] : resourceGroup().name var existingName = useExisting ? existingSegments[8] : '' -resource newAccount 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = if (!useExisting) { +resource newAccount 'Microsoft.CognitiveServices/accounts@2026-03-01' = if (!useExisting) { name: 'aoai-finops-${resourceToken}' location: aoaiLocation tags: tags - kind: 'OpenAI' + kind: 'AIServices' sku: { name: 'S0' } @@ -32,13 +32,16 @@ resource newAccount 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' = type: 'SystemAssigned' } properties: { + // Foundry account (enables projects/agents/evals later). Key auth OFF — + // access is managed-identity / Entra token only (no API keys). + allowProjectManagement: true customSubDomainName: 'aoai-finops-${resourceToken}' publicNetworkAccess: 'Enabled' disableLocalAuth: true } } -resource modelDeployment 'Microsoft.CognitiveServices/accounts/deployments@2025-04-01-preview' = if (!useExisting) { +resource modelDeployment 'Microsoft.CognitiveServices/accounts/deployments@2026-03-01' = if (!useExisting) { parent: newAccount name: deploymentName sku: { @@ -54,7 +57,7 @@ resource modelDeployment 'Microsoft.CognitiveServices/accounts/deployments@2025- } } -resource existingAccount 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = if (useExisting) { +resource existingAccount 'Microsoft.CognitiveServices/accounts@2026-03-01' existing = if (useExisting) { name: existingName scope: resourceGroup(existingSubId, existingRg) } diff --git a/infra/modules/roles-aoai.bicep b/infra/modules/roles-aoai.bicep index fc61e1c..55cbbef 100644 --- a/infra/modules/roles-aoai.bicep +++ b/infra/modules/roles-aoai.bicep @@ -1,20 +1,20 @@ -// Nested module so the role assignment is created in the AOAI account's own +// Nested module so the role assignment is created in the Foundry account's own // resource group / subscription (which may differ when reusing an existing -// AOAI account via `existingAoaiResourceId`). +// account via `existingAoaiResourceId`). param aoaiName string param webAppPrincipalId string -param cognitiveServicesUserRoleId string +param cognitiveServicesOpenAIUserRoleId string -resource aoai 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = { +resource aoai 'Microsoft.CognitiveServices/accounts@2026-03-01' existing = { name: aoaiName } -resource aoaiUserAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { +resource aoaiOpenAIUserAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { scope: aoai - name: guid(aoai.id, webAppPrincipalId, cognitiveServicesUserRoleId) + name: guid(aoai.id, webAppPrincipalId, cognitiveServicesOpenAIUserRoleId) properties: { principalId: webAppPrincipalId principalType: 'ServicePrincipal' - roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', cognitiveServicesUserRoleId) + roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', cognitiveServicesOpenAIUserRoleId) } } diff --git a/infra/modules/roles.bicep b/infra/modules/roles.bicep index 0e20f05..89d207f 100644 --- a/infra/modules/roles.bicep +++ b/infra/modules/roles.bicep @@ -1,6 +1,7 @@ // Role assignments for the Web App's system-assigned managed identity: // - AcrPull on the ACR (so the Web App can pull container images) -// - Cognitive Services User on the Azure OpenAI account (BYOK token via DefaultAzureCredential) +// - Cognitive Services OpenAI User on the Foundry (AIServices) account — data-plane +// access to call model deployments via managed-identity token (no API keys) // // The AOAI assignment is scoped to either a freshly-created account in this RG // or an existing account in another RG/subscription. @@ -13,7 +14,8 @@ param aoaiSubscriptionId string // Built-in role definition IDs (constant across all Azure subscriptions). var acrPullRoleId = '7f951dda-4ed3-4680-a7ca-43fe172d538d' -var cognitiveServicesUserRoleId = 'a97b65f3-24c7-4388-baec-2e87135dc908' +// Cognitive Services OpenAI User — data-plane inference; matches the production finops-agent-ai grants. +var cognitiveServicesOpenAIUserRoleId = '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd' resource acr 'Microsoft.ContainerRegistry/registries@2024-11-01-preview' existing = { name: acrName @@ -29,14 +31,14 @@ resource acrPullAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' } } -// Cognitive Services User role on AOAI — applied via a nested module because -// the AOAI account may live in a different RG/subscription when reused. +// Cognitive Services OpenAI User role on the Foundry account — applied via a nested +// module because the account may live in a different RG/subscription when reused. module aoaiRole 'roles-aoai.bicep' = { name: 'aoai-role' scope: resourceGroup(aoaiSubscriptionId, aoaiResourceGroup) params: { aoaiName: aoaiName webAppPrincipalId: webAppPrincipalId - cognitiveServicesUserRoleId: cognitiveServicesUserRoleId + cognitiveServicesOpenAIUserRoleId: cognitiveServicesOpenAIUserRoleId } } From 5a3ece41c540d59a581236f7dced24f45d715392 Mon Sep 17 00:00:00 2001 From: Ali Reza Farahnak Date: Tue, 30 Jun 2026 22:41:35 +0200 Subject: [PATCH 08/15] feat(infra): delete the azd-created Entra app on azd down Add infra/scripts/postdown.ps1 to remove the Entra app registration that preprovision creates - only when azd created it (AZURE_ENTRA_APP_CREATED_BY_AZD marker), never a bring-your-own AZURE_ENTRA_APP_ID. preprovision sets the marker and preserves it across azd up retries. Best-effort (exit 0 + continueOnError) so it never blocks teardown. Without this, every up/down cycle orphaned a multi-tenant app and secret. README and copilot-instructions updated. --- .github/copilot-instructions.md | 2 +- README.md | 2 +- azure.yaml | 11 +++++++ infra/scripts/postdown.ps1 | 58 +++++++++++++++++++++++++++++++++ infra/scripts/preprovision.ps1 | 10 ++++++ 5 files changed, 81 insertions(+), 2 deletions(-) create mode 100644 infra/scripts/postdown.ps1 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 5ee5c7f..1bd8471 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -274,7 +274,7 @@ The production app runs as a Docker container on Azure App Service, fed from Azu ### Customer-facing `azd up` path -For external users deploying into their own subscription, the repo ships with `azure.yaml` + `infra/` (Bicep) + `infra/scripts/` (PowerShell hooks). A single `azd up` provisions everything: RG, Log Analytics + App Insights, ACR (admin disabled, MI-based pull), Azure AI Foundry (`AIServices`) account + model deployment, App Service Plan + Web App with system-assigned MI, role assignments (`AcrPull`, `Cognitive Services OpenAI User`), and the multi-tenant Entra app registration (via `setup-entra-app.ps1 -OutputJson` invoked from the preprovision hook). The postdeploy hook runs `az acr build` so users do not need a local Docker daemon. See README "Deploy to Azure (`azd up`)" for the full UX. **Note:** the production owner deployment (`crfinopsagent` / `finops-agent-container`) predates this and is unaffected — `azd up` always creates new resources under the chosen `azd env` name. +For external users deploying into their own subscription, the repo ships with `azure.yaml` + `infra/` (Bicep) + `infra/scripts/` (PowerShell hooks). A single `azd up` provisions everything: RG, Log Analytics + App Insights, ACR (admin disabled, MI-based pull), Azure AI Foundry (`AIServices`) account + model deployment, App Service Plan + Web App with system-assigned MI, role assignments (`AcrPull`, `Cognitive Services OpenAI User`), and the multi-tenant Entra app registration (via `setup-entra-app.ps1 -OutputJson` invoked from the preprovision hook). The postdeploy hook runs `az acr build` so users do not need a local Docker daemon. A `postdown` hook deletes the azd-created Entra app on `azd down` (BYO apps via `AZURE_ENTRA_APP_ID` are left untouched) so nothing is orphaned. See README "Deploy to Azure (`azd up`)" for the full UX. **Note:** the production owner deployment (`crfinopsagent` / `finops-agent-container`) predates this and is unaffected — `azd up` always creates new resources under the chosen `azd env` name. ## Code Conventions diff --git a/README.md b/README.md index f23e49f..b1bfe4e 100644 --- a/README.md +++ b/README.md @@ -58,7 +58,7 @@ Override defaults via `azd env set` before running `azd up`: | `EXISTING_AOAI_RESOURCE_ID` | _(empty)_ | Full resource ID to reuse an existing AOAI account instead of creating one | | `AZURE_ENTRA_APP_ID` / `AZURE_ENTRA_CLIENT_SECRET` | _(empty)_ | Reuse an existing Entra app instead of creating one | -Tear down with `azd down --purge` (purge is required because Cognitive Services soft-deletes by default). +Tear down with `azd down --purge` (purge is required because Cognitive Services soft-deletes by default). A `postdown` hook then deletes the Entra app registration that `azd up` created — only when azd created it; a bring-your-own `AZURE_ENTRA_APP_ID` is left untouched — so no orphaned app or secret is left behind. ### Required permissions diff --git a/azure.yaml b/azure.yaml index 0b0cef3..d3485ef 100644 --- a/azure.yaml +++ b/azure.yaml @@ -45,3 +45,14 @@ hooks: shell: pwsh run: ./infra/scripts/postdeploy.ps1 interactive: true + postdown: + posix: + shell: pwsh + run: ./infra/scripts/postdown.ps1 + interactive: true + continueOnError: true + windows: + shell: pwsh + run: ./infra/scripts/postdown.ps1 + interactive: true + continueOnError: true diff --git a/infra/scripts/postdown.ps1 b/infra/scripts/postdown.ps1 new file mode 100644 index 0000000..fb64802 --- /dev/null +++ b/infra/scripts/postdown.ps1 @@ -0,0 +1,58 @@ +# Postdown hook (azd) — runs after `azd down` tears down the infrastructure. +# +# `azd down` only deletes ARM resources (the resource group + its contents, and +# with `--purge` the soft-deleted Cognitive Services / Foundry account). The +# Entra app registration is created by the preprovision hook as a DIRECTORY +# object — not an ARM resource — so azd cannot remove it. Without this hook, +# every `azd up` / `azd down` cycle would leave an orphaned multi-tenant app +# (and its client secret) behind in the tenant. +# +# This deletes that app, but ONLY when azd created it +# (AZURE_ENTRA_APP_CREATED_BY_AZD = 'true'). A user-supplied app passed via +# AZURE_ENTRA_APP_ID is never touched. + +# Best-effort cleanup: never let a non-zero az exit code throw or block `azd down`. +$ErrorActionPreference = 'Continue' +$PSNativeCommandUseErrorActionPreference = $false + +Write-Host "`n=== azd postdown ===" -ForegroundColor Cyan + +function Get-AzdEnvValue { + param([string]$Key) + $val = azd env get-value $Key 2>$null + if ($LASTEXITCODE -ne 0 -or $null -eq $val) { return '' } + return ([string]$val).Trim().Trim('"') +} + +$createdByAzd = Get-AzdEnvValue 'AZURE_ENTRA_APP_CREATED_BY_AZD' +$objectId = Get-AzdEnvValue 'AZURE_ENTRA_OBJECT_ID' +$appId = Get-AzdEnvValue 'AZURE_ENTRA_APP_ID' + +if ($createdByAzd -ne 'true') { + Write-Host " Entra app was user-supplied (or none was created) — leaving it in place." -ForegroundColor Yellow + Write-Host "=== postdown complete ===`n" -ForegroundColor Cyan + exit 0 +} + +if ([string]::IsNullOrWhiteSpace($objectId)) { + Write-Host " AZURE_ENTRA_OBJECT_ID missing from azd env — can't auto-delete the app." -ForegroundColor Yellow + if ($appId) { Write-Host " Delete it manually: az ad app delete --id $appId" -ForegroundColor Gray } + Write-Host "=== postdown complete ===`n" -ForegroundColor Cyan + exit 0 +} + +Write-Host " Deleting the Entra app registration azd created: $appId ($objectId)" -ForegroundColor Yellow +az ad app delete --id $objectId 2>$null +if ($LASTEXITCODE -eq 0) { + Write-Host " Entra app deleted." -ForegroundColor Green + # Clear the cached identifiers so a later `azd up` in this env creates a fresh app. + foreach ($k in 'AZURE_ENTRA_APP_ID', 'AZURE_ENTRA_CLIENT_SECRET', 'AZURE_ENTRA_OBJECT_ID', 'AZURE_ENTRA_APP_CREATED_BY_AZD') { + azd env set $k '' | Out-Null + } +} else { + Write-Host " Could not delete the app (exit $LASTEXITCODE). Delete it manually:" -ForegroundColor Red + Write-Host " az ad app delete --id $objectId" -ForegroundColor Gray +} + +Write-Host "=== postdown complete ===`n" -ForegroundColor Cyan +exit 0 diff --git a/infra/scripts/preprovision.ps1 b/infra/scripts/preprovision.ps1 index db9ba93..9824edc 100644 --- a/infra/scripts/preprovision.ps1 +++ b/infra/scripts/preprovision.ps1 @@ -69,6 +69,14 @@ if (-not [string]::IsNullOrWhiteSpace($existingAppId)) { exit 1 } azd env set AZURE_ENTRA_OBJECT_ID $existingObjectId | Out-Null + # Mark as user-supplied so postdown won't delete it — but NEVER downgrade a + # prior azd-created marker (re-running `azd up` after a partial failure lands + # here too, with AZURE_ENTRA_APP_ID already stored from the first run). + $priorCreated = azd env get-value AZURE_ENTRA_APP_CREATED_BY_AZD 2>$null + if ($LASTEXITCODE -ne 0) { $priorCreated = '' } + if ("$priorCreated".Trim().Trim('"') -ne 'true') { + azd env set AZURE_ENTRA_APP_CREATED_BY_AZD false | Out-Null + } Write-Host " Object ID: $existingObjectId (cached for postprovision redirect-URI patch)" -ForegroundColor Gray } else { Write-Host " Creating Entra ID app registration (multi-tenant, 5 consent tiers)..." -ForegroundColor Yellow @@ -88,6 +96,8 @@ if (-not [string]::IsNullOrWhiteSpace($existingAppId)) { azd env set AZURE_ENTRA_APP_ID $appInfo.appId | Out-Null azd env set AZURE_ENTRA_CLIENT_SECRET $appInfo.clientSecret | Out-Null azd env set AZURE_ENTRA_OBJECT_ID $appInfo.objectId | Out-Null + # azd created this app — the postdown hook will delete it on `azd down`. + azd env set AZURE_ENTRA_APP_CREATED_BY_AZD true | Out-Null Write-Host " Created app: $($appInfo.appId)" -ForegroundColor Green Write-Host " Secret stored in azd env (AZURE_ENTRA_CLIENT_SECRET)." -ForegroundColor Gray From 1599c04ed9698f350fd2b12125aaef06ced72601 Mon Sep 17 00:00:00 2001 From: Ali Reza Farahnak Date: Tue, 30 Jun 2026 23:35:18 +0200 Subject: [PATCH 09/15] fix(azd): make azd up deploy end-to-end (secretless federation, robust hooks, classic-builder Dockerfile) - Secretless: preprovision creates the Entra app with -NoSecret; postprovision federates the App Service MI to it (Workload Identity Federation). No client secret anywhere. - Hooks: read azd env via 'azd env get-values -o json | ConvertFrom-Json -AsHashtable' (ConvertFrom-StringData broke on the App Insights connection string); force redirect-URI array with @(). - Dockerfile: drop BuildKit-only RUN --mount=type=cache so az acr build classic builder works (buildx prod build still works via registry cache). - .dockerignore: **/node_modules/ (frontend/node_modules was bloating the context to 438MB). --- infra/scripts/postdeploy.ps1 | 4 +-- infra/scripts/postprovision.ps1 | 53 +++++++++++++++++++++++++++++-- infra/scripts/preprovision.ps1 | 19 +++-------- src/Dashboard/.dockerignore | 4 +-- src/Dashboard/Dockerfile | 31 ++++++------------ src/Dashboard/setup-entra-app.ps1 | 43 +++++++++++++++---------- 6 files changed, 96 insertions(+), 58 deletions(-) diff --git a/infra/scripts/postdeploy.ps1 b/infra/scripts/postdeploy.ps1 index c7ef44f..64b2946 100644 --- a/infra/scripts/postdeploy.ps1 +++ b/infra/scripts/postdeploy.ps1 @@ -12,7 +12,7 @@ $dashboardDir = Join-Path $repoRoot 'src/Dashboard' Write-Host "`n=== azd postdeploy ===" -ForegroundColor Cyan -$envValues = azd env get-values | ConvertFrom-StringData +$envValues = azd env get-values -o json 2>$null | ConvertFrom-Json -AsHashtable $acrName = $envValues['AZURE_CONTAINER_REGISTRY_NAME'] $image = $envValues['AZURE_CONTAINER_REGISTRY_IMAGE'] $webApp = $envValues['WEB_APP_NAME'] @@ -20,7 +20,7 @@ $webUrl = $envValues['WEB_APP_URL'] $rg = $envValues['AZURE_RESOURCE_GROUP'] foreach ($v in 'acrName','image','webApp','rg') { - if (-not (Get-Variable -Name $v -ValueOnly).Trim('"')) { + if (-not "$((Get-Variable -Name $v -ValueOnly))".Trim('"')) { Write-Host " Missing azd env var: $v. Aborting." -ForegroundColor Red exit 1 } diff --git a/infra/scripts/postprovision.ps1 b/infra/scripts/postprovision.ps1 index d73cf92..72dedf0 100644 --- a/infra/scripts/postprovision.ps1 +++ b/infra/scripts/postprovision.ps1 @@ -8,10 +8,13 @@ # Safe to re-run: az ad app update is idempotent and we de-dupe before sending. $ErrorActionPreference = 'Stop' +# az returns non-zero on benign conditions (e.g. an empty list); handle those via +# $LASTEXITCODE checks instead of letting native command errors throw. +$PSNativeCommandUseErrorActionPreference = $false Write-Host "`n=== azd postprovision ===" -ForegroundColor Cyan -$envValues = azd env get-values | ConvertFrom-StringData +$envValues = azd env get-values -o json 2>$null | ConvertFrom-Json -AsHashtable $objectId = $envValues['AZURE_ENTRA_OBJECT_ID'] $webHost = $envValues['WEB_APP_HOSTNAME'] $webUrl = $envValues['WEB_APP_URL'] @@ -29,7 +32,9 @@ if (-not $objectId -or -not $webHost) { Write-Host " Patching Entra app redirect URIs for hostname: $webHost" -ForegroundColor Yellow $existingJson = az ad app show --id $objectId --query 'web.redirectUris' -o json 2>$null - $existing = if ($existingJson) { $existingJson | ConvertFrom-Json } else { @() } + # @() forces an array — ConvertFrom-Json unwraps a single-element list to a + # bare string, which would make `$existing + $desired` do string concatenation. + $existing = if ($existingJson) { @($existingJson | ConvertFrom-Json) } else { @() } $merged = @($existing + $desired | Select-Object -Unique) az ad app update --id $objectId --web-redirect-uris @merged --output none @@ -42,6 +47,50 @@ if (-not $objectId -or -not $webHost) { } } +# ── Federated identity credential (secretless auth) ── +# Let the App Service system-assigned managed identity authenticate the app +# registration via Workload Identity Federation, so NO client secret is needed. +# EntraClientCredentials.cs mints a client_assertion from the MI when no secret +# is configured. +$appObjectId = $envValues['AZURE_ENTRA_OBJECT_ID'] +$tenantId = $envValues['AZURE_TENANT_ID'] +$miPrincipalId = $envValues['WEB_APP_PRINCIPAL_ID'] +if ($appObjectId) { $appObjectId = $appObjectId.Trim('"') } +if (-not $appObjectId -or -not $tenantId -or -not $miPrincipalId) { + Write-Host " Skipping federated credential — AZURE_ENTRA_OBJECT_ID / AZURE_TENANT_ID / WEB_APP_PRINCIPAL_ID missing." -ForegroundColor Yellow +} else { + $tenantId = $tenantId.Trim('"') + $miPrincipalId = $miPrincipalId.Trim('"') + $ficName = 'finops-appservice-mi' + Write-Host " Federating the App Service managed identity to the app (secretless OAuth)..." -ForegroundColor Yellow + + # Idempotent: remove any prior credential of the same name first. + $existingFic = az ad app federated-credential list --id $appObjectId --query "[?name=='$ficName'].id" -o tsv 2>$null + if ($existingFic) { + az ad app federated-credential delete --id $appObjectId --federated-credential-id $existingFic --output none 2>$null + } + + $ficFile = Join-Path ([System.IO.Path]::GetTempPath()) "finops-fic.json" + @{ + name = $ficName + issuer = "https://login.microsoftonline.com/$tenantId/v2.0" + subject = $miPrincipalId + audiences = @('api://AzureADTokenExchange') + description = 'Azure FinOps Agent App Service managed identity (secretless OAuth confidential client)' + } | ConvertTo-Json | Set-Content -Path $ficFile -Encoding utf8 + + az ad app federated-credential create --id $appObjectId --parameters "@$ficFile" --output none 2>$null + $ficExit = $LASTEXITCODE + Remove-Item $ficFile -Force -ErrorAction SilentlyContinue + + if ($ficExit -eq 0) { + Write-Host " Federated credential created (subject = App Service MI $miPrincipalId)." -ForegroundColor Green + } else { + Write-Host " WARNING: federated credential creation failed (exit $ficExit)." -ForegroundColor Red + Write-Host " 'Connect Azure' OAuth will not work until a credential (audience api://AzureADTokenExchange, subject $miPrincipalId) is added to app $appObjectId." -ForegroundColor Gray + } +} + Write-Host "`n Web App: $webUrl" -ForegroundColor Cyan Write-Host " Next: image will be built and pushed by the postdeploy hook." -ForegroundColor Gray Write-Host "=== postprovision complete ===`n" -ForegroundColor Cyan diff --git a/infra/scripts/preprovision.ps1 b/infra/scripts/preprovision.ps1 index 9824edc..0559d57 100644 --- a/infra/scripts/preprovision.ps1 +++ b/infra/scripts/preprovision.ps1 @@ -48,16 +48,6 @@ if ([string]::IsNullOrWhiteSpace($envName)) { $envName = 'finops-agent' } if (-not [string]::IsNullOrWhiteSpace($existingAppId)) { $appId = $existingAppId - $secret = $existingSecret - - # Validate the secret is also provided — Bicep would otherwise pass an empty - # string into Microsoft__ClientSecret and OAuth would silently fall back to - # anonymous mode at runtime. - if ([string]::IsNullOrWhiteSpace($secret)) { - Write-Host " AZURE_ENTRA_APP_ID is set but AZURE_ENTRA_CLIENT_SECRET is missing." -ForegroundColor Red - Write-Host " Run: azd env set AZURE_ENTRA_CLIENT_SECRET ''" -ForegroundColor Yellow - exit 1 - } # Verify the app actually exists in this tenant and capture its objectId so # postprovision.ps1 can patch the App Service hostname into the redirect URIs. @@ -86,7 +76,9 @@ if (-not [string]::IsNullOrWhiteSpace($existingAppId)) { # Production redirect URI is unknown until Bicep runs. Register localhost now; # postprovision.ps1 patches the App Service hostname in afterwards. - $jsonOutput = & $entraScript -AppName $appName -OutputJson + # -NoSecret: no client secret — the App Service managed identity is federated + # to this app in postprovision (Workload Identity Federation, fully secretless). + $jsonOutput = & $entraScript -AppName $appName -NoSecret -OutputJson if ($LASTEXITCODE -ne 0 -or -not $jsonOutput) { Write-Host " setup-entra-app.ps1 failed." -ForegroundColor Red exit 1 @@ -94,14 +86,11 @@ if (-not [string]::IsNullOrWhiteSpace($existingAppId)) { $appInfo = $jsonOutput | ConvertFrom-Json azd env set AZURE_ENTRA_APP_ID $appInfo.appId | Out-Null - azd env set AZURE_ENTRA_CLIENT_SECRET $appInfo.clientSecret | Out-Null azd env set AZURE_ENTRA_OBJECT_ID $appInfo.objectId | Out-Null # azd created this app — the postdown hook will delete it on `azd down`. azd env set AZURE_ENTRA_APP_CREATED_BY_AZD true | Out-Null - Write-Host " Created app: $($appInfo.appId)" -ForegroundColor Green - Write-Host " Secret stored in azd env (AZURE_ENTRA_CLIENT_SECRET)." -ForegroundColor Gray - Write-Host " Service-principal propagation can take 30-60s; Bicep will retry on first failure." -ForegroundColor DarkGray + Write-Host " Created app: $($appInfo.appId) (secretless — App Service MI federation set up in postprovision)" -ForegroundColor Green } Write-Host "=== preprovision complete ===`n" -ForegroundColor Cyan diff --git a/src/Dashboard/.dockerignore b/src/Dashboard/.dockerignore index 1347cb4..469d0e3 100644 --- a/src/Dashboard/.dockerignore +++ b/src/Dashboard/.dockerignore @@ -1,9 +1,9 @@ bin/ obj/ publish/ -node_modules/ -client/node_modules/ +**/node_modules/ wwwroot/ +frontend/dist/ *.user *.suo .vs/ diff --git a/src/Dashboard/Dockerfile b/src/Dashboard/Dockerfile index 420e4a3..a93c9c3 100644 --- a/src/Dashboard/Dockerfile +++ b/src/Dashboard/Dockerfile @@ -6,9 +6,8 @@ COPY frontend/package.json frontend/package-lock.json* ./ # Use `npm install` instead of `npm ci` so that Linux-only platform-optional # deps (e.g. @emnapi/*) that aren't present when the lockfile was last # regenerated on a Windows dev machine still resolve cleanly in CI. -# BuildKit cache mount keeps ~/.npm warm across CI builds. -RUN --mount=type=cache,target=/root/.npm \ - npm install --no-audit --no-fund --prefer-offline +# Plain RUN (no BuildKit cache mount) so azd's `az acr build` classic builder works. +RUN npm install --no-audit --no-fund --prefer-offline COPY frontend/ ./ RUN npx vite build --outDir ../wwwroot @@ -16,34 +15,24 @@ RUN npx vite build --outDir ../wwwroot FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build WORKDIR /src COPY Dashboard.csproj ./ -RUN --mount=type=cache,target=/root/.nuget/packages \ - dotnet restore -r linux-x64 +RUN dotnet restore -r linux-x64 COPY . . COPY --from=frontend /app/wwwroot ./wwwroot/ -# Note: we deliberately do NOT pass --no-restore here. BuildKit cache mounts -# only exist for the duration of a single RUN, so the restore step above -# can't share its /root/.nuget/packages with this step. Re-mounting the -# cache makes the second restore essentially free (everything cache-hits). -RUN --mount=type=cache,target=/root/.nuget/packages \ - dotnet publish -c Release -r linux-x64 --self-contained false -o /app/publish +RUN dotnet publish -c Release -r linux-x64 --self-contained false -o /app/publish # ── Stage 3: Final runtime image ── FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final -# Install Python 3 + CLI tools. apt cache mounted so re-installs are cheap; we -# disable the apt auto-clean docker hook so the cache survives the RUN. -RUN rm -f /etc/apt/apt.conf.d/docker-clean -RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ - --mount=type=cache,target=/var/lib/apt,sharing=locked \ - apt-get update -qq && \ +# Install Python 3 + CLI tools (plain RUN — classic-builder compatible, no BuildKit). +RUN apt-get update -qq && \ apt-get install -y --no-install-recommends \ python3 python3-pip \ jq sqlite3 && \ - ln -sf /usr/bin/python3 /usr/bin/python + ln -sf /usr/bin/python3 /usr/bin/python && \ + rm -rf /var/lib/apt/lists/* -# Pip packages — cached separately so changing apt vs pip doesn't bust both. -RUN --mount=type=cache,target=/root/.cache/pip \ - pip3 install --break-system-packages \ +# Pip packages for the file-analysis tool (plain RUN — classic-builder compatible). +RUN pip3 install --break-system-packages \ requests pandas numpy openpyxl tabulate python-dateutil \ "pdfminer.six>=20240706" "pyarrow>=17.0" diff --git a/src/Dashboard/setup-entra-app.ps1 b/src/Dashboard/setup-entra-app.ps1 index 31fcc60..f0ee40b 100644 --- a/src/Dashboard/setup-entra-app.ps1 +++ b/src/Dashboard/setup-entra-app.ps1 @@ -40,6 +40,10 @@ param( # Extra redirect URIs to register beyond the localhost + ProductionUrl defaults. # Used by the azd preprovision hook to register additional callbacks (e.g. App Service hostname). [string[]]$ExtraRedirectUris = @(), + # When set, no client secret is created — the app authenticates via a federated + # identity credential (App Service managed identity), configured by the azd + # postprovision hook. Fully secretless (Workload Identity Federation). + [switch]$NoSecret, # When set, suppresses all human-readable Write-Host output and prints a single # JSON object {appId, clientSecret, tenantId, redirectUris} to stdout — intended # for consumption by the azd preprovision hook. @@ -192,24 +196,31 @@ Write-Status "" Write-Status " NOTE: All Graph and Log Analytics scopes use incremental consent —" 'DarkYellow' Write-Status " users only see consent prompts when they opt into each tier." 'DarkYellow' -# ── 5. Create client secret ── -Write-Status "`n[5/6] Creating client secret (valid $SecretExpiryMonths months)..." 'Yellow' - -$endDate = (Get-Date).AddMonths($SecretExpiryMonths).ToString("yyyy-MM-ddTHH:mm:ssZ") -$secretJson = az ad app credential reset ` - --id $objectId ` - --display-name "FinOps Agent Secret" ` - --end-date $endDate ` - --query "{password: password}" ` - 2>$null - -if (-not $secretJson) { - Write-Status " Failed to create client secret." 'Red' - exit 1 +# ── 5. Create client secret (skipped for federated managed-identity mode) ── +if ($NoSecret) { + Write-Status "`n[5/6] Skipping client secret — federated managed identity (no secret)." 'Yellow' + $secret = '' + $endDate = '' } +else { + Write-Status "`n[5/6] Creating client secret (valid $SecretExpiryMonths months)..." 'Yellow' + + $endDate = (Get-Date).AddMonths($SecretExpiryMonths).ToString("yyyy-MM-ddTHH:mm:ssZ") + $secretJson = az ad app credential reset ` + --id $objectId ` + --display-name "FinOps Agent Secret" ` + --end-date $endDate ` + --query "{password: password}" ` + 2>$null + + if (-not $secretJson) { + Write-Status " Failed to create client secret." 'Red' + exit 1 + } -$secret = ($secretJson | ConvertFrom-Json).password -Write-Status " Secret created (expires: $endDate)" 'Gray' + $secret = ($secretJson | ConvertFrom-Json).password + Write-Status " Secret created (expires: $endDate)" 'Gray' +} # ── 6. Output configuration ── if ($OutputJson) { From d2d86d4989d495919aa71c1640eaa66aab8cd419 Mon Sep 17 00:00:00 2001 From: Ali Reza Farahnak Date: Tue, 30 Jun 2026 23:53:26 +0200 Subject: [PATCH 10/15] fix(app): allow same-origin writes on any host + wildcard App Insights CSP CSRF middleware blocked POSTs from any host not in a hardcoded production allowlist, so /api/chat returned 403 on every azd deployment (app-finops-.azurewebsites.net). Now allows same-origin writes (Origin/Referer host == request host). CSP connect-src hardcoded the canadacentral App Insights endpoint, blocking frontend telemetry in other regions. Wildcarded to *.in.applicationinsights.azure.com and *.livediagnostics.monitor.azure.com. --- src/Dashboard/Program.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Dashboard/Program.cs b/src/Dashboard/Program.cs index 0730244..acc2b7b 100644 --- a/src/Dashboard/Program.cs +++ b/src/Dashboard/Program.cs @@ -135,7 +135,7 @@ "worker-src 'self' blob:; " + "style-src 'self' 'unsafe-inline'; " + "img-src 'self' data:; " + - "connect-src 'self' blob: data: https://cdn.jsdelivr.net https://js.monitor.azure.com https://canadacentral-1.in.applicationinsights.azure.com https://canadacentral.livediagnostics.monitor.azure.com; " + + "connect-src 'self' blob: data: https://cdn.jsdelivr.net https://js.monitor.azure.com https://*.in.applicationinsights.azure.com https://*.livediagnostics.monitor.azure.com; " + "font-src 'self'; " + "frame-ancestors 'none'"; } @@ -204,7 +204,15 @@ if (Uri.TryCreate(origin, UriKind.Absolute, out var oUri)) sourceHost = oUri.Authority; else if (Uri.TryCreate(referer, UriKind.Absolute, out var rUri)) sourceHost = rUri.Authority; - if (string.IsNullOrEmpty(sourceHost) || !allowedOriginHosts.Contains(sourceHost)) + // Same-origin writes are always allowed: the Origin/Referer host equals the + // host this request arrived on. This makes the agent work on ANY deployment + // hostname (e.g. azd's app-finops-.azurewebsites.net) with no config. + // The allowlist below only adds extra *cross-host* origins (www → apex, custom domain). + var selfHost = ctx.Request.Host.Value ?? ""; + var sameOrigin = !string.IsNullOrEmpty(sourceHost) && + string.Equals(sourceHost, selfHost, StringComparison.OrdinalIgnoreCase); + + if (string.IsNullOrEmpty(sourceHost) || (!sameOrigin && !allowedOriginHosts.Contains(sourceHost))) { ctx.Response.StatusCode = 403; await ctx.Response.WriteAsync("Forbidden: cross-origin write blocked"); From e3b15a7325363e15fed9a76f0e98ab79a2403281 Mon Sep 17 00:00:00 2001 From: Ali Reza Farahnak Date: Wed, 1 Jul 2026 08:26:53 +0200 Subject: [PATCH 11/15] fix(byok): use Foundry OpenAI-compatible /openai/v1/ + Responses API for GPT-5 reasoning The session sets ReasoningEffort=xhigh on gpt-5.4 (GPT-5 series), which requires the Responses API. Provider was type=azure (Chat Completions, api-version 2024-10-21) with no /openai/v1/ path or WireApi, so reasoning requests never completed and chat hung forever. Switch to type=openai + {endpoint}/openai/v1/ + WireApi=responses per GitHub Copilot SDK BYOK docs for Azure AI Foundry. Also derive SEO/sitemap base URL from request host and gate IndexNow on PUBLIC_SITE_HOST so the template works on any deployment domain. --- src/Dashboard/AI/CopilotSessionFactory.cs | 12 ++++++-- src/Dashboard/AI/Tools/FaqTools.cs | 12 ++++++-- src/Dashboard/Endpoints/SeoEndpoints.cs | 35 +++++++++++++---------- 3 files changed, 40 insertions(+), 19 deletions(-) diff --git a/src/Dashboard/AI/CopilotSessionFactory.cs b/src/Dashboard/AI/CopilotSessionFactory.cs index 2f6bbd3..e4b4d3c 100644 --- a/src/Dashboard/AI/CopilotSessionFactory.cs +++ b/src/Dashboard/AI/CopilotSessionFactory.cs @@ -313,9 +313,17 @@ private async Task CreateSessionConfigAsync(long userId) OnPermissionRequest = (_, _) => Task.FromResult(new PermissionRequestResult { Kind = PermissionRequestResultKind.Approved }), Provider = new ProviderConfig { - Type = "azure", - BaseUrl = _endpoint.TrimEnd('/'), + // Azure AI Foundry exposes an OpenAI-compatible endpoint at /openai/v1/. + // GPT-5 series models AND ReasoningEffort require the Responses API, which is + // only reachable via the "openai" provider type with WireApi="responses". + // The classic "azure" type uses the Chat Completions API (api-version 2024-10-21) + // and does not support reasoning on these models — the request never completes. + // See GitHub Copilot SDK BYOK docs (Azure AI Foundry OpenAI-compatible endpoint): + // https://github.com/github/copilot-sdk/blob/main/docs/auth/byok.md + Type = "openai", + BaseUrl = $"{_endpoint.TrimEnd('/')}/openai/v1/", BearerToken = bearerToken, + WireApi = "responses", }, SystemMessage = new SystemMessageConfig { diff --git a/src/Dashboard/AI/Tools/FaqTools.cs b/src/Dashboard/AI/Tools/FaqTools.cs index f31d110..371d35f 100644 --- a/src/Dashboard/AI/Tools/FaqTools.cs +++ b/src/Dashboard/AI/Tools/FaqTools.cs @@ -28,6 +28,12 @@ public class FaqTools string.Equals(Environment.GetEnvironmentVariable("FAQ_AUTO_APPROVE"), "true", StringComparison.OrdinalIgnoreCase); private static readonly string ApprovalKey = Environment.GetEnvironmentVariable("FAQ_APPROVAL_KEY") ?? ""; private static readonly string IndexNowKey = Environment.GetEnvironmentVariable("INDEXNOW_KEY") ?? "finopsagent2026"; + // Public site host for IndexNow submissions (e.g. "azure-finops-agent.com"). IndexNow requires + // domain ownership plus a key file, so this only applies to a public owner deployment. When + // unset (customer / internal deployments), IndexNow pinging is skipped instead of announcing + // a domain the deployment does not own. + private static readonly string? PublicSiteHost = + Environment.GetEnvironmentVariable("PUBLIC_SITE_HOST"); static FaqTools() { @@ -150,13 +156,15 @@ private static void Load() private static async Task PingIndexNowAsync(string slug) { + // No public domain configured — skip IndexNow (nothing to announce / not owned). + if (string.IsNullOrWhiteSpace(PublicSiteHost)) return; try { using var http = new HttpClient { Timeout = TimeSpan.FromSeconds(10) }; var body = JsonSerializer.Serialize(new { - host = "azure-finops-agent.com", - urlList = new[] { $"https://azure-finops-agent.com/faq/{slug}" }, + host = PublicSiteHost, + urlList = new[] { $"https://{PublicSiteHost}/faq/{slug}" }, key = IndexNowKey }); await http.PostAsync("https://api.indexnow.org/indexnow", diff --git a/src/Dashboard/Endpoints/SeoEndpoints.cs b/src/Dashboard/Endpoints/SeoEndpoints.cs index 704599a..a679961 100644 --- a/src/Dashboard/Endpoints/SeoEndpoints.cs +++ b/src/Dashboard/Endpoints/SeoEndpoints.cs @@ -55,7 +55,7 @@ public static class SeoEndpoints public static void MapSeoEndpoints(this IEndpointRouteBuilder app) { - app.MapGet("/faq/{slug}", (string slug) => + app.MapGet("/faq/{slug}", (string slug, HttpContext ctx) => { string title, question, answer, prompt, date; if (StaticFaqs.TryGetValue(slug, out var page)) @@ -71,19 +71,24 @@ public static void MapSeoEndpoints(this IEndpointRouteBuilder app) return Results.NotFound("FAQ page not found") as IResult; } - return Results.Content(RenderFaqHtml(slug, title, question, answer, prompt, date), "text/html; charset=utf-8"); + return Results.Content(RenderFaqHtml(BaseUrl(ctx), slug, title, question, answer, prompt, date), "text/html; charset=utf-8"); }); - app.MapGet("/faq", () => Results.Content(RenderFaqIndex(), "text/html")); + app.MapGet("/faq", (HttpContext ctx) => Results.Content(RenderFaqIndex(BaseUrl(ctx)), "text/html")); - app.MapGet("/sitemap.xml", () => Results.Content(RenderSitemap(), "application/xml")); + app.MapGet("/sitemap.xml", (HttpContext ctx) => Results.Content(RenderSitemap(BaseUrl(ctx)), "application/xml")); } - private static string RenderFaqHtml(string slug, string title, string question, string answer, string prompt, string date) + // Public base URL for canonical links, sitemap, and structured data — derived from the + // incoming request so the deployed hostname is used automatically. Works on any azd + // deployment host, a custom domain, or localhost, with no hardcoded domain. + private static string BaseUrl(HttpContext ctx) => $"{ctx.Request.Scheme}://{ctx.Request.Host}"; + + private static string RenderFaqHtml(string baseUrl, string slug, string title, string question, string answer, string prompt, string date) { Func e = System.Net.WebUtility.HtmlEncode!; var desc = answer.Length > 155 ? answer[..155] + "..." : answer; - var faqUrl = $"https://azure-finops-agent.com/faq/{slug}"; + var faqUrl = $"{baseUrl}/faq/{slug}"; var isoDate = date + "T00:00:00+00:00"; var jsonLd = JsonSerializer.Serialize(new Dictionary { @@ -101,7 +106,7 @@ private static string RenderFaqHtml(string slug, string title, string question, { ["@type"] = "Organization", ["name"] = "Azure FinOps Agent", - ["url"] = "https://azure-finops-agent.com" + ["url"] = baseUrl }, ["acceptedAnswer"] = new Dictionary { @@ -115,7 +120,7 @@ private static string RenderFaqHtml(string slug, string title, string question, { ["@type"] = "Organization", ["name"] = "Azure FinOps Agent", - ["url"] = "https://azure-finops-agent.com" + ["url"] = baseUrl } } } @@ -142,7 +147,7 @@ private static string RenderFaqHtml(string slug, string title, string question, + ""; } - private static string RenderFaqIndex() + private static string RenderFaqIndex(string baseUrl) { Func e = System.Net.WebUtility.HtmlEncode!; var listItems = string.Join("", StaticFaqs.Select(kv => @@ -155,7 +160,7 @@ private static string RenderFaqIndex() + "Azure FinOps FAQ — Cloud Cost Optimization Questions & Answers" + "" + "" - + "" + + "" + "" + "" + "

    Azure FinOps FAQ

    " @@ -166,17 +171,17 @@ private static string RenderFaqIndex() + ""; } - private static string RenderSitemap() + private static string RenderSitemap(string baseUrl) { var today = DateTime.UtcNow.ToString("yyyy-MM-dd"); - var urls = "https://azure-finops-agent.com/" + today + "weekly1.0" - + "https://azure-finops-agent.com/faq" + today + "weekly0.9"; + var urls = "" + baseUrl + "/" + today + "weekly1.0" + + "" + baseUrl + "/faq" + today + "weekly0.9"; foreach (var kv in StaticFaqs) - urls += "https://azure-finops-agent.com/faq/" + kv.Key + "" + today + "monthly0.8"; + urls += "" + baseUrl + "/faq/" + kv.Key + "" + today + "monthly0.8"; foreach (var kv in FaqTools.GetAll()) - urls += "https://azure-finops-agent.com/faq/" + kv.Key + "" + kv.Value.CreatedUtc + "monthly0.7"; + urls += "" + baseUrl + "/faq/" + kv.Key + "" + kv.Value.CreatedUtc + "monthly0.7"; return "" + urls + ""; } From 7abd427776073e211784c0071534469d3c3dd695 Mon Sep 17 00:00:00 2001 From: Ali Reza Farahnak Date: Wed, 1 Jul 2026 12:27:25 +0200 Subject: [PATCH 12/15] fix(infra): keep container warm (Always On) + raise container start time limit Default B1 had Always On off, so the app unloaded when idle and the next request paid an ~80s container cold start (looks like the agent is unresponsive). Enable Always On (supported on all allowed SKUs) and set WEBSITES_CONTAINER_START_TIME_LIMIT=1800 so the heavy node+.NET+Python+OTel image doesn't exceed the 230s default on first start. --- infra/modules/appservice.bicep | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/infra/modules/appservice.bicep b/infra/modules/appservice.bicep index aa91d13..6701fff 100644 --- a/infra/modules/appservice.bicep +++ b/infra/modules/appservice.bicep @@ -44,7 +44,10 @@ resource webApp 'Microsoft.Web/sites@2024-04-01' = { siteConfig: { linuxFxVersion: 'DOCKER|${acrLoginServer}/${containerImageName}' acrUseManagedIdentityCreds: true - alwaysOn: appServicePlanSku != 'F1' && !startsWith(appServicePlanSku, 'B') + // Keep the container (and the warm Copilot CLI subprocess) resident so the + // first request after idle doesn't pay an ~80s cold start. Always On is + // supported on Basic and above (all SKUs allowed by main.bicep). + alwaysOn: true http20Enabled: true ftpsState: 'Disabled' minTlsVersion: '1.2' @@ -53,6 +56,9 @@ resource webApp 'Microsoft.Web/sites@2024-04-01' = { // Tells App Service which port the container listens on (matches Dockerfile EXPOSE 8080). { name: 'WEBSITES_PORT', value: '8080' } { name: 'WEBSITES_ENABLE_APP_SERVICE_STORAGE', value: 'false' } + // The image is heavy (node + .NET + Python + OTel collector); the first + // cold start can exceed the 230s default. Allow up to 30 min to warm up. + { name: 'WEBSITES_CONTAINER_START_TIME_LIMIT', value: '1800' } { name: 'DOCKER_REGISTRY_SERVER_URL', value: 'https://${acrLoginServer}' } // BYOK Azure OpenAI (Program.cs fail-fast key). { name: 'AzureOpenAI__Endpoint', value: aoaiEndpoint } From dab75db1f60bd6326641e6772d010e1cfd8aa7b0 Mon Sep 17 00:00:00 2001 From: Ali Reza Farahnak Date: Wed, 1 Jul 2026 12:38:03 +0200 Subject: [PATCH 13/15] fix(infra): raise default AOAI capacity 30->100K TPM (xhigh reasoning was hitting 429) GlobalStandard capacity is a per-token-billed rate-limit knob (raising it is free). 30K TPM rate-limited the agent's xhigh reasoning + tool calls. Default now 100K; overridable via AZURE_OPENAI_MODEL_CAPACITY. --- infra/main.bicep | 4 ++-- infra/main.parameters.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/infra/main.bicep b/infra/main.bicep index 751c5cd..26234d1 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -88,8 +88,8 @@ param aoaiModelVersion string = '2026-03-05' @description('Azure OpenAI deployment name surfaced as `AzureOpenAI__DeploymentName` to the app.') param aoaiDeploymentName string = 'gpt-5.4' -@description('Azure OpenAI model deployment capacity (TPM in thousands).') -param aoaiModelCapacity int = 30 +@description('Azure OpenAI model deployment capacity — TPM in THOUSANDS (100 = 100K tokens/min). For GlobalStandard this is purely a rate-limit knob billed per-token, so raising it costs nothing. The agent uses xhigh reasoning (token-heavy) — 30 hits 429 rate limits under real use. Default 100. Lower it only if your subscription''s gpt-5.4 GlobalStandard quota is under 100K; raise it (200-300) for heavy demo traffic.') +param aoaiModelCapacity int = 100 @description('Optional resource ID of an existing Azure OpenAI account to reuse instead of creating a new one. When set, `aoaiLocation`/`aoaiModelName`/`aoaiModelVersion` are ignored — the deployment must already exist on the existing account.') param existingAoaiResourceId string = '' diff --git a/infra/main.parameters.json b/infra/main.parameters.json index 83cd47c..0c6b820 100644 --- a/infra/main.parameters.json +++ b/infra/main.parameters.json @@ -24,7 +24,7 @@ "value": "${AZURE_OPENAI_DEPLOYMENT_NAME=gpt-5.4}" }, "aoaiModelCapacity": { - "value": "${AZURE_OPENAI_MODEL_CAPACITY=30}" + "value": "${AZURE_OPENAI_MODEL_CAPACITY=100}" }, "existingAoaiResourceId": { "value": "${EXISTING_AOAI_RESOURCE_ID=}" From 2506f4b48a9a5c15f07b3e1e4cf9810eb2588908 Mon Sep 17 00:00:00 2001 From: Ali Reza Farahnak Date: Wed, 1 Jul 2026 12:38:43 +0200 Subject: [PATCH 14/15] fix(infra): correct Bicep apostrophe escaping in aoaiModelCapacity description --- infra/main.bicep | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/infra/main.bicep b/infra/main.bicep index 26234d1..22e36d7 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -88,7 +88,7 @@ param aoaiModelVersion string = '2026-03-05' @description('Azure OpenAI deployment name surfaced as `AzureOpenAI__DeploymentName` to the app.') param aoaiDeploymentName string = 'gpt-5.4' -@description('Azure OpenAI model deployment capacity — TPM in THOUSANDS (100 = 100K tokens/min). For GlobalStandard this is purely a rate-limit knob billed per-token, so raising it costs nothing. The agent uses xhigh reasoning (token-heavy) — 30 hits 429 rate limits under real use. Default 100. Lower it only if your subscription''s gpt-5.4 GlobalStandard quota is under 100K; raise it (200-300) for heavy demo traffic.') +@description('Azure OpenAI model deployment capacity — TPM in THOUSANDS (100 = 100K tokens/min). For GlobalStandard this is purely a rate-limit knob billed per-token, so raising it costs nothing. The agent uses xhigh reasoning (token-heavy) — 30 hits 429 rate limits under real use. Default 100. Lower it only if your gpt-5.4 GlobalStandard quota is under 100K; raise it (200-300) for heavy demo traffic.') param aoaiModelCapacity int = 100 @description('Optional resource ID of an existing Azure OpenAI account to reuse instead of creating a new one. When set, `aoaiLocation`/`aoaiModelName`/`aoaiModelVersion` are ignored — the deployment must already exist on the existing account.') From 2578cd1b9292126bdcf3fa37d2c97bf58f94ab85 Mon Sep 17 00:00:00 2001 From: Ali Reza Farahnak Date: Wed, 1 Jul 2026 12:47:38 +0200 Subject: [PATCH 15/15] feat(infra): provision a Foundry project under the AI Services account Template created the AIServices account + account-level gpt-5.4 deployment but no project, so nothing showed under the account in the AI Foundry portal. Add a Microsoft.CognitiveServices/accounts/projects resource (schema validated live) and surface AZURE_AI_PROJECT_NAME. Not required by the app runtime (BYOK calls the account /openai/v1/ endpoint) but completes the Foundry reference architecture. --- infra/main-resources.bicep | 1 + infra/main.bicep | 1 + infra/modules/aoai.bicep | 20 ++++++++++++++++++++ 3 files changed, 22 insertions(+) diff --git a/infra/main-resources.bicep b/infra/main-resources.bicep index a07f690..a4a2de9 100644 --- a/infra/main-resources.bicep +++ b/infra/main-resources.bicep @@ -88,5 +88,6 @@ output webAppUrl string = 'https://${appservice.outputs.hostname}' output webAppPrincipalId string = appservice.outputs.principalId output aoaiEndpoint string = aoai.outputs.endpoint output aoaiDeploymentName string = aoai.outputs.deploymentName +output aiProjectName string = aoai.outputs.projectName output appInsightsConnectionString string = monitoring.outputs.appInsightsConnectionString output logAnalyticsWorkspaceId string = monitoring.outputs.logAnalyticsWorkspaceId diff --git a/infra/main.bicep b/infra/main.bicep index 22e36d7..841a6ce 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -158,6 +158,7 @@ output WEB_APP_PRINCIPAL_ID string = resources.outputs.webAppPrincipalId output AZURE_OPENAI_ENDPOINT string = resources.outputs.aoaiEndpoint output AZURE_OPENAI_DEPLOYMENT_NAME string = resources.outputs.aoaiDeploymentName +output AZURE_AI_PROJECT_NAME string = resources.outputs.aiProjectName output APPLICATIONINSIGHTS_CONNECTION_STRING string = resources.outputs.appInsightsConnectionString output AZURE_LOG_ANALYTICS_WORKSPACE_ID string = resources.outputs.logAnalyticsWorkspaceId diff --git a/infra/modules/aoai.bicep b/infra/modules/aoai.bicep index aaa43ff..b1fda33 100644 --- a/infra/modules/aoai.bicep +++ b/infra/modules/aoai.bicep @@ -57,6 +57,25 @@ resource modelDeployment 'Microsoft.CognitiveServices/accounts/deployments@2026- } } +// Foundry project — a workspace under the account for the AI Foundry portal +// experience (agents, evals, playground). NOT required by the app runtime (the +// app calls the account's /openai/v1/ endpoint + deployment directly), but +// provisioning it completes the Foundry reference architecture and gives the +// deployment a visible project home in ai.azure.com. Only created for a new +// account; an existing reused account keeps its own project layout. +resource project 'Microsoft.CognitiveServices/accounts/projects@2026-03-01' = if (!useExisting) { + parent: newAccount + name: 'proj-finops-${resourceToken}' + location: aoaiLocation + identity: { + type: 'SystemAssigned' + } + properties: { + displayName: 'Azure FinOps Agent' + description: 'Azure FinOps Agent Foundry project' + } +} + resource existingAccount 'Microsoft.CognitiveServices/accounts@2026-03-01' existing = if (useExisting) { name: existingName scope: resourceGroup(existingSubId, existingRg) @@ -67,3 +86,4 @@ output accountName string = useExisting ? existingName : newAccount!.name output deploymentName string = deploymentName output resourceGroup string = useExisting ? existingRg : resourceGroup().name output subscriptionId string = useExisting ? existingSubId : subscription().subscriptionId +output projectName string = useExisting ? '' : 'proj-finops-${resourceToken}'