diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fe95159c0..c00b0395b 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -11,6 +11,18 @@ /samples/python/foundry-local/ @microsoft-foundry/foundry-local /samples/rust/foundry-local/ @microsoft-foundry/foundry-local +#### Partner / external collaborator sample areas ####################################################### +# Partners are outside collaborators (not org members). Each entry is the source of truth for that +# partner. Update alongside step 3 of the onboarding process in docs/external-contributions.md. +# +# Format: +# # — onboarded: YYYY-MM-DD — partner: @ — dri: @ +# /samples/// @ +# +# Mistral AI — add entry here once DRI and sample paths are confirmed: +# # Mistral AI — onboarded: 2026-06-22 — partner: @peymanmohajerian — dri: @truptiparkar7 +# /samples/python/mistral/ @FILL_IN + #### files referenced in docs (DO NOT EDIT, except for Docs team!!!) ########################################## /infrastructure/infrastructure-setup-bicep/01-connections/connection-key-vault.bicep @microsoft-foundry/AI-Platform-Docs /infrastructure/infrastructure-setup-bicep/05-custom-policy-definitions/deny-disallowed-connections.json @microsoft-foundry/AI-Platform-Docs diff --git a/.gitignore b/.gitignore index 037d1c906..a0f5f559b 100644 --- a/.gitignore +++ b/.gitignore @@ -457,7 +457,7 @@ override.tf.json # !example_override.tf # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan -# example: *tfplan* +*tfplan* # Ignore CLI configuration files .terraformrc diff --git a/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/README.md b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/README.md index f19e1a5fb..f06267113 100644 --- a/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/README.md +++ b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/README.md @@ -339,6 +339,19 @@ az group delete --name --yes --no-wait - Format: From `modelFormat` parameter - Version: From `modelVersion` parameter +Azure Monitor (Application Insights & Log Analytics) +- Log Analytics Workspace: Microsoft.OperationalInsights/workspaces + - SKU: PerGB2018 + - Retention: 30 days +- Application Insights: Microsoft.Insights/components + - Kind: web + - Linked to Log Analytics workspace + - Public ingestion disabled (reached privately via AMPLS) +- Azure Monitor Private Link Scope (AMPLS): microsoft.insights/privateLinkScopes + - Access mode: PrivateOnly ingestion, Open query + - Scoped resources: Application Insights + Log Analytics + - Enables hosted agents to export telemetry via private network + ### Network Security Design This implementation utilizes a BYO VNet (Bring Your Own Virtual Network) approach with subnet delegation. Within your virtual network, two subnets are created: one delegated for agent workloads and one for private endpoints. @@ -360,6 +373,7 @@ A private endpoint ensures secure, internal-only connectivity to the AI Services | Private Link Resource Type | Sub Resource | Private DNS Zone Name | Public DNS Zone Forwarders | |----------------------------|--------------|------------------------|-----------------------------| | **Microsoft Foundry** | account | `privatelink.cognitiveservices.azure.com`
`privatelink.openai.azure.com`
`privatelink.services.ai.azure.com` | `cognitiveservices.azure.com`
`openai.azure.com`
`services.ai.azure.com` | +| **Azure Monitor (AMPLS)** | azuremonitor | `privatelink.monitor.azure.com`
`privatelink.oms.opinsights.azure.com`
`privatelink.ods.opinsights.azure.com`
`privatelink.agentsvc.azure-automation.net` | `monitor.azure.com`
`oms.opinsights.azure.com`
`ods.opinsights.azure.com`
`agentsvc.azure-automation.net` | ### Authentication & Authorization @@ -380,6 +394,8 @@ A private endpoint ensures secure, internal-only connectivity to the AI Services modules-network-secured/ ├── ai-account-identity.bicep # AI Services account with network injection ├── add-project-capability-host.bicep # Basic capability host (no BYO connections) +├── application-insights.bicep # Workspace-based Application Insights for agent tracing +├── monitor-private-link-scope.bicep # Azure Monitor Private Link Scope (AMPLS) for private telemetry ingestion ├── network-agent-vnet.bicep # VNet router (new or existing) ├── vnet.bicep # New VNet creation ├── existing-vnet.bicep # Existing VNet integration diff --git a/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/main.bicep b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/main.bicep index 03fb6c997..4c32c16b3 100644 --- a/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/main.bicep +++ b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/main.bicep @@ -114,6 +114,14 @@ param existingDnsZones object = { 'privatelink.azurecr.io': '' } +@description('Object mapping Azure Monitor private DNS zone names to the resource group of an existing zone, or empty string to create it. Use to bring your own centralized Private DNS Zones (e.g. an Azure Landing Zone connectivity subscription) for agent tracing.') +param existingMonitorDnsZones object = { + 'privatelink.monitor.azure.com': '' + 'privatelink.oms.opinsights.azure.com': '' + 'privatelink.ods.opinsights.azure.com': '' + 'privatelink.agentsvc.azure-automation.net': '' +} + @description('Zone Names for Validation of existing Private Dns Zones') param dnsZoneNames array = [ 'privatelink.services.ai.azure.com' @@ -221,6 +229,37 @@ module acr 'modules-network-secured/container-registry.bicep' = if (enableContai ] } +// Application Insights for hosted-agent tracing (this template ships none). Creates a +// workspace-based Application Insights and connects it to the account so the agent exports traces. +module applicationInsights 'modules-network-secured/application-insights.bicep' = { + name: 'app-insights-${uniqueSuffix}-deployment' + params: { + location: location + suffix: uniqueSuffix + aiAccountName: aiAccount.outputs.accountName + disablePublicIngestion: true + } +} + +// Private trace ingestion path (Azure Monitor Private Link Scope) so an in-VNet agent's traces +// reach Application Insights over the private link rather than the (disabled) public endpoint. +module monitorPrivateLink 'modules-network-secured/monitor-private-link-scope.bicep' = { + name: 'monitor-pls-${uniqueSuffix}-deployment' + params: { + location: location + suffix: uniqueSuffix + appInsightsId: applicationInsights.outputs.appInsightsId + logAnalyticsId: applicationInsights.outputs.logAnalyticsId + vnetId: vnet.outputs.virtualNetworkId + peSubnetId: vnet.outputs.peSubnetId + existingDnsZones: existingMonitorDnsZones + dnsZonesSubscriptionId: resolvedDnsZonesSubscriptionId + } + dependsOn: [ + privateEndpointAndDNS + ] +} + /* Step 4: Create a Project - Sub-resource of the AI Services account diff --git a/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/application-insights.bicep b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/application-insights.bicep new file mode 100644 index 000000000..22ada9222 --- /dev/null +++ b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/application-insights.bicep @@ -0,0 +1,82 @@ +/* +Application Insights Module +--------------------------- +This module creates workspace-based Application Insights for agent tracing with: +1. Log Analytics workspace +2. Application Insights component (private ingestion for network-secured templates) +3. Connection on the Foundry account so agents export OpenTelemetry traces here +*/ + +@description('Azure region for the tracing resources.') +param location string + +@description('Suffix for unique resource names (the template uniqueSuffix).') +param suffix string + +@description('Name of the Foundry (AI Services) account to connect Application Insights to.') +param aiAccountName string + +@description('When true, disable public ingestion (reach Application Insights privately via AMPLS). Set false for public templates.') +param disablePublicIngestion bool = true + +@description('Name of the Log Analytics workspace to create.') +param logAnalyticsName string = 'law-tracing-${suffix}' + +@description('Name of the Application Insights component to create.') +param appInsightsName string = 'appi-tracing-${suffix}' + +resource aiAccount 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = { + name: aiAccountName + scope: resourceGroup() +} + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { + name: logAnalyticsName + location: location + properties: { + sku: { + name: 'PerGB2018' + } + retentionInDays: 30 + } +} + +resource appInsights 'Microsoft.Insights/components@2020-02-02' = { + name: appInsightsName + location: location + kind: 'web' + properties: { + Application_Type: 'web' + WorkspaceResourceId: logAnalytics.id + publicNetworkAccessForIngestion: disablePublicIngestion ? 'Disabled' : 'Enabled' + publicNetworkAccessForQuery: 'Enabled' + } +} + +// Foundry account connection (category AppInsights) so the agent exports OTel traces here. +resource connection 'Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview' = { + name: '${aiAccountName}-appinsights' + parent: aiAccount + properties: { + category: 'AppInsights' + target: appInsights.id + authType: 'ApiKey' + isSharedToAll: true + credentials: { + key: appInsights.properties.ConnectionString + } + metadata: { + ApiType: 'Azure' + ResourceId: appInsights.id + } + } +} + +@description('Resource ID of the Application Insights component.') +output appInsightsId string = appInsights.id + +@description('Application ID of the Application Insights component (for trace queries).') +output appInsightsAppId string = appInsights.properties.AppId + +@description('Resource ID of the Log Analytics workspace backing Application Insights.') +output logAnalyticsId string = logAnalytics.id diff --git a/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/monitor-private-link-scope.bicep b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/monitor-private-link-scope.bicep new file mode 100644 index 000000000..522f0f20d --- /dev/null +++ b/infrastructure/infrastructure-setup-bicep/11-private-network-basic-vnet/modules-network-secured/monitor-private-link-scope.bicep @@ -0,0 +1,146 @@ +/* +Azure Monitor Private Link Scope (AMPLS) Module +----------------------------------------------- +This module enables private trace ingestion to Application Insights with: +1. Azure Monitor Private Link Scope (PrivateOnly ingestion, Open query) +2. Application Insights and Log Analytics added as scoped resources +3. Azure Monitor private DNS zones linked to the VNet +4. Private Endpoint (azuremonitor) in the PE subnet with a DNS zone group +*/ + +@description('Azure region for the private endpoint.') +param location string + +@description('Suffix for unique resource names (the template uniqueSuffix).') +param suffix string + +@description('Resource ID of the Application Insights component to scope into the AMPLS.') +param appInsightsId string + +@description('Resource ID of the Log Analytics workspace to scope into the AMPLS.') +param logAnalyticsId string + +@description('Resource ID of the Virtual Network.') +param vnetId string + +@description('Resource ID of the Private Endpoint subnet.') +param peSubnetId string + +@description('Map of Azure Monitor private DNS zone name to the resource group of an existing zone. An empty string for a zone means the module creates and links it in this resource group; a non-empty resource group means bring your own existing (e.g. centralized Azure Landing Zone) zone, which is referenced instead of recreated.') +param existingDnsZones object = { + 'privatelink.monitor.azure.com': '' + 'privatelink.oms.opinsights.azure.com': '' + 'privatelink.ods.opinsights.azure.com': '' + 'privatelink.agentsvc.azure-automation.net': '' +} + +@description('Subscription ID where existing Azure Monitor private DNS zones are located. Defaults to the current subscription.') +param dnsZonesSubscriptionId string = subscription().subscriptionId + +// Azure Monitor private DNS zones. Blob zone omitted: the standard templates already create + link it for BYO storage. +var monitorDnsZoneNames = [ + 'privatelink.monitor.azure.com' + 'privatelink.oms.opinsights.azure.com' + 'privatelink.ods.opinsights.azure.com' + 'privatelink.agentsvc.azure-automation.net' +] + +// 1. Azure Monitor Private Link Scope (private ingestion, open query). +resource ampls 'Microsoft.Insights/privateLinkScopes@2021-07-01-preview' = { + name: 'ampls-tracing-${suffix}' + location: 'global' + properties: { + accessModeSettings: { + ingestionAccessMode: 'PrivateOnly' + queryAccessMode: 'Open' + } + } +} + +// 2. Scope the Application Insights component and its Log Analytics workspace. +resource amplsAppInsights 'Microsoft.Insights/privateLinkScopes/scopedResources@2021-07-01-preview' = { + parent: ampls + name: 'appinsights-scoped' + properties: { + linkedResourceId: appInsightsId + } +} + +resource amplsLogAnalytics 'Microsoft.Insights/privateLinkScopes/scopedResources@2021-07-01-preview' = { + parent: ampls + name: 'law-scoped' + properties: { + linkedResourceId: logAnalyticsId + } +} + +// 3. The Azure Monitor private DNS zones. Zones are created and linked to the VNet only when +// not supplied via existingDnsZones; bring-your-own (centralized) zones are referenced as-is and +// are neither recreated nor relinked here, matching the ALZ centralized Private DNS Zone model. +resource monitorDnsZones 'Microsoft.Network/privateDnsZones@2020-06-01' = [for zone in monitorDnsZoneNames: if (empty(existingDnsZones[zone])) { + name: zone + location: 'global' +}] + +resource monitorDnsZoneLinks 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = [for (zone, i) in monitorDnsZoneNames: if (empty(existingDnsZones[zone])) { + parent: monitorDnsZones[i] + name: '${replace(zone, '.', '-')}-link' + location: 'global' + properties: { + virtualNetwork: { + id: vnetId + } + registrationEnabled: false + } +}] + +// Resolve each zone's resource ID: a newly created zone lives in this resource group, while a +// bring-your-own zone is referenced in its (optionally cross-subscription) resource group. +var monitorDnsZoneIds = [for zone in monitorDnsZoneNames: empty(existingDnsZones[zone]) + ? resourceId('Microsoft.Network/privateDnsZones', zone) + : extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', dnsZonesSubscriptionId, existingDnsZones[zone]), 'Microsoft.Network/privateDnsZones', zone)] + +// 4. Private endpoint to the AMPLS (group 'azuremonitor') + DNS zone group. +resource amplsPrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' = { + name: 'ampls-tracing-${suffix}-pe' + location: location + properties: { + subnet: { + id: peSubnetId + } + privateLinkServiceConnections: [ + { + name: 'ampls-connection' + properties: { + privateLinkServiceId: ampls.id + groupIds: [ + 'azuremonitor' + ] + } + } + ] + } + dependsOn: [ + amplsAppInsights + amplsLogAnalytics + ] +} + +resource amplsDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = { + parent: amplsPrivateEndpoint + name: 'ampls-dns' + properties: { + privateDnsZoneConfigs: [for (zone, i) in monitorDnsZoneNames: { + name: replace(zone, '.', '-') + properties: { + privateDnsZoneId: monitorDnsZoneIds[i] + } + }] + } + dependsOn: [ + monitorDnsZoneLinks + ] +} + +@description('Resource ID of the Azure Monitor Private Link Scope.') +output amplsId string = ampls.id diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/README.md b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/README.md index 484feef21..42680a4ca 100644 --- a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/README.md +++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/README.md @@ -414,6 +414,19 @@ Cosmos DB Account - Disabled local auth - Single region deployment +Azure Monitor (Application Insights & Log Analytics) +- Log Analytics Workspace: Microsoft.OperationalInsights/workspaces + - SKU: PerGB2018 + - Retention: 30 days +- Application Insights: Microsoft.Insights/components + - Kind: web + - Linked to Log Analytics workspace + - Public ingestion disabled (reached privately via AMPLS) +- Azure Monitor Private Link Scope (AMPLS): microsoft.insights/privateLinkScopes + - Access mode: PrivateOnly ingestion, Open query + - Scoped resources: Application Insights + Log Analytics + - Enables hosted agents to export telemetry via private network + ### Network Security Design This implementation utilizes a BYO VNet (Bring Your Own Virtual Network) approach, also known as custom VNet support with subnet delegation. Within your existing virtual network, one delegated subnet will be created. @@ -433,6 +446,7 @@ Private endpoints ensure secure, internal-only connectivity. Private endpoints a - Azure AI Search - Azure Storage - Azure Cosmos DB +- Azure Monitor Private Link Scope (AMPLS) — enables telemetry export from hosted agents **Private DNS Zones** | Private Link Resource Type | Sub Resource | Private DNS Zone Name | Public DNS Zone Forwarders | @@ -441,6 +455,7 @@ Private endpoints ensure secure, internal-only connectivity. Private endpoints a | **Azure AI Search** | searchService| `privatelink.search.windows.net` | `search.windows.net` | | **Azure Cosmos DB** | Sql | `privatelink.documents.azure.com` | `documents.azure.com` | | **Azure Storage** | blob | `privatelink.blob.core.windows.net` | `blob.core.windows.net` | +| **Azure Monitor (AMPLS)** | azuremonitor | `privatelink.monitor.azure.com`
`privatelink.oms.opinsights.azure.com`
`privatelink.ods.opinsights.azure.com`
`privatelink.agentsvc.azure-automation.net` | `monitor.azure.com`
`oms.opinsights.azure.com`
`ods.opinsights.azure.com`
`agentsvc.azure-automation.net` | ### Authentication & Authorization @@ -483,12 +498,14 @@ modules-network-secured/ ├── ai-account-identity.bicep # Microsoft Foundry deployment and configuration (supports BYO existing account) ├── ai-project-identity.bicep # Foundry project deployment and connection configuration ├── ai-search-role-assignments.bicep # AI Search RBAC configuration +├── application-insights.bicep # Workspace-based Application Insights for agent tracing ├── azure-storage-account-role-assignments.bicep # Storage Account RBAC configuration ├── blob-storage-container-role-assignments.bicep # Blob Storage Container RBAC configuration ├── cosmos-container-role-assignments.bicep # CosmosDB container Account RBAC configuration ├── cosmosdb-account-role-assignment.bicep # CosmosDB Account RBAC configuration ├── existing-vnet.bicep # Bring your existing virtual network to template deployment ├── format-project-workspace-id.bicep # Formatting the project workspace ID +├── monitor-private-link-scope.bicep # Azure Monitor Private Link Scope (AMPLS) for private telemetry ingestion ├── network-agent-vnet.bicep # Logic for routing virtual network set-up if existing virtual network is selected ├── private-endpoint-and-dns.bicep # Creating virtual networks and DNS zones. ├── standard-dependent-resources.bicep # Deploying CosmosDB, Storage, and Search diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/azuredeploy.json b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/azuredeploy.json index 7715317d6..724f299b2 100644 --- a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/azuredeploy.json +++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/azuredeploy.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "12392809195493929439" + "templateHash": "10039508639687813420" } }, "parameters": { @@ -3064,7 +3064,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "8509907656517137061" + "templateHash": "1735356790233715529" } }, "parameters": { @@ -3095,7 +3095,7 @@ "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageName'))]", - "name": "[guid(resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'), resourceId('Microsoft.Storage/storageAccounts', parameters('storageName')))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageName')), parameters('aiProjectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'), parameters('workspaceId'))]", "properties": { "principalId": "[parameters('aiProjectPrincipalId')]", "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b')]", diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/main.bicep b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/main.bicep index af360f299..d7c57540c 100644 --- a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/main.bicep +++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/main.bicep @@ -153,6 +153,14 @@ param existingDnsZones object = { 'privatelink.azurecr.io': '' } +@description('Object mapping Azure Monitor private DNS zone names to the resource group of an existing zone, or empty string to create it. Use to bring your own centralized Private DNS Zones (e.g. an Azure Landing Zone connectivity subscription) for agent tracing.') +param existingMonitorDnsZones object = { + 'privatelink.monitor.azure.com': '' + 'privatelink.oms.opinsights.azure.com': '' + 'privatelink.ods.opinsights.azure.com': '' + 'privatelink.agentsvc.azure-automation.net': '' +} + @description('Zone Names for Validation of existing Private Dns Zones') param dnsZoneNames array = [ 'privatelink.services.ai.azure.com' @@ -382,6 +390,37 @@ module acr 'modules-network-secured/container-registry.bicep' = if (enableContai ] } +// Application Insights for hosted-agent tracing (this template ships none). Creates a +// workspace-based Application Insights and connects it to the account so the agent exports traces. +module applicationInsights 'modules-network-secured/application-insights.bicep' = { + name: 'app-insights-${uniqueSuffix}-deployment' + params: { + location: location + suffix: uniqueSuffix + aiAccountName: aiAccount.outputs.accountName + disablePublicIngestion: true + } +} + +// Private trace ingestion path (Azure Monitor Private Link Scope) so an in-VNet agent's traces +// reach Application Insights over the private link rather than the (disabled) public endpoint. +module monitorPrivateLink 'modules-network-secured/monitor-private-link-scope.bicep' = { + name: 'monitor-pls-${uniqueSuffix}-deployment' + params: { + location: location + suffix: uniqueSuffix + appInsightsId: applicationInsights.outputs.appInsightsId + logAnalyticsId: applicationInsights.outputs.logAnalyticsId + vnetId: vnet.outputs.virtualNetworkId + peSubnetId: vnet.outputs.peSubnetId + existingDnsZones: existingMonitorDnsZones + dnsZonesSubscriptionId: resolvedDnsZonesSubscriptionId + } + dependsOn: [ + privateEndpointAndDNS + ] +} + /* Creates a new project (sub-resource of the AI Services account) */ diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/application-insights.bicep b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/application-insights.bicep new file mode 100644 index 000000000..22ada9222 --- /dev/null +++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/application-insights.bicep @@ -0,0 +1,82 @@ +/* +Application Insights Module +--------------------------- +This module creates workspace-based Application Insights for agent tracing with: +1. Log Analytics workspace +2. Application Insights component (private ingestion for network-secured templates) +3. Connection on the Foundry account so agents export OpenTelemetry traces here +*/ + +@description('Azure region for the tracing resources.') +param location string + +@description('Suffix for unique resource names (the template uniqueSuffix).') +param suffix string + +@description('Name of the Foundry (AI Services) account to connect Application Insights to.') +param aiAccountName string + +@description('When true, disable public ingestion (reach Application Insights privately via AMPLS). Set false for public templates.') +param disablePublicIngestion bool = true + +@description('Name of the Log Analytics workspace to create.') +param logAnalyticsName string = 'law-tracing-${suffix}' + +@description('Name of the Application Insights component to create.') +param appInsightsName string = 'appi-tracing-${suffix}' + +resource aiAccount 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = { + name: aiAccountName + scope: resourceGroup() +} + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { + name: logAnalyticsName + location: location + properties: { + sku: { + name: 'PerGB2018' + } + retentionInDays: 30 + } +} + +resource appInsights 'Microsoft.Insights/components@2020-02-02' = { + name: appInsightsName + location: location + kind: 'web' + properties: { + Application_Type: 'web' + WorkspaceResourceId: logAnalytics.id + publicNetworkAccessForIngestion: disablePublicIngestion ? 'Disabled' : 'Enabled' + publicNetworkAccessForQuery: 'Enabled' + } +} + +// Foundry account connection (category AppInsights) so the agent exports OTel traces here. +resource connection 'Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview' = { + name: '${aiAccountName}-appinsights' + parent: aiAccount + properties: { + category: 'AppInsights' + target: appInsights.id + authType: 'ApiKey' + isSharedToAll: true + credentials: { + key: appInsights.properties.ConnectionString + } + metadata: { + ApiType: 'Azure' + ResourceId: appInsights.id + } + } +} + +@description('Resource ID of the Application Insights component.') +output appInsightsId string = appInsights.id + +@description('Application ID of the Application Insights component (for trace queries).') +output appInsightsAppId string = appInsights.properties.AppId + +@description('Resource ID of the Log Analytics workspace backing Application Insights.') +output logAnalyticsId string = logAnalytics.id diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/blob-storage-container-role-assignments.bicep b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/blob-storage-container-role-assignments.bicep index 71abc97d6..817db115e 100644 --- a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/blob-storage-container-role-assignments.bicep +++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/blob-storage-container-role-assignments.bicep @@ -25,7 +25,7 @@ var conditionStr= '((!(ActionMatches{\'Microsoft.Storage/storageAccounts/blobSer // Assign Storage Blob Data Owner role resource storageBlobDataOwnerAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { scope: storage - name: guid(storageBlobDataOwner.id, storage.id) + name: guid(storage.id, aiProjectPrincipalId, storageBlobDataOwner.id, workspaceId) properties: { principalId: aiProjectPrincipalId roleDefinitionId: storageBlobDataOwner.id diff --git a/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/monitor-private-link-scope.bicep b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/monitor-private-link-scope.bicep new file mode 100644 index 000000000..522f0f20d --- /dev/null +++ b/infrastructure/infrastructure-setup-bicep/15-private-network-standard-agent-setup/modules-network-secured/monitor-private-link-scope.bicep @@ -0,0 +1,146 @@ +/* +Azure Monitor Private Link Scope (AMPLS) Module +----------------------------------------------- +This module enables private trace ingestion to Application Insights with: +1. Azure Monitor Private Link Scope (PrivateOnly ingestion, Open query) +2. Application Insights and Log Analytics added as scoped resources +3. Azure Monitor private DNS zones linked to the VNet +4. Private Endpoint (azuremonitor) in the PE subnet with a DNS zone group +*/ + +@description('Azure region for the private endpoint.') +param location string + +@description('Suffix for unique resource names (the template uniqueSuffix).') +param suffix string + +@description('Resource ID of the Application Insights component to scope into the AMPLS.') +param appInsightsId string + +@description('Resource ID of the Log Analytics workspace to scope into the AMPLS.') +param logAnalyticsId string + +@description('Resource ID of the Virtual Network.') +param vnetId string + +@description('Resource ID of the Private Endpoint subnet.') +param peSubnetId string + +@description('Map of Azure Monitor private DNS zone name to the resource group of an existing zone. An empty string for a zone means the module creates and links it in this resource group; a non-empty resource group means bring your own existing (e.g. centralized Azure Landing Zone) zone, which is referenced instead of recreated.') +param existingDnsZones object = { + 'privatelink.monitor.azure.com': '' + 'privatelink.oms.opinsights.azure.com': '' + 'privatelink.ods.opinsights.azure.com': '' + 'privatelink.agentsvc.azure-automation.net': '' +} + +@description('Subscription ID where existing Azure Monitor private DNS zones are located. Defaults to the current subscription.') +param dnsZonesSubscriptionId string = subscription().subscriptionId + +// Azure Monitor private DNS zones. Blob zone omitted: the standard templates already create + link it for BYO storage. +var monitorDnsZoneNames = [ + 'privatelink.monitor.azure.com' + 'privatelink.oms.opinsights.azure.com' + 'privatelink.ods.opinsights.azure.com' + 'privatelink.agentsvc.azure-automation.net' +] + +// 1. Azure Monitor Private Link Scope (private ingestion, open query). +resource ampls 'Microsoft.Insights/privateLinkScopes@2021-07-01-preview' = { + name: 'ampls-tracing-${suffix}' + location: 'global' + properties: { + accessModeSettings: { + ingestionAccessMode: 'PrivateOnly' + queryAccessMode: 'Open' + } + } +} + +// 2. Scope the Application Insights component and its Log Analytics workspace. +resource amplsAppInsights 'Microsoft.Insights/privateLinkScopes/scopedResources@2021-07-01-preview' = { + parent: ampls + name: 'appinsights-scoped' + properties: { + linkedResourceId: appInsightsId + } +} + +resource amplsLogAnalytics 'Microsoft.Insights/privateLinkScopes/scopedResources@2021-07-01-preview' = { + parent: ampls + name: 'law-scoped' + properties: { + linkedResourceId: logAnalyticsId + } +} + +// 3. The Azure Monitor private DNS zones. Zones are created and linked to the VNet only when +// not supplied via existingDnsZones; bring-your-own (centralized) zones are referenced as-is and +// are neither recreated nor relinked here, matching the ALZ centralized Private DNS Zone model. +resource monitorDnsZones 'Microsoft.Network/privateDnsZones@2020-06-01' = [for zone in monitorDnsZoneNames: if (empty(existingDnsZones[zone])) { + name: zone + location: 'global' +}] + +resource monitorDnsZoneLinks 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = [for (zone, i) in monitorDnsZoneNames: if (empty(existingDnsZones[zone])) { + parent: monitorDnsZones[i] + name: '${replace(zone, '.', '-')}-link' + location: 'global' + properties: { + virtualNetwork: { + id: vnetId + } + registrationEnabled: false + } +}] + +// Resolve each zone's resource ID: a newly created zone lives in this resource group, while a +// bring-your-own zone is referenced in its (optionally cross-subscription) resource group. +var monitorDnsZoneIds = [for zone in monitorDnsZoneNames: empty(existingDnsZones[zone]) + ? resourceId('Microsoft.Network/privateDnsZones', zone) + : extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', dnsZonesSubscriptionId, existingDnsZones[zone]), 'Microsoft.Network/privateDnsZones', zone)] + +// 4. Private endpoint to the AMPLS (group 'azuremonitor') + DNS zone group. +resource amplsPrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' = { + name: 'ampls-tracing-${suffix}-pe' + location: location + properties: { + subnet: { + id: peSubnetId + } + privateLinkServiceConnections: [ + { + name: 'ampls-connection' + properties: { + privateLinkServiceId: ampls.id + groupIds: [ + 'azuremonitor' + ] + } + } + ] + } + dependsOn: [ + amplsAppInsights + amplsLogAnalytics + ] +} + +resource amplsDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = { + parent: amplsPrivateEndpoint + name: 'ampls-dns' + properties: { + privateDnsZoneConfigs: [for (zone, i) in monitorDnsZoneNames: { + name: replace(zone, '.', '-') + properties: { + privateDnsZoneId: monitorDnsZoneIds[i] + } + }] + } + dependsOn: [ + monitorDnsZoneLinks + ] +} + +@description('Resource ID of the Azure Monitor Private Link Scope.') +output amplsId string = ampls.id diff --git a/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/azuredeploy.json b/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/azuredeploy.json index 5f5892a58..8432768b8 100644 --- a/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/azuredeploy.json +++ b/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/azuredeploy.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "16032808877948152637" + "templateHash": "9005518444680591480" } }, "parameters": { @@ -2149,7 +2149,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "8509907656517137061" + "templateHash": "1735356790233715529" } }, "parameters": { @@ -2180,7 +2180,7 @@ "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageName'))]", - "name": "[guid(resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'), resourceId('Microsoft.Storage/storageAccounts', parameters('storageName')))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageName')), parameters('aiProjectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'), parameters('workspaceId'))]", "properties": { "principalId": "[parameters('aiProjectPrincipalId')]", "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b')]", diff --git a/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/modules-network-secured/blob-storage-container-role-assignments.bicep b/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/modules-network-secured/blob-storage-container-role-assignments.bicep index 71abc97d6..817db115e 100644 --- a/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/modules-network-secured/blob-storage-container-role-assignments.bicep +++ b/infrastructure/infrastructure-setup-bicep/15a-private-network-evaluation-only-setup/modules-network-secured/blob-storage-container-role-assignments.bicep @@ -25,7 +25,7 @@ var conditionStr= '((!(ActionMatches{\'Microsoft.Storage/storageAccounts/blobSer // Assign Storage Blob Data Owner role resource storageBlobDataOwnerAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { scope: storage - name: guid(storageBlobDataOwner.id, storage.id) + name: guid(storage.id, aiProjectPrincipalId, storageBlobDataOwner.id, workspaceId) properties: { principalId: aiProjectPrincipalId roleDefinitionId: storageBlobDataOwner.id diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/README.md b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/README.md index 228418332..12f52e06d 100644 --- a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/README.md +++ b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/README.md @@ -409,6 +409,19 @@ Azure API Management (Optional — BYO) - Private endpoint within the VNet for secure API access - No public internet exposure when configured behind private endpoint +Azure Monitor (Application Insights & Log Analytics) +- Log Analytics Workspace: Microsoft.OperationalInsights/workspaces + - SKU: PerGB2018 + - Retention: 30 days +- Application Insights: Microsoft.Insights/components + - Kind: web + - Linked to Log Analytics workspace + - Public ingestion disabled (reached privately via AMPLS) +- Azure Monitor Private Link Scope (AMPLS): microsoft.insights/privateLinkScopes + - Access mode: PrivateOnly ingestion, Open query + - Scoped resources: Application Insights + Log Analytics + - Enables hosted agents to export telemetry via private network + ### Network Security Design This implementation utilizes a BYO VNet (Bring Your Own Virtual Network) approach, also known as custom VNet support with subnet delegation. Within your existing virtual network, one delegated subnet will be created. @@ -429,6 +442,7 @@ Private endpoints ensure secure, internal-only connectivity. Private endpoints a - Azure Storage - Azure Cosmos DB - Azure API Management (if provided) +- Azure Monitor Private Link Scope (AMPLS) — enables telemetry export from hosted agents **Private DNS Zones** | Private Link Resource Type | Sub Resource | Private DNS Zone Name | Public DNS Zone Forwarders | @@ -438,6 +452,7 @@ Private endpoints ensure secure, internal-only connectivity. Private endpoints a | **Azure Cosmos DB** | Sql | `privatelink.documents.azure.com` | `documents.azure.com` | | **Azure Storage** | blob | `privatelink.blob.core.windows.net` | `blob.core.windows.net` | | **Azure API Management** (Optional) | Gateway | `privatelink.azure-api.net` | `azure-api.net` | +| **Azure Monitor (AMPLS)** | azuremonitor | `privatelink.monitor.azure.com`
`privatelink.oms.opinsights.azure.com`
`privatelink.ods.opinsights.azure.com`
`privatelink.agentsvc.azure-automation.net` | `monitor.azure.com`
`oms.opinsights.azure.com`
`ods.opinsights.azure.com`
`agentsvc.azure-automation.net` | ### Authentication & Authorization @@ -479,12 +494,14 @@ modules-network-secured/ ├── ai-account-identity.bicep # Microsoft Foundry deployment and configuration ├── ai-project-identity.bicep # Foundry project deployment and connection configuration ├── ai-search-role-assignments.bicep # AI Search RBAC configuration +├── application-insights.bicep # Workspace-based Application Insights for agent tracing ├── azure-storage-account-role-assignments.bicep # Storage Account RBAC configuration ├── blob-storage-container-role-assignments.bicep # Blob Storage Container RBAC configuration ├── cosmos-container-role-assignments.bicep # CosmosDB container Account RBAC configuration ├── cosmosdb-account-role-assignment.bicep # CosmosDB Account RBAC configuration ├── existing-vnet.bicep # Bring your existing virtual network to template deployment ├── format-project-workspace-id.bicep # Formatting the project workspace ID +├── monitor-private-link-scope.bicep # Azure Monitor Private Link Scope (AMPLS) for private telemetry ingestion ├── network-agent-vnet.bicep # Logic for routing virtual network set-up if existing virtual network is selected ├── private-endpoint-and-dns.bicep # Creating virtual networks and DNS zones. ├── standard-dependent-resources.bicep # Deploying CosmosDB, Storage, and Search diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/azuredeploy.json b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/azuredeploy.json index 58d3d3c06..0ea67e2ec 100644 --- a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/azuredeploy.json +++ b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/azuredeploy.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "16534706758296881327" + "templateHash": "17590430880698278268" } }, "parameters": { @@ -2961,7 +2961,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "8509907656517137061" + "templateHash": "1735356790233715529" } }, "parameters": { @@ -2992,7 +2992,7 @@ "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageName'))]", - "name": "[guid(resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'), resourceId('Microsoft.Storage/storageAccounts', parameters('storageName')))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageName')), parameters('aiProjectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'), parameters('workspaceId'))]", "properties": { "principalId": "[parameters('aiProjectPrincipalId')]", "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b')]", diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/main.bicep b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/main.bicep index 562849d8f..049c8a447 100644 --- a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/main.bicep +++ b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/main.bicep @@ -118,6 +118,14 @@ param existingDnsZones object = { 'privatelink.azurecr.io': '' } +@description('Object mapping Azure Monitor private DNS zone names to the resource group of an existing zone, or empty string to create it. Use to bring your own centralized Private DNS Zones (e.g. an Azure Landing Zone connectivity subscription) for agent tracing.') +param existingMonitorDnsZones object = { + 'privatelink.monitor.azure.com': '' + 'privatelink.oms.opinsights.azure.com': '' + 'privatelink.ods.opinsights.azure.com': '' + 'privatelink.agentsvc.azure-automation.net': '' +} + @description('Zone Names for Validation of existing Private Dns Zones') param dnsZoneNames array = [ 'privatelink.services.ai.azure.com' @@ -320,6 +328,37 @@ module acr 'modules-network-secured/container-registry.bicep' = if (enableContai ] } +// Application Insights for hosted-agent tracing (this template ships none). Creates a +// workspace-based Application Insights and connects it to the account so the agent exports traces. +module applicationInsights 'modules-network-secured/application-insights.bicep' = { + name: 'app-insights-${uniqueSuffix}-deployment' + params: { + location: location + suffix: uniqueSuffix + aiAccountName: aiAccount.outputs.accountName + disablePublicIngestion: true + } +} + +// Private trace ingestion path (Azure Monitor Private Link Scope) so an in-VNet agent's traces +// reach Application Insights over the private link rather than the (disabled) public endpoint. +module monitorPrivateLink 'modules-network-secured/monitor-private-link-scope.bicep' = { + name: 'monitor-pls-${uniqueSuffix}-deployment' + params: { + location: location + suffix: uniqueSuffix + appInsightsId: applicationInsights.outputs.appInsightsId + logAnalyticsId: applicationInsights.outputs.logAnalyticsId + vnetId: vnet.outputs.virtualNetworkId + peSubnetId: vnet.outputs.peSubnetId + existingDnsZones: existingMonitorDnsZones + dnsZonesSubscriptionId: resolvedDnsZonesSubscriptionId + } + dependsOn: [ + privateEndpointAndDNS + ] +} + /* Creates a new project (sub-resource of the AI Services account) */ diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/application-insights.bicep b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/application-insights.bicep new file mode 100644 index 000000000..22ada9222 --- /dev/null +++ b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/application-insights.bicep @@ -0,0 +1,82 @@ +/* +Application Insights Module +--------------------------- +This module creates workspace-based Application Insights for agent tracing with: +1. Log Analytics workspace +2. Application Insights component (private ingestion for network-secured templates) +3. Connection on the Foundry account so agents export OpenTelemetry traces here +*/ + +@description('Azure region for the tracing resources.') +param location string + +@description('Suffix for unique resource names (the template uniqueSuffix).') +param suffix string + +@description('Name of the Foundry (AI Services) account to connect Application Insights to.') +param aiAccountName string + +@description('When true, disable public ingestion (reach Application Insights privately via AMPLS). Set false for public templates.') +param disablePublicIngestion bool = true + +@description('Name of the Log Analytics workspace to create.') +param logAnalyticsName string = 'law-tracing-${suffix}' + +@description('Name of the Application Insights component to create.') +param appInsightsName string = 'appi-tracing-${suffix}' + +resource aiAccount 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = { + name: aiAccountName + scope: resourceGroup() +} + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { + name: logAnalyticsName + location: location + properties: { + sku: { + name: 'PerGB2018' + } + retentionInDays: 30 + } +} + +resource appInsights 'Microsoft.Insights/components@2020-02-02' = { + name: appInsightsName + location: location + kind: 'web' + properties: { + Application_Type: 'web' + WorkspaceResourceId: logAnalytics.id + publicNetworkAccessForIngestion: disablePublicIngestion ? 'Disabled' : 'Enabled' + publicNetworkAccessForQuery: 'Enabled' + } +} + +// Foundry account connection (category AppInsights) so the agent exports OTel traces here. +resource connection 'Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview' = { + name: '${aiAccountName}-appinsights' + parent: aiAccount + properties: { + category: 'AppInsights' + target: appInsights.id + authType: 'ApiKey' + isSharedToAll: true + credentials: { + key: appInsights.properties.ConnectionString + } + metadata: { + ApiType: 'Azure' + ResourceId: appInsights.id + } + } +} + +@description('Resource ID of the Application Insights component.') +output appInsightsId string = appInsights.id + +@description('Application ID of the Application Insights component (for trace queries).') +output appInsightsAppId string = appInsights.properties.AppId + +@description('Resource ID of the Log Analytics workspace backing Application Insights.') +output logAnalyticsId string = logAnalytics.id diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/blob-storage-container-role-assignments.bicep b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/blob-storage-container-role-assignments.bicep index 71abc97d6..817db115e 100644 --- a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/blob-storage-container-role-assignments.bicep +++ b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/blob-storage-container-role-assignments.bicep @@ -25,7 +25,7 @@ var conditionStr= '((!(ActionMatches{\'Microsoft.Storage/storageAccounts/blobSer // Assign Storage Blob Data Owner role resource storageBlobDataOwnerAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { scope: storage - name: guid(storageBlobDataOwner.id, storage.id) + name: guid(storage.id, aiProjectPrincipalId, storageBlobDataOwner.id, workspaceId) properties: { principalId: aiProjectPrincipalId roleDefinitionId: storageBlobDataOwner.id diff --git a/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/monitor-private-link-scope.bicep b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/monitor-private-link-scope.bicep new file mode 100644 index 000000000..522f0f20d --- /dev/null +++ b/infrastructure/infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup/modules-network-secured/monitor-private-link-scope.bicep @@ -0,0 +1,146 @@ +/* +Azure Monitor Private Link Scope (AMPLS) Module +----------------------------------------------- +This module enables private trace ingestion to Application Insights with: +1. Azure Monitor Private Link Scope (PrivateOnly ingestion, Open query) +2. Application Insights and Log Analytics added as scoped resources +3. Azure Monitor private DNS zones linked to the VNet +4. Private Endpoint (azuremonitor) in the PE subnet with a DNS zone group +*/ + +@description('Azure region for the private endpoint.') +param location string + +@description('Suffix for unique resource names (the template uniqueSuffix).') +param suffix string + +@description('Resource ID of the Application Insights component to scope into the AMPLS.') +param appInsightsId string + +@description('Resource ID of the Log Analytics workspace to scope into the AMPLS.') +param logAnalyticsId string + +@description('Resource ID of the Virtual Network.') +param vnetId string + +@description('Resource ID of the Private Endpoint subnet.') +param peSubnetId string + +@description('Map of Azure Monitor private DNS zone name to the resource group of an existing zone. An empty string for a zone means the module creates and links it in this resource group; a non-empty resource group means bring your own existing (e.g. centralized Azure Landing Zone) zone, which is referenced instead of recreated.') +param existingDnsZones object = { + 'privatelink.monitor.azure.com': '' + 'privatelink.oms.opinsights.azure.com': '' + 'privatelink.ods.opinsights.azure.com': '' + 'privatelink.agentsvc.azure-automation.net': '' +} + +@description('Subscription ID where existing Azure Monitor private DNS zones are located. Defaults to the current subscription.') +param dnsZonesSubscriptionId string = subscription().subscriptionId + +// Azure Monitor private DNS zones. Blob zone omitted: the standard templates already create + link it for BYO storage. +var monitorDnsZoneNames = [ + 'privatelink.monitor.azure.com' + 'privatelink.oms.opinsights.azure.com' + 'privatelink.ods.opinsights.azure.com' + 'privatelink.agentsvc.azure-automation.net' +] + +// 1. Azure Monitor Private Link Scope (private ingestion, open query). +resource ampls 'Microsoft.Insights/privateLinkScopes@2021-07-01-preview' = { + name: 'ampls-tracing-${suffix}' + location: 'global' + properties: { + accessModeSettings: { + ingestionAccessMode: 'PrivateOnly' + queryAccessMode: 'Open' + } + } +} + +// 2. Scope the Application Insights component and its Log Analytics workspace. +resource amplsAppInsights 'Microsoft.Insights/privateLinkScopes/scopedResources@2021-07-01-preview' = { + parent: ampls + name: 'appinsights-scoped' + properties: { + linkedResourceId: appInsightsId + } +} + +resource amplsLogAnalytics 'Microsoft.Insights/privateLinkScopes/scopedResources@2021-07-01-preview' = { + parent: ampls + name: 'law-scoped' + properties: { + linkedResourceId: logAnalyticsId + } +} + +// 3. The Azure Monitor private DNS zones. Zones are created and linked to the VNet only when +// not supplied via existingDnsZones; bring-your-own (centralized) zones are referenced as-is and +// are neither recreated nor relinked here, matching the ALZ centralized Private DNS Zone model. +resource monitorDnsZones 'Microsoft.Network/privateDnsZones@2020-06-01' = [for zone in monitorDnsZoneNames: if (empty(existingDnsZones[zone])) { + name: zone + location: 'global' +}] + +resource monitorDnsZoneLinks 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = [for (zone, i) in monitorDnsZoneNames: if (empty(existingDnsZones[zone])) { + parent: monitorDnsZones[i] + name: '${replace(zone, '.', '-')}-link' + location: 'global' + properties: { + virtualNetwork: { + id: vnetId + } + registrationEnabled: false + } +}] + +// Resolve each zone's resource ID: a newly created zone lives in this resource group, while a +// bring-your-own zone is referenced in its (optionally cross-subscription) resource group. +var monitorDnsZoneIds = [for zone in monitorDnsZoneNames: empty(existingDnsZones[zone]) + ? resourceId('Microsoft.Network/privateDnsZones', zone) + : extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', dnsZonesSubscriptionId, existingDnsZones[zone]), 'Microsoft.Network/privateDnsZones', zone)] + +// 4. Private endpoint to the AMPLS (group 'azuremonitor') + DNS zone group. +resource amplsPrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' = { + name: 'ampls-tracing-${suffix}-pe' + location: location + properties: { + subnet: { + id: peSubnetId + } + privateLinkServiceConnections: [ + { + name: 'ampls-connection' + properties: { + privateLinkServiceId: ampls.id + groupIds: [ + 'azuremonitor' + ] + } + } + ] + } + dependsOn: [ + amplsAppInsights + amplsLogAnalytics + ] +} + +resource amplsDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = { + parent: amplsPrivateEndpoint + name: 'ampls-dns' + properties: { + privateDnsZoneConfigs: [for (zone, i) in monitorDnsZoneNames: { + name: replace(zone, '.', '-') + properties: { + privateDnsZoneId: monitorDnsZoneIds[i] + } + }] + } + dependsOn: [ + monitorDnsZoneLinks + ] +} + +@description('Resource ID of the Azure Monitor Private Link Scope.') +output amplsId string = ampls.id diff --git a/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/README.md b/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/README.md index 76b82cfe9..7b069505c 100644 --- a/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/README.md +++ b/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/README.md @@ -369,6 +369,19 @@ Cosmos DB Account - Disabled local auth - Single region deployment +Azure Monitor (Application Insights & Log Analytics) +- Log Analytics Workspace: Microsoft.OperationalInsights/workspaces + - SKU: PerGB2018 + - Retention: 30 days +- Application Insights: Microsoft.Insights/components + - Kind: web + - Linked to Log Analytics workspace + - Public ingestion disabled (reached privately via AMPLS) +- Azure Monitor Private Link Scope (AMPLS): microsoft.insights/privateLinkScopes + - Access mode: PrivateOnly ingestion, Open query + - Scoped resources: Application Insights + Log Analytics + - Enables hosted agents to export telemetry via private network + ### Network Security Design This implementation utilizes a BYO VNet (Bring Your Own Virtual Network) approach, also known as custom VNet support with subnet delegation. Within your existing virtual network, one delegated subnet will be created. @@ -388,6 +401,7 @@ Private endpoints ensure secure, internal-only connectivity. Private endpoints a - Azure AI Search - Azure Storage - Azure Cosmos DB +- Azure Monitor Private Link Scope (AMPLS) — enables telemetry export from hosted agents **Private DNS Zones** | Private Link Resource Type | Sub Resource | Private DNS Zone Name | Public DNS Zone Forwarders | @@ -396,6 +410,7 @@ Private endpoints ensure secure, internal-only connectivity. Private endpoints a | **Azure AI Search** | searchService| `privatelink.search.windows.net` | `search.windows.net` | | **Azure Cosmos DB** | Sql | `privatelink.documents.azure.com` | `documents.azure.com` | | **Azure Storage** | blob | `privatelink.blob.core.windows.net` | `blob.core.windows.net` | +| **Azure Monitor (AMPLS)** | azuremonitor | `privatelink.monitor.azure.com`
`privatelink.oms.opinsights.azure.com`
`privatelink.ods.opinsights.azure.com`
`privatelink.agentsvc.azure-automation.net` | `monitor.azure.com`
`oms.opinsights.azure.com`
`ods.opinsights.azure.com`
`agentsvc.azure-automation.net` | ### Authentication & Authorization @@ -437,12 +452,14 @@ modules-network-secured/ ├── ai-account-identity.bicep # Microsoft Foundry deployment and configuration ├── ai-project-identity.bicep # Foundry project deployment and connection configuration ├── ai-search-role-assignments.bicep # AI Search RBAC configuration +├── application-insights.bicep # Workspace-based Application Insights for agent tracing ├── azure-storage-account-role-assignments.bicep # Storage Account RBAC configuration ├── blob-storage-container-role-assignments.bicep # Blob Storage Container RBAC configuration ├── cosmos-container-role-assignments.bicep # CosmosDB container Account RBAC configuration ├── cosmosdb-account-role-assignment.bicep # CosmosDB Account RBAC configuration ├── existing-vnet.bicep # Bring your existing virtual network to template deployment ├── format-project-workspace-id.bicep # Formatting the project workspace ID +├── monitor-private-link-scope.bicep # Azure Monitor Private Link Scope (AMPLS) for private telemetry ingestion ├── network-agent-vnet.bicep # Logic for routing virtual network set-up if existing virtual network is selected ├── private-endpoint-and-dns.bicep # Creating virtual networks and DNS zones. ├── standard-dependent-resources.bicep # Deploying CosmosDB, Storage, and Search diff --git a/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/azuredeploy.json b/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/azuredeploy.json index ad2fb1cc0..26ccd9d0a 100644 --- a/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/azuredeploy.json +++ b/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/azuredeploy.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "14940490256573158788" + "templateHash": "11886312503062392905" } }, "parameters": { @@ -2927,7 +2927,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "8509907656517137061" + "templateHash": "1735356790233715529" } }, "parameters": { @@ -2958,7 +2958,7 @@ "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageName'))]", - "name": "[guid(resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'), resourceId('Microsoft.Storage/storageAccounts', parameters('storageName')))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageName')), parameters('aiProjectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'), parameters('workspaceId'))]", "properties": { "principalId": "[parameters('aiProjectPrincipalId')]", "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b')]", diff --git a/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/main.bicep b/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/main.bicep index 30422c1a6..c0e033783 100644 --- a/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/main.bicep +++ b/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/main.bicep @@ -117,6 +117,14 @@ param existingDnsZones object = { 'privatelink.azurecr.io': '' } +@description('Object mapping Azure Monitor private DNS zone names to the resource group of an existing zone, or empty string to create it. Use to bring your own centralized Private DNS Zones (e.g. an Azure Landing Zone connectivity subscription) for agent tracing.') +param existingMonitorDnsZones object = { + 'privatelink.monitor.azure.com': '' + 'privatelink.oms.opinsights.azure.com': '' + 'privatelink.ods.opinsights.azure.com': '' + 'privatelink.agentsvc.azure-automation.net': '' +} + @description('Zone Names for Validation of existing Private Dns Zones') param dnsZoneNames array = [ 'privatelink.services.ai.azure.com' @@ -320,6 +328,37 @@ module acr 'modules-network-secured/container-registry.bicep' = if (enableContai ] } +// Application Insights for hosted-agent tracing (this template ships none). Creates a +// workspace-based Application Insights and connects it to the account so the agent exports traces. +module applicationInsights 'modules-network-secured/application-insights.bicep' = { + name: 'app-insights-${uniqueSuffix}-deployment' + params: { + location: location + suffix: uniqueSuffix + aiAccountName: aiAccount.outputs.accountName + disablePublicIngestion: true + } +} + +// Private trace ingestion path (Azure Monitor Private Link Scope) so an in-VNet agent's traces +// reach Application Insights over the private link rather than the (disabled) public endpoint. +module monitorPrivateLink 'modules-network-secured/monitor-private-link-scope.bicep' = { + name: 'monitor-pls-${uniqueSuffix}-deployment' + params: { + location: location + suffix: uniqueSuffix + appInsightsId: applicationInsights.outputs.appInsightsId + logAnalyticsId: applicationInsights.outputs.logAnalyticsId + vnetId: vnet.outputs.virtualNetworkId + peSubnetId: vnet.outputs.peSubnetId + existingDnsZones: existingMonitorDnsZones + dnsZonesSubscriptionId: resolvedDnsZonesSubscriptionId + } + dependsOn: [ + privateEndpointAndDNS + ] +} + /* Creates a new project (sub-resource of the AI Services account) */ diff --git a/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/modules-network-secured/application-insights.bicep b/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/modules-network-secured/application-insights.bicep new file mode 100644 index 000000000..22ada9222 --- /dev/null +++ b/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/modules-network-secured/application-insights.bicep @@ -0,0 +1,82 @@ +/* +Application Insights Module +--------------------------- +This module creates workspace-based Application Insights for agent tracing with: +1. Log Analytics workspace +2. Application Insights component (private ingestion for network-secured templates) +3. Connection on the Foundry account so agents export OpenTelemetry traces here +*/ + +@description('Azure region for the tracing resources.') +param location string + +@description('Suffix for unique resource names (the template uniqueSuffix).') +param suffix string + +@description('Name of the Foundry (AI Services) account to connect Application Insights to.') +param aiAccountName string + +@description('When true, disable public ingestion (reach Application Insights privately via AMPLS). Set false for public templates.') +param disablePublicIngestion bool = true + +@description('Name of the Log Analytics workspace to create.') +param logAnalyticsName string = 'law-tracing-${suffix}' + +@description('Name of the Application Insights component to create.') +param appInsightsName string = 'appi-tracing-${suffix}' + +resource aiAccount 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = { + name: aiAccountName + scope: resourceGroup() +} + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { + name: logAnalyticsName + location: location + properties: { + sku: { + name: 'PerGB2018' + } + retentionInDays: 30 + } +} + +resource appInsights 'Microsoft.Insights/components@2020-02-02' = { + name: appInsightsName + location: location + kind: 'web' + properties: { + Application_Type: 'web' + WorkspaceResourceId: logAnalytics.id + publicNetworkAccessForIngestion: disablePublicIngestion ? 'Disabled' : 'Enabled' + publicNetworkAccessForQuery: 'Enabled' + } +} + +// Foundry account connection (category AppInsights) so the agent exports OTel traces here. +resource connection 'Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview' = { + name: '${aiAccountName}-appinsights' + parent: aiAccount + properties: { + category: 'AppInsights' + target: appInsights.id + authType: 'ApiKey' + isSharedToAll: true + credentials: { + key: appInsights.properties.ConnectionString + } + metadata: { + ApiType: 'Azure' + ResourceId: appInsights.id + } + } +} + +@description('Resource ID of the Application Insights component.') +output appInsightsId string = appInsights.id + +@description('Application ID of the Application Insights component (for trace queries).') +output appInsightsAppId string = appInsights.properties.AppId + +@description('Resource ID of the Log Analytics workspace backing Application Insights.') +output logAnalyticsId string = logAnalytics.id diff --git a/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/modules-network-secured/blob-storage-container-role-assignments.bicep b/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/modules-network-secured/blob-storage-container-role-assignments.bicep index 71abc97d6..817db115e 100644 --- a/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/modules-network-secured/blob-storage-container-role-assignments.bicep +++ b/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/modules-network-secured/blob-storage-container-role-assignments.bicep @@ -25,7 +25,7 @@ var conditionStr= '((!(ActionMatches{\'Microsoft.Storage/storageAccounts/blobSer // Assign Storage Blob Data Owner role resource storageBlobDataOwnerAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { scope: storage - name: guid(storageBlobDataOwner.id, storage.id) + name: guid(storage.id, aiProjectPrincipalId, storageBlobDataOwner.id, workspaceId) properties: { principalId: aiProjectPrincipalId roleDefinitionId: storageBlobDataOwner.id diff --git a/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/modules-network-secured/monitor-private-link-scope.bicep b/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/modules-network-secured/monitor-private-link-scope.bicep new file mode 100644 index 000000000..522f0f20d --- /dev/null +++ b/infrastructure/infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup/modules-network-secured/monitor-private-link-scope.bicep @@ -0,0 +1,146 @@ +/* +Azure Monitor Private Link Scope (AMPLS) Module +----------------------------------------------- +This module enables private trace ingestion to Application Insights with: +1. Azure Monitor Private Link Scope (PrivateOnly ingestion, Open query) +2. Application Insights and Log Analytics added as scoped resources +3. Azure Monitor private DNS zones linked to the VNet +4. Private Endpoint (azuremonitor) in the PE subnet with a DNS zone group +*/ + +@description('Azure region for the private endpoint.') +param location string + +@description('Suffix for unique resource names (the template uniqueSuffix).') +param suffix string + +@description('Resource ID of the Application Insights component to scope into the AMPLS.') +param appInsightsId string + +@description('Resource ID of the Log Analytics workspace to scope into the AMPLS.') +param logAnalyticsId string + +@description('Resource ID of the Virtual Network.') +param vnetId string + +@description('Resource ID of the Private Endpoint subnet.') +param peSubnetId string + +@description('Map of Azure Monitor private DNS zone name to the resource group of an existing zone. An empty string for a zone means the module creates and links it in this resource group; a non-empty resource group means bring your own existing (e.g. centralized Azure Landing Zone) zone, which is referenced instead of recreated.') +param existingDnsZones object = { + 'privatelink.monitor.azure.com': '' + 'privatelink.oms.opinsights.azure.com': '' + 'privatelink.ods.opinsights.azure.com': '' + 'privatelink.agentsvc.azure-automation.net': '' +} + +@description('Subscription ID where existing Azure Monitor private DNS zones are located. Defaults to the current subscription.') +param dnsZonesSubscriptionId string = subscription().subscriptionId + +// Azure Monitor private DNS zones. Blob zone omitted: the standard templates already create + link it for BYO storage. +var monitorDnsZoneNames = [ + 'privatelink.monitor.azure.com' + 'privatelink.oms.opinsights.azure.com' + 'privatelink.ods.opinsights.azure.com' + 'privatelink.agentsvc.azure-automation.net' +] + +// 1. Azure Monitor Private Link Scope (private ingestion, open query). +resource ampls 'Microsoft.Insights/privateLinkScopes@2021-07-01-preview' = { + name: 'ampls-tracing-${suffix}' + location: 'global' + properties: { + accessModeSettings: { + ingestionAccessMode: 'PrivateOnly' + queryAccessMode: 'Open' + } + } +} + +// 2. Scope the Application Insights component and its Log Analytics workspace. +resource amplsAppInsights 'Microsoft.Insights/privateLinkScopes/scopedResources@2021-07-01-preview' = { + parent: ampls + name: 'appinsights-scoped' + properties: { + linkedResourceId: appInsightsId + } +} + +resource amplsLogAnalytics 'Microsoft.Insights/privateLinkScopes/scopedResources@2021-07-01-preview' = { + parent: ampls + name: 'law-scoped' + properties: { + linkedResourceId: logAnalyticsId + } +} + +// 3. The Azure Monitor private DNS zones. Zones are created and linked to the VNet only when +// not supplied via existingDnsZones; bring-your-own (centralized) zones are referenced as-is and +// are neither recreated nor relinked here, matching the ALZ centralized Private DNS Zone model. +resource monitorDnsZones 'Microsoft.Network/privateDnsZones@2020-06-01' = [for zone in monitorDnsZoneNames: if (empty(existingDnsZones[zone])) { + name: zone + location: 'global' +}] + +resource monitorDnsZoneLinks 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = [for (zone, i) in monitorDnsZoneNames: if (empty(existingDnsZones[zone])) { + parent: monitorDnsZones[i] + name: '${replace(zone, '.', '-')}-link' + location: 'global' + properties: { + virtualNetwork: { + id: vnetId + } + registrationEnabled: false + } +}] + +// Resolve each zone's resource ID: a newly created zone lives in this resource group, while a +// bring-your-own zone is referenced in its (optionally cross-subscription) resource group. +var monitorDnsZoneIds = [for zone in monitorDnsZoneNames: empty(existingDnsZones[zone]) + ? resourceId('Microsoft.Network/privateDnsZones', zone) + : extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', dnsZonesSubscriptionId, existingDnsZones[zone]), 'Microsoft.Network/privateDnsZones', zone)] + +// 4. Private endpoint to the AMPLS (group 'azuremonitor') + DNS zone group. +resource amplsPrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' = { + name: 'ampls-tracing-${suffix}-pe' + location: location + properties: { + subnet: { + id: peSubnetId + } + privateLinkServiceConnections: [ + { + name: 'ampls-connection' + properties: { + privateLinkServiceId: ampls.id + groupIds: [ + 'azuremonitor' + ] + } + } + ] + } + dependsOn: [ + amplsAppInsights + amplsLogAnalytics + ] +} + +resource amplsDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = { + parent: amplsPrivateEndpoint + name: 'ampls-dns' + properties: { + privateDnsZoneConfigs: [for (zone, i) in monitorDnsZoneNames: { + name: replace(zone, '.', '-') + properties: { + privateDnsZoneId: monitorDnsZoneIds[i] + } + }] + } + dependsOn: [ + monitorDnsZoneLinks + ] +} + +@description('Resource ID of the Azure Monitor Private Link Scope.') +output amplsId string = ampls.id diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/azuredeploy.json b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/azuredeploy.json index e196493a9..7aeeaa717 100644 --- a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/azuredeploy.json +++ b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/azuredeploy.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "6450781060789677542" + "templateHash": "15438675374015107303" } }, "parameters": { @@ -1533,7 +1533,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "13762210290131983061" + "templateHash": "7265170931147016210" } }, "parameters": { @@ -1736,9 +1736,9 @@ "dependsOn": [ "[resourceId('Microsoft.Network/privateEndpoints', format('ampls-pe-{0}', parameters('suffix')))]", "[resourceId('Microsoft.Network/privateDnsZones', variables('amplsDnsZones')[0])]", - "[resourceId('Microsoft.Network/privateDnsZones', variables('amplsDnsZones')[2])]", "[resourceId('Microsoft.Network/privateDnsZones', variables('amplsDnsZones')[3])]", - "[resourceId('Microsoft.Network/privateDnsZones', variables('amplsDnsZones')[1])]" + "[resourceId('Microsoft.Network/privateDnsZones', variables('amplsDnsZones')[1])]", + "[resourceId('Microsoft.Network/privateDnsZones', variables('amplsDnsZones')[2])]" ] } ], @@ -3517,7 +3517,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "8509907656517137061" + "templateHash": "1735356790233715529" } }, "parameters": { @@ -3548,7 +3548,7 @@ "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageName'))]", - "name": "[guid(resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'), resourceId('Microsoft.Storage/storageAccounts', parameters('storageName')))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageName')), parameters('aiProjectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'), parameters('workspaceId'))]", "properties": { "principalId": "[parameters('aiProjectPrincipalId')]", "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b')]", diff --git a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/blob-storage-container-role-assignments.bicep b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/blob-storage-container-role-assignments.bicep index 71abc97d6..817db115e 100644 --- a/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/blob-storage-container-role-assignments.bicep +++ b/infrastructure/infrastructure-setup-bicep/18-managed-virtual-network/modules-network-secured/blob-storage-container-role-assignments.bicep @@ -25,7 +25,7 @@ var conditionStr= '((!(ActionMatches{\'Microsoft.Storage/storageAccounts/blobSer // Assign Storage Blob Data Owner role resource storageBlobDataOwnerAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { scope: storage - name: guid(storageBlobDataOwner.id, storage.id) + name: guid(storage.id, aiProjectPrincipalId, storageBlobDataOwner.id, workspaceId) properties: { principalId: aiProjectPrincipalId roleDefinitionId: storageBlobDataOwner.id diff --git a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/README.md b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/README.md index f932214e9..110c1c6af 100644 --- a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/README.md +++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/README.md @@ -514,6 +514,19 @@ Cosmos DB Account - Disabled local auth - Single region deployment +Azure Monitor (Application Insights & Log Analytics) +- Log Analytics Workspace: Microsoft.OperationalInsights/workspaces + - SKU: PerGB2018 + - Retention: 30 days +- Application Insights: Microsoft.Insights/components + - Kind: web + - Linked to Log Analytics workspace + - Public ingestion disabled (reached privately via AMPLS) +- Azure Monitor Private Link Scope (AMPLS): microsoft.insights/privateLinkScopes + - Access mode: PrivateOnly ingestion, Open query + - Scoped resources: Application Insights + Log Analytics + - Enables hosted agents to export telemetry via private network + ### Network Security Design This implementation utilizes a BYO VNet (Bring Your Own Virtual Network) approach, also known as custom VNet support with subnet delegation. Within your existing virtual network, delegated subnets will be created. @@ -534,6 +547,7 @@ Private endpoints ensure secure, internal-only connectivity. Private endpoints a - Azure AI Search - Azure Storage - Azure Cosmos DB +- Azure Monitor Private Link Scope (AMPLS) — enables telemetry export from hosted agents **Private DNS Zones** | Private Link Resource Type | Sub Resource | Private DNS Zone Name | Public DNS Zone Forwarders | @@ -542,6 +556,7 @@ Private endpoints ensure secure, internal-only connectivity. Private endpoints a | **Azure AI Search** | searchService| `privatelink.search.windows.net` | `search.windows.net` | | **Azure Cosmos DB** | Sql | `privatelink.documents.azure.com` | `documents.azure.com` | | **Azure Storage** | blob | `privatelink.blob.core.windows.net` | `blob.core.windows.net` | +| **Azure Monitor (AMPLS)** | azuremonitor | `privatelink.monitor.azure.com`
`privatelink.oms.opinsights.azure.com`
`privatelink.ods.opinsights.azure.com`
`privatelink.agentsvc.azure-automation.net` | `monitor.azure.com`
`oms.opinsights.azure.com`
`ods.opinsights.azure.com`
`agentsvc.azure-automation.net` | ### Authentication & Authorization @@ -642,6 +657,7 @@ modules-network-secured/ ├── ai-project-identity.bicep # Foundry project deployment and connection configuration ├── ai-project-identity-unique.bicep # Modified project module with unique connection names ├── ai-search-role-assignments.bicep # AI Search RBAC configuration +├── application-insights.bicep # Workspace-based Application Insights for agent tracing ├── azure-storage-account-role-assignment.bicep # Storage Account RBAC configuration ├── blob-storage-container-role-assignments.bicep # Blob Storage Container RBAC configuration ├── blob-storage-container-role-assignments-unique.bicep # Modified storage role assignment module @@ -649,6 +665,7 @@ modules-network-secured/ ├── cosmosdb-account-role-assignment.bicep # CosmosDB Account RBAC configuration ├── existing-vnet.bicep # Bring your existing virtual network to template deployment ├── format-project-workspace-id.bicep # Formatting the project workspace ID +├── monitor-private-link-scope.bicep # Azure Monitor Private Link Scope (AMPLS) for private telemetry ingestion ├── network-agent-vnet.bicep # Logic for routing virtual network set-up if existing virtual network is selected ├── private-endpoint-and-dns.bicep # Creating virtual networks and DNS zones. ├── standard-dependent-resources.bicep # Deploying CosmosDB, Storage, and Search diff --git a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azuredeploy.json b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azuredeploy.json index c2aa228bd..df6e44466 100644 --- a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azuredeploy.json +++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/azuredeploy.json @@ -5,7 +5,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "8682967860099869832" + "templateHash": "3037343684485026788" } }, "parameters": { @@ -3075,7 +3075,7 @@ "_generator": { "name": "bicep", "version": "0.44.1.10279", - "templateHash": "8509907656517137061" + "templateHash": "1735356790233715529" } }, "parameters": { @@ -3106,7 +3106,7 @@ "type": "Microsoft.Authorization/roleAssignments", "apiVersion": "2022-04-01", "scope": "[resourceId('Microsoft.Storage/storageAccounts', parameters('storageName'))]", - "name": "[guid(resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'), resourceId('Microsoft.Storage/storageAccounts', parameters('storageName')))]", + "name": "[guid(resourceId('Microsoft.Storage/storageAccounts', parameters('storageName')), parameters('aiProjectPrincipalId'), resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b'), parameters('workspaceId'))]", "properties": { "principalId": "[parameters('aiProjectPrincipalId')]", "roleDefinitionId": "[resourceId('Microsoft.Authorization/roleDefinitions', 'b7e6dc6d-f1e8-4753-8033-0f276bb0955b')]", diff --git a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/main.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/main.bicep index b962a3036..0965ebc81 100644 --- a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/main.bicep +++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/main.bicep @@ -151,6 +151,14 @@ param existingDnsZones object = { 'privatelink.azurecr.io': { subscriptionId: '', resourceGroup: '' } } +@description('Object mapping Azure Monitor private DNS zone names to an existing zone subscription/resource group, or empty strings to create it. Use to bring your own centralized Private DNS Zones (e.g. an Azure Landing Zone) for agent tracing.') +param existingMonitorDnsZones object = { + 'privatelink.monitor.azure.com': { subscriptionId: '', resourceGroup: '' } + 'privatelink.oms.opinsights.azure.com': { subscriptionId: '', resourceGroup: '' } + 'privatelink.ods.opinsights.azure.com': { subscriptionId: '', resourceGroup: '' } + 'privatelink.agentsvc.azure-automation.net': { subscriptionId: '', resourceGroup: '' } +} + var projectName = toLower('${firstProjectName}${uniqueSuffix}') var cosmosDBName = toLower('${aiServices}${uniqueSuffix}cosmosdb') var aiSearchName = toLower('${aiServices}${uniqueSuffix}search') @@ -311,7 +319,10 @@ module acr 'modules-network-secured/container-registry.bicep' = if (enableContai peSubnetId: vnet.outputs.peSubnetId vnetId: vnet.outputs.virtualNetworkId suffix: uniqueSuffix - existingDnsZoneResourceGroup: existingDnsZones['privatelink.azurecr.io'].resourceGroup + // The central private-endpoint-and-dns module already creates privatelink.azurecr.io and + // links it to the VNet (azurecr is in existingDnsZones). Point the ACR module at that existing + // zone so it references it instead of creating a second (conflicting) VNet link. + existingDnsZoneResourceGroup: empty(existingDnsZones['privatelink.azurecr.io'].resourceGroup) ? resourceGroup().name : existingDnsZones['privatelink.azurecr.io'].resourceGroup dnsZonesSubscriptionId: empty(existingDnsZones['privatelink.azurecr.io'].subscriptionId) ? subscription().subscriptionId : existingDnsZones['privatelink.azurecr.io'].subscriptionId developerIpCidr: developerIpCidr projectPrincipalId: aiProject.outputs.projectPrincipalId @@ -321,6 +332,36 @@ module acr 'modules-network-secured/container-registry.bicep' = if (enableContai ] } +// Application Insights for hosted-agent tracing (this template ships none). Creates a +// workspace-based Application Insights and connects it to the account so the agent exports traces. +module applicationInsights 'modules-network-secured/application-insights.bicep' = { + name: 'app-insights-${uniqueSuffix}-deployment' + params: { + location: location + suffix: uniqueSuffix + aiAccountName: aiAccount.outputs.accountName + disablePublicIngestion: true + } +} + +// Private trace ingestion path (Azure Monitor Private Link Scope) so an in-VNet agent's traces +// reach Application Insights over the private link rather than the (disabled) public endpoint. +module monitorPrivateLink 'modules-network-secured/monitor-private-link-scope.bicep' = { + name: 'monitor-pls-${uniqueSuffix}-deployment' + params: { + location: location + suffix: uniqueSuffix + appInsightsId: applicationInsights.outputs.appInsightsId + logAnalyticsId: applicationInsights.outputs.logAnalyticsId + vnetId: vnet.outputs.virtualNetworkId + peSubnetId: vnet.outputs.peSubnetId + existingDnsZones: existingMonitorDnsZones + } + dependsOn: [ + privateEndpointAndDNS + ] +} + /* Creates a new project (sub-resource of the AI Services account) */ diff --git a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/application-insights.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/application-insights.bicep new file mode 100644 index 000000000..22ada9222 --- /dev/null +++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/application-insights.bicep @@ -0,0 +1,82 @@ +/* +Application Insights Module +--------------------------- +This module creates workspace-based Application Insights for agent tracing with: +1. Log Analytics workspace +2. Application Insights component (private ingestion for network-secured templates) +3. Connection on the Foundry account so agents export OpenTelemetry traces here +*/ + +@description('Azure region for the tracing resources.') +param location string + +@description('Suffix for unique resource names (the template uniqueSuffix).') +param suffix string + +@description('Name of the Foundry (AI Services) account to connect Application Insights to.') +param aiAccountName string + +@description('When true, disable public ingestion (reach Application Insights privately via AMPLS). Set false for public templates.') +param disablePublicIngestion bool = true + +@description('Name of the Log Analytics workspace to create.') +param logAnalyticsName string = 'law-tracing-${suffix}' + +@description('Name of the Application Insights component to create.') +param appInsightsName string = 'appi-tracing-${suffix}' + +resource aiAccount 'Microsoft.CognitiveServices/accounts@2025-04-01-preview' existing = { + name: aiAccountName + scope: resourceGroup() +} + +resource logAnalytics 'Microsoft.OperationalInsights/workspaces@2023-09-01' = { + name: logAnalyticsName + location: location + properties: { + sku: { + name: 'PerGB2018' + } + retentionInDays: 30 + } +} + +resource appInsights 'Microsoft.Insights/components@2020-02-02' = { + name: appInsightsName + location: location + kind: 'web' + properties: { + Application_Type: 'web' + WorkspaceResourceId: logAnalytics.id + publicNetworkAccessForIngestion: disablePublicIngestion ? 'Disabled' : 'Enabled' + publicNetworkAccessForQuery: 'Enabled' + } +} + +// Foundry account connection (category AppInsights) so the agent exports OTel traces here. +resource connection 'Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview' = { + name: '${aiAccountName}-appinsights' + parent: aiAccount + properties: { + category: 'AppInsights' + target: appInsights.id + authType: 'ApiKey' + isSharedToAll: true + credentials: { + key: appInsights.properties.ConnectionString + } + metadata: { + ApiType: 'Azure' + ResourceId: appInsights.id + } + } +} + +@description('Resource ID of the Application Insights component.') +output appInsightsId string = appInsights.id + +@description('Application ID of the Application Insights component (for trace queries).') +output appInsightsAppId string = appInsights.properties.AppId + +@description('Resource ID of the Log Analytics workspace backing Application Insights.') +output logAnalyticsId string = logAnalytics.id diff --git a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/blob-storage-container-role-assignments.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/blob-storage-container-role-assignments.bicep index 71abc97d6..817db115e 100644 --- a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/blob-storage-container-role-assignments.bicep +++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/blob-storage-container-role-assignments.bicep @@ -25,7 +25,7 @@ var conditionStr= '((!(ActionMatches{\'Microsoft.Storage/storageAccounts/blobSer // Assign Storage Blob Data Owner role resource storageBlobDataOwnerAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = { scope: storage - name: guid(storageBlobDataOwner.id, storage.id) + name: guid(storage.id, aiProjectPrincipalId, storageBlobDataOwner.id, workspaceId) properties: { principalId: aiProjectPrincipalId roleDefinitionId: storageBlobDataOwner.id diff --git a/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/monitor-private-link-scope.bicep b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/monitor-private-link-scope.bicep new file mode 100644 index 000000000..be8eeb1a1 --- /dev/null +++ b/infrastructure/infrastructure-setup-bicep/19-private-network-agent-tools/modules-network-secured/monitor-private-link-scope.bicep @@ -0,0 +1,146 @@ +/* +Azure Monitor Private Link Scope (AMPLS) Module +----------------------------------------------- +This module enables private trace ingestion to Application Insights with: +1. Azure Monitor Private Link Scope (PrivateOnly ingestion, Open query) +2. Application Insights and Log Analytics added as scoped resources +3. Azure Monitor private DNS zones linked to the VNet +4. Private Endpoint (azuremonitor) in the PE subnet with a DNS zone group +*/ + +@description('Azure region for the private endpoint.') +param location string + +@description('Suffix for unique resource names (the template uniqueSuffix).') +param suffix string + +@description('Resource ID of the Application Insights component to scope into the AMPLS.') +param appInsightsId string + +@description('Resource ID of the Log Analytics workspace to scope into the AMPLS.') +param logAnalyticsId string + +@description('Resource ID of the Virtual Network.') +param vnetId string + +@description('Resource ID of the Private Endpoint subnet.') +param peSubnetId string + +@description('Map of Azure Monitor private DNS zone name to an existing zone subscription/resource group. Empty strings for a zone mean the module creates and links it in this resource group.') +param existingDnsZones object = { + 'privatelink.monitor.azure.com': { subscriptionId: '', resourceGroup: '' } + 'privatelink.oms.opinsights.azure.com': { subscriptionId: '', resourceGroup: '' } + 'privatelink.ods.opinsights.azure.com': { subscriptionId: '', resourceGroup: '' } + 'privatelink.agentsvc.azure-automation.net': { subscriptionId: '', resourceGroup: '' } +} + +// Azure Monitor private DNS zones. Blob zone omitted: the standard templates already create + link it for BYO storage. +var monitorDnsZoneNames = [ + 'privatelink.monitor.azure.com' + 'privatelink.oms.opinsights.azure.com' + 'privatelink.ods.opinsights.azure.com' + 'privatelink.agentsvc.azure-automation.net' +] + +var existingDnsZoneResourceGroups = [for zone in monitorDnsZoneNames: existingDnsZones[zone].resourceGroup] +var existingDnsZoneSubscriptionIds = [for zone in monitorDnsZoneNames: empty(existingDnsZones[zone].subscriptionId) ? subscription().subscriptionId : existingDnsZones[zone].subscriptionId] + +// 1. Azure Monitor Private Link Scope (private ingestion, open query). +resource ampls 'Microsoft.Insights/privateLinkScopes@2021-07-01-preview' = { + name: 'ampls-tracing-${suffix}' + location: 'global' + properties: { + accessModeSettings: { + ingestionAccessMode: 'PrivateOnly' + queryAccessMode: 'Open' + } + } +} + +// 2. Scope the Application Insights component and its Log Analytics workspace. +resource amplsAppInsights 'Microsoft.Insights/privateLinkScopes/scopedResources@2021-07-01-preview' = { + parent: ampls + name: 'appinsights-scoped' + properties: { + linkedResourceId: appInsightsId + } +} + +resource amplsLogAnalytics 'Microsoft.Insights/privateLinkScopes/scopedResources@2021-07-01-preview' = { + parent: ampls + name: 'law-scoped' + properties: { + linkedResourceId: logAnalyticsId + } +} + +// 3. The Azure Monitor private DNS zones. Zones are created and linked to the VNet only when +// not supplied via existingDnsZones; bring-your-own (centralized) zones are referenced as-is and +// are neither recreated nor relinked here, matching the ALZ centralized Private DNS Zone model. +resource monitorDnsZones 'Microsoft.Network/privateDnsZones@2020-06-01' = [for (zone, i) in monitorDnsZoneNames: if (empty(existingDnsZoneResourceGroups[i])) { + name: zone + location: 'global' +}] + +resource monitorDnsZoneLinks 'Microsoft.Network/privateDnsZones/virtualNetworkLinks@2024-06-01' = [for (zone, i) in monitorDnsZoneNames: if (empty(existingDnsZoneResourceGroups[i])) { + parent: monitorDnsZones[i] + name: '${replace(zone, '.', '-')}-link' + location: 'global' + properties: { + virtualNetwork: { + id: vnetId + } + registrationEnabled: false + } +}] + +// Resolve each zone's resource ID: a newly created zone lives in this resource group, while a +// bring-your-own zone is referenced in its (optionally cross-subscription) resource group. +var monitorDnsZoneIds = [for (zone, i) in monitorDnsZoneNames: empty(existingDnsZoneResourceGroups[i]) + ? resourceId('Microsoft.Network/privateDnsZones', zone) + : extensionResourceId(format('/subscriptions/{0}/resourceGroups/{1}', existingDnsZoneSubscriptionIds[i], existingDnsZoneResourceGroups[i]), 'Microsoft.Network/privateDnsZones', zone)] + +// 4. Private endpoint to the AMPLS (group 'azuremonitor') + DNS zone group. +resource amplsPrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' = { + name: 'ampls-tracing-${suffix}-pe' + location: location + properties: { + subnet: { + id: peSubnetId + } + privateLinkServiceConnections: [ + { + name: 'ampls-connection' + properties: { + privateLinkServiceId: ampls.id + groupIds: [ + 'azuremonitor' + ] + } + } + ] + } + dependsOn: [ + amplsAppInsights + amplsLogAnalytics + ] +} + +resource amplsDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = { + parent: amplsPrivateEndpoint + name: 'ampls-dns' + properties: { + privateDnsZoneConfigs: [for (zone, i) in monitorDnsZoneNames: { + name: replace(zone, '.', '-') + properties: { + privateDnsZoneId: monitorDnsZoneIds[i] + } + }] + } + dependsOn: [ + monitorDnsZoneLinks + ] +} + +@description('Resource ID of the Azure Monitor Private Link Scope.') +output amplsId string = ampls.id diff --git a/infrastructure/infrastructure-setup-terraform/11-private-network-basic-vnet/README.md b/infrastructure/infrastructure-setup-terraform/11-private-network-basic-vnet/README.md index 4f62b405f..d39c8aff1 100644 --- a/infrastructure/infrastructure-setup-terraform/11-private-network-basic-vnet/README.md +++ b/infrastructure/infrastructure-setup-terraform/11-private-network-basic-vnet/README.md @@ -93,6 +93,9 @@ terraform apply -var-file=terraform.tfvars - Capability Host (basic agent) - Model Deployment (GPT-4o) - Azure Container Registry with Private Endpoint (optional) +- Application Insights and Log Analytics Workspace (agent tracing) +- Azure Monitor Private Link Scope (AMPLS) with Private Endpoint +- Private DNS Zones for Azure Monitor (monitor, oms, ods, agentsvc) ## Documentation diff --git a/infrastructure/infrastructure-setup-terraform/11-private-network-basic-vnet/code/monitor.tf b/infrastructure/infrastructure-setup-terraform/11-private-network-basic-vnet/code/monitor.tf new file mode 100644 index 000000000..3e86ba3de --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/11-private-network-basic-vnet/code/monitor.tf @@ -0,0 +1,197 @@ +########## Create Azure Monitor resources for agent tracing +########## +## Deploys Application Insights and an Azure Monitor Private Link Scope (AMPLS) so the +## hosted agent in the VNet can export OpenTelemetry traces to Application Insights over +## the private network instead of having the spans dropped. + +## Create the Log Analytics workspace that backs Application Insights +## +resource "azurerm_log_analytics_workspace" "loganalytics" { + name = "loganalytics-tracing-${random_string.unique.result}" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + sku = "PerGB2018" + retention_in_days = 30 +} + +## Create the workspace-based Application Insights. Public ingestion is disabled; traces +## are ingested privately through the Azure Monitor Private Link Scope below. +## +resource "azurerm_application_insights" "app_insights" { + name = "appinsights-tracing-${random_string.unique.result}" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + workspace_id = azurerm_log_analytics_workspace.loganalytics.id + application_type = "web" + internet_ingestion_enabled = false + internet_query_enabled = true +} + +## Create the Azure Monitor Private Link Scope (private ingestion, open query) +## +resource "azurerm_monitor_private_link_scope" "ampls" { + name = "ampls-tracing-${random_string.unique.result}" + resource_group_name = azurerm_resource_group.rg.name + ingestion_access_mode = "PrivateOnly" + query_access_mode = "Open" +} + +## Scope Application Insights and the Log Analytics workspace into the AMPLS +## +resource "azurerm_monitor_private_link_scoped_service" "ampls_app_insights" { + name = "appinsights-scoped" + resource_group_name = azurerm_resource_group.rg.name + scope_name = azurerm_monitor_private_link_scope.ampls.name + linked_resource_id = azurerm_application_insights.app_insights.id +} + +resource "azurerm_monitor_private_link_scoped_service" "ampls_loganalytics" { + name = "loganalytics-scoped" + resource_group_name = azurerm_resource_group.rg.name + scope_name = azurerm_monitor_private_link_scope.ampls.name + linked_resource_id = azurerm_log_analytics_workspace.loganalytics.id +} + +## Create the Azure Monitor Private DNS Zones +## +resource "azurerm_private_dns_zone" "plz_monitor" { + name = "privatelink.monitor.azure.com" + resource_group_name = azurerm_resource_group.rg.name +} + +resource "azurerm_private_dns_zone" "plz_oms" { + name = "privatelink.oms.opinsights.azure.com" + resource_group_name = azurerm_resource_group.rg.name +} + +resource "azurerm_private_dns_zone" "plz_ods" { + name = "privatelink.ods.opinsights.azure.com" + resource_group_name = azurerm_resource_group.rg.name +} + +resource "azurerm_private_dns_zone" "plz_agentsvc" { + name = "privatelink.agentsvc.azure-automation.net" + resource_group_name = azurerm_resource_group.rg.name +} + +## Create Private DNS Zone Links to link the Azure Monitor zones to the virtual network +## +resource "azurerm_private_dns_zone_virtual_network_link" "plz_monitor_link" { + depends_on = [ + azurerm_private_dns_zone.plz_monitor, + azurerm_virtual_network.vnet + ] + name = "monitor-${random_string.unique.result}-link" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.plz_monitor.name + virtual_network_id = azurerm_virtual_network.vnet.id + registration_enabled = false +} + +resource "azurerm_private_dns_zone_virtual_network_link" "plz_oms_link" { + depends_on = [ + azurerm_private_dns_zone_virtual_network_link.plz_monitor_link, + azurerm_private_dns_zone.plz_oms, + azurerm_virtual_network.vnet + ] + name = "oms-${random_string.unique.result}-link" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.plz_oms.name + virtual_network_id = azurerm_virtual_network.vnet.id + registration_enabled = false +} + +resource "azurerm_private_dns_zone_virtual_network_link" "plz_ods_link" { + depends_on = [ + azurerm_private_dns_zone_virtual_network_link.plz_oms_link, + azurerm_private_dns_zone.plz_ods, + azurerm_virtual_network.vnet + ] + name = "ods-${random_string.unique.result}-link" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.plz_ods.name + virtual_network_id = azurerm_virtual_network.vnet.id + registration_enabled = false +} + +resource "azurerm_private_dns_zone_virtual_network_link" "plz_agentsvc_link" { + depends_on = [ + azurerm_private_dns_zone_virtual_network_link.plz_ods_link, + azurerm_private_dns_zone.plz_agentsvc, + azurerm_virtual_network.vnet + ] + name = "agentsvc-${random_string.unique.result}-link" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.plz_agentsvc.name + virtual_network_id = azurerm_virtual_network.vnet.id + registration_enabled = false +} + +## Create the Private Endpoint for the AMPLS (group "azuremonitor") +## +resource "azurerm_private_endpoint" "pe_ampls" { + depends_on = [ + azurerm_monitor_private_link_scoped_service.ampls_app_insights, + azurerm_monitor_private_link_scoped_service.ampls_loganalytics, + azurerm_private_dns_zone_virtual_network_link.plz_monitor_link, + azurerm_private_dns_zone_virtual_network_link.plz_oms_link, + azurerm_private_dns_zone_virtual_network_link.plz_ods_link, + azurerm_private_dns_zone_virtual_network_link.plz_agentsvc_link, + azurerm_virtual_network.vnet + ] + + name = "ampls-tracing-${random_string.unique.result}-private-endpoint" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + subnet_id = azurerm_subnet.subnet_pe.id + + private_service_connection { + name = "ampls-tracing-private-link-service-connection" + private_connection_resource_id = azurerm_monitor_private_link_scope.ampls.id + subresource_names = [ + "azuremonitor" + ] + is_manual_connection = false + } + + private_dns_zone_group { + name = "ampls-tracing-dns-config" + private_dns_zone_ids = [ + azurerm_private_dns_zone.plz_monitor.id, + azurerm_private_dns_zone.plz_oms.id, + azurerm_private_dns_zone.plz_ods.id, + azurerm_private_dns_zone.plz_agentsvc.id + ] + } +} + +## Create the AI Foundry project connection to Application Insights so the agent exports +## its OpenTelemetry traces here +## +resource "azapi_resource" "conn_app_insights" { + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-06-01" + name = azurerm_application_insights.app_insights.name + parent_id = azapi_resource.ai_foundry_project.id + schema_validation_enabled = false + + depends_on = [ + azapi_resource.ai_foundry_project + ] + + body = { + name = azurerm_application_insights.app_insights.name + properties = { + category = "AppInsights" + target = azurerm_application_insights.app_insights.id + authType = "ApiKey" + credentials = { + key = azurerm_application_insights.app_insights.connection_string + } + metadata = { + ApiType = "Azure" + ResourceId = azurerm_application_insights.app_insights.id + location = var.location + } + } + } +} diff --git a/infrastructure/infrastructure-setup-terraform/15a-private-network-standard-agent-setup/README.md b/infrastructure/infrastructure-setup-terraform/15a-private-network-standard-agent-setup/README.md index 8f2572cdb..aef7d2c58 100644 --- a/infrastructure/infrastructure-setup-terraform/15a-private-network-standard-agent-setup/README.md +++ b/infrastructure/infrastructure-setup-terraform/15a-private-network-standard-agent-setup/README.md @@ -258,6 +258,7 @@ code/ ├── data.tf # Creates data objects for active subscription being deployed to and deployment security context ├── locals.tf # Creates local variables for project GUID ├── main.tf # Main deployment file +├── monitor.tf # Azure Monitor (Application Insights + AMPLS) for agent tracing ├── outputs.tf # Placeholder file for future outputs ├── providers.tf # Terraform provider configuration ├── example.tfvars # Sample tfvars file diff --git a/infrastructure/infrastructure-setup-terraform/15a-private-network-standard-agent-setup/code/main.tf b/infrastructure/infrastructure-setup-terraform/15a-private-network-standard-agent-setup/code/main.tf index 14950dd85..cee79be36 100644 --- a/infrastructure/infrastructure-setup-terraform/15a-private-network-standard-agent-setup/code/main.tf +++ b/infrastructure/infrastructure-setup-terraform/15a-private-network-standard-agent-setup/code/main.tf @@ -752,7 +752,7 @@ resource "azurerm_role_assignment" "storage_blob_data_owner_ai_foundry_project" resource "azapi_resource_action" "purge_ai_foundry" { method = "DELETE" resource_id = "/subscriptions/${data.azurerm_client_config.current.subscription_id}/providers/Microsoft.CognitiveServices/locations/${azurerm_resource_group.rg.location}/resourceGroups/${azurerm_resource_group.rg.name}/deletedAccounts/aifoundry${random_string.unique.result}" - type = "Microsoft.Resources/resourceGroups/deletedAccounts@2021-04-30" + type = "Microsoft.CognitiveServices/locations/resourceGroups/deletedAccounts@2021-04-30" when = "destroy" depends_on = [time_sleep.purge_ai_foundry_cooldown] diff --git a/infrastructure/infrastructure-setup-terraform/15a-private-network-standard-agent-setup/code/monitor.tf b/infrastructure/infrastructure-setup-terraform/15a-private-network-standard-agent-setup/code/monitor.tf new file mode 100644 index 000000000..9abcd0a7a --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/15a-private-network-standard-agent-setup/code/monitor.tf @@ -0,0 +1,198 @@ +########## Create Azure Monitor resources for agent tracing +########## +## Deploys Application Insights and an Azure Monitor Private Link Scope (AMPLS) so the +## hosted agent in the VNet can export OpenTelemetry traces to Application Insights over +## the private network instead of having the spans dropped. + +## Create the Log Analytics workspace that backs Application Insights +## +resource "azurerm_log_analytics_workspace" "loganalytics" { + name = "loganalytics-tracing-${random_string.unique.result}" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + sku = "PerGB2018" + retention_in_days = 30 +} + +## Create the workspace-based Application Insights. Public ingestion is disabled; traces +## are ingested privately through the Azure Monitor Private Link Scope below. +## +resource "azurerm_application_insights" "app_insights" { + name = "appinsights-tracing-${random_string.unique.result}" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + workspace_id = azurerm_log_analytics_workspace.loganalytics.id + application_type = "web" + internet_ingestion_enabled = false + internet_query_enabled = true +} + +## Create the Azure Monitor Private Link Scope (private ingestion, open query) +## +resource "azurerm_monitor_private_link_scope" "ampls" { + name = "ampls-tracing-${random_string.unique.result}" + resource_group_name = azurerm_resource_group.rg.name + ingestion_access_mode = "PrivateOnly" + query_access_mode = "Open" +} + +## Scope Application Insights and the Log Analytics workspace into the AMPLS +## +resource "azurerm_monitor_private_link_scoped_service" "ampls_app_insights" { + name = "appinsights-scoped" + resource_group_name = azurerm_resource_group.rg.name + scope_name = azurerm_monitor_private_link_scope.ampls.name + linked_resource_id = azurerm_application_insights.app_insights.id +} + +resource "azurerm_monitor_private_link_scoped_service" "ampls_loganalytics" { + name = "loganalytics-scoped" + resource_group_name = azurerm_resource_group.rg.name + scope_name = azurerm_monitor_private_link_scope.ampls.name + linked_resource_id = azurerm_log_analytics_workspace.loganalytics.id +} + +## Create the Azure Monitor Private DNS Zones. The blob zone is intentionally omitted: the +## standard template already creates and links privatelink.blob.core.windows.net. +## +resource "azurerm_private_dns_zone" "plz_monitor" { + name = "privatelink.monitor.azure.com" + resource_group_name = azurerm_resource_group.rg.name +} + +resource "azurerm_private_dns_zone" "plz_oms" { + name = "privatelink.oms.opinsights.azure.com" + resource_group_name = azurerm_resource_group.rg.name +} + +resource "azurerm_private_dns_zone" "plz_ods" { + name = "privatelink.ods.opinsights.azure.com" + resource_group_name = azurerm_resource_group.rg.name +} + +resource "azurerm_private_dns_zone" "plz_agentsvc" { + name = "privatelink.agentsvc.azure-automation.net" + resource_group_name = azurerm_resource_group.rg.name +} + +## Create Private DNS Zone Links to link the Azure Monitor zones to the virtual network +## +resource "azurerm_private_dns_zone_virtual_network_link" "plz_monitor_link" { + depends_on = [ + azurerm_private_dns_zone.plz_monitor, + azurerm_virtual_network.vnet + ] + name = "monitor-${random_string.unique.result}-link" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.plz_monitor.name + virtual_network_id = azurerm_virtual_network.vnet.id + registration_enabled = false +} + +resource "azurerm_private_dns_zone_virtual_network_link" "plz_oms_link" { + depends_on = [ + azurerm_private_dns_zone_virtual_network_link.plz_monitor_link, + azurerm_private_dns_zone.plz_oms, + azurerm_virtual_network.vnet + ] + name = "oms-${random_string.unique.result}-link" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.plz_oms.name + virtual_network_id = azurerm_virtual_network.vnet.id + registration_enabled = false +} + +resource "azurerm_private_dns_zone_virtual_network_link" "plz_ods_link" { + depends_on = [ + azurerm_private_dns_zone_virtual_network_link.plz_oms_link, + azurerm_private_dns_zone.plz_ods, + azurerm_virtual_network.vnet + ] + name = "ods-${random_string.unique.result}-link" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.plz_ods.name + virtual_network_id = azurerm_virtual_network.vnet.id + registration_enabled = false +} + +resource "azurerm_private_dns_zone_virtual_network_link" "plz_agentsvc_link" { + depends_on = [ + azurerm_private_dns_zone_virtual_network_link.plz_ods_link, + azurerm_private_dns_zone.plz_agentsvc, + azurerm_virtual_network.vnet + ] + name = "agentsvc-${random_string.unique.result}-link" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.plz_agentsvc.name + virtual_network_id = azurerm_virtual_network.vnet.id + registration_enabled = false +} + +## Create the Private Endpoint for the AMPLS (group "azuremonitor") +## +resource "azurerm_private_endpoint" "pe_ampls" { + depends_on = [ + azurerm_monitor_private_link_scoped_service.ampls_app_insights, + azurerm_monitor_private_link_scoped_service.ampls_loganalytics, + azurerm_private_dns_zone_virtual_network_link.plz_monitor_link, + azurerm_private_dns_zone_virtual_network_link.plz_oms_link, + azurerm_private_dns_zone_virtual_network_link.plz_ods_link, + azurerm_private_dns_zone_virtual_network_link.plz_agentsvc_link, + azurerm_virtual_network.vnet + ] + + name = "ampls-tracing-${random_string.unique.result}-private-endpoint" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + subnet_id = azurerm_subnet.subnet_pe.id + + private_service_connection { + name = "ampls-tracing-private-link-service-connection" + private_connection_resource_id = azurerm_monitor_private_link_scope.ampls.id + subresource_names = [ + "azuremonitor" + ] + is_manual_connection = false + } + + private_dns_zone_group { + name = "ampls-tracing-dns-config" + private_dns_zone_ids = [ + azurerm_private_dns_zone.plz_monitor.id, + azurerm_private_dns_zone.plz_oms.id, + azurerm_private_dns_zone.plz_ods.id, + azurerm_private_dns_zone.plz_agentsvc.id + ] + } +} + +## Create the AI Foundry project connection to Application Insights so the agent exports +## its OpenTelemetry traces here +## +resource "azapi_resource" "conn_app_insights" { + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-06-01" + name = azurerm_application_insights.app_insights.name + parent_id = azapi_resource.ai_foundry_project.id + schema_validation_enabled = false + + depends_on = [ + azapi_resource.ai_foundry_project + ] + + body = { + name = azurerm_application_insights.app_insights.name + properties = { + category = "AppInsights" + target = azurerm_application_insights.app_insights.id + authType = "ApiKey" + credentials = { + key = azurerm_application_insights.app_insights.connection_string + } + metadata = { + ApiType = "Azure" + ResourceId = azurerm_application_insights.app_insights.id + location = var.location + } + } + } +} diff --git a/infrastructure/infrastructure-setup-terraform/15b-private-network-standard-agent-setup-byovnet/README.md b/infrastructure/infrastructure-setup-terraform/15b-private-network-standard-agent-setup-byovnet/README.md index fa1076d88..a228e0653 100644 --- a/infrastructure/infrastructure-setup-terraform/15b-private-network-standard-agent-setup-byovnet/README.md +++ b/infrastructure/infrastructure-setup-terraform/15b-private-network-standard-agent-setup-byovnet/README.md @@ -48,7 +48,7 @@ Use the table below to choose the right Terraform infrastructure template for yo | [**15a**](../15a-private-network-standard-agent-setup/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | Standalone E2E network isolation (creates VNet, DNS zones, and resource group) | | [**17**](../17-private-network-standard-user-assigned-identity-agent-setup/) | Standard (BYO resources) | BYO VNet + Private Endpoints | **User Assigned MI** | Same as 15a but with user-managed identity | | [**16**](../16-private-network-standard-agent-apim-setup-preview/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | Same as 15a **plus** private APIM integration (preview) | -| [**19**](../19-hybrid-private-resources-agent-setup/) | Standard (BYO resources) | Hybrid (selective private/public) | System Assigned MI | Hybrid networking with selective private endpoints | +| [**19**](../19-private-network-agent-setup-with-tools/) | Standard (BYO resources) | BYO VNet + Private Endpoints | System Assigned MI | E2E network isolation with tools behind VNet (Functions, MCP, OpenAPI, A2A) | | [**10**](../10-private-network-basic/) | Basic (platform-managed) | BYO VNet + Private Endpoints | System Assigned MI | Basic Foundry with private networking — no agent BYO resources | | [**41**](../41-standard-agent-setup/) | Standard (BYO resources) | **Public** (no VNet) | System Assigned MI | Standard agents without network isolation | @@ -111,7 +111,7 @@ Use the table below to choose the right Terraform infrastructure template for yo 1. The delegated agent subnet must be exclusively used by a single Foundry account. It cannot be shared across accounts. 2. The Foundry resource and the virtual network must be in the same Azure region. BYO resources (Storage, Cosmos DB, AI Search) may be in different regions. 3. Private Class A IP address ranges (10.x.x.x) are only supported in the following regions: **Australia East, Brazil South, Canada East, East US, East US 2, France Central, Germany West Central, Italy North, Japan East, South Africa North, South Central US, South India, Spain Central, Sweden Central, UAE North, UK South, West US, West US 3.** Use Class B (172.16.x.x) or C (192.168.x.x) ranges for other regions. -4. This template does **not** support tools (MCP servers, OpenAPI tools, Azure Functions, A2A) behind the VNet. Use [template 19](../19-hybrid-private-resources-agent-setup/) for that scenario. +4. This template does **not** support tools (MCP servers, OpenAPI tools, Azure Functions, A2A) behind the VNet. Use [template 19](../19-private-network-agent-setup-with-tools/) for that scenario. 5. There is no upgrade path from BYO VNet (this template) to Managed Virtual Network. A Foundry resource redeployment is required. 6. All projects within the same Foundry account share model deployments. Per-project model isolation is not supported. 7. Cosmos DB is deployed as single-region. Multi-region replication must be configured manually post-deployment. @@ -371,6 +371,18 @@ By bundling these BYO features (file storage, search, and thread storage), the s - Disabled local auth - Single region deployment +**Azure Monitor (Application Insights & Log Analytics)** +- Log Analytics Workspace: Microsoft.OperationalInsights/workspaces + - SKU: PerGB2018 + - Retention: 30 days +- Application Insights: Microsoft.Insights/components + - Kind: web + - Linked to Log Analytics workspace + - Public ingestion disabled (reached privately via AMPLS) +- Azure Monitor Private Link Scope (AMPLS): Microsoft.Insights/privateLinkScopes + - Access mode: PrivateOnly ingestion, Open query + - Scoped resources: Application Insights + Log Analytics + ### Network Security Design This implementation utilizes a BYO VNet (Bring Your Own Virtual Network) approach with subnet delegation. The pre-existing virtual network must include subnets for agent delegation and private endpoints. @@ -382,6 +394,7 @@ Private endpoints ensure secure, internal-only connectivity. Private endpoints a - Azure AI Search - Azure Storage - Azure Cosmos DB +- Azure Monitor Private Link Scope (AMPLS) **Private DNS Zones** @@ -391,6 +404,7 @@ Private endpoints ensure secure, internal-only connectivity. Private endpoints a | **Azure AI Search** | searchService| `privatelink.search.windows.net` | `search.windows.net` | | **Azure Cosmos DB** | Sql | `privatelink.documents.azure.com` | `documents.azure.com` | | **Azure Storage** | blob | `privatelink.blob.core.windows.net` | `blob.core.windows.net` | +| **Azure Monitor (AMPLS)** | azuremonitor | `privatelink.monitor.azure.com`
`privatelink.oms.opinsights.azure.com`
`privatelink.ods.opinsights.azure.com`
`privatelink.agentsvc.azure-automation.net` | `monitor.azure.com`
`oms.opinsights.azure.com`
`ods.opinsights.azure.com`
`agentsvc.azure-automation.net` | --- @@ -440,6 +454,7 @@ code/ ├── data.tf # Creates data objects for active subscription being deployed to and deployment security context ├── locals.tf # Creates local variables for project GUID ├── main.tf # Main deployment file +├── monitor.tf # Azure Monitor (Application Insights + AMPLS) for agent tracing ├── outputs.tf # Placeholder file for future outputs ├── providers.tf # Terraform provider configuration ├── example.tfvars # Sample tfvars file diff --git a/infrastructure/infrastructure-setup-terraform/15b-private-network-standard-agent-setup-byovnet/code/monitor.tf b/infrastructure/infrastructure-setup-terraform/15b-private-network-standard-agent-setup-byovnet/code/monitor.tf new file mode 100644 index 000000000..ce0e09b5d --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/15b-private-network-standard-agent-setup-byovnet/code/monitor.tf @@ -0,0 +1,134 @@ +########## Create Azure Monitor resources for agent tracing +########## +## Deploys Application Insights and an Azure Monitor Private Link Scope (AMPLS) so the +## hosted agent in the VNet can export OpenTelemetry traces to Application Insights over +## the private network instead of having the spans dropped. As with the other resources in +## this Bring-Your-Own-VNet template, the Azure Monitor Private DNS Zones are expected to +## already exist in the infrastructure subscription and are referenced by resource id. + +## Create the Log Analytics workspace that backs Application Insights +## +resource "azurerm_log_analytics_workspace" "loganalytics" { + provider = azurerm.workload_subscription + + name = "loganalytics-tracing-${random_string.unique.result}" + location = var.location + resource_group_name = var.resource_group_name_resources + sku = "PerGB2018" + retention_in_days = 30 +} + +## Create the workspace-based Application Insights. Public ingestion is disabled; traces +## are ingested privately through the Azure Monitor Private Link Scope below. +## +resource "azurerm_application_insights" "app_insights" { + provider = azurerm.workload_subscription + + name = "appinsights-tracing-${random_string.unique.result}" + location = var.location + resource_group_name = var.resource_group_name_resources + workspace_id = azurerm_log_analytics_workspace.loganalytics.id + application_type = "web" + internet_ingestion_enabled = false + internet_query_enabled = true +} + +## Create the Azure Monitor Private Link Scope (private ingestion, open query) +## +resource "azurerm_monitor_private_link_scope" "ampls" { + provider = azurerm.workload_subscription + + name = "ampls-tracing-${random_string.unique.result}" + resource_group_name = var.resource_group_name_resources + ingestion_access_mode = "PrivateOnly" + query_access_mode = "Open" +} + +## Scope Application Insights and the Log Analytics workspace into the AMPLS +## +resource "azurerm_monitor_private_link_scoped_service" "ampls_app_insights" { + provider = azurerm.workload_subscription + + name = "appinsights-scoped" + resource_group_name = var.resource_group_name_resources + scope_name = azurerm_monitor_private_link_scope.ampls.name + linked_resource_id = azurerm_application_insights.app_insights.id +} + +resource "azurerm_monitor_private_link_scoped_service" "ampls_loganalytics" { + provider = azurerm.workload_subscription + + name = "loganalytics-scoped" + resource_group_name = var.resource_group_name_resources + scope_name = azurerm_monitor_private_link_scope.ampls.name + linked_resource_id = azurerm_log_analytics_workspace.loganalytics.id +} + +## Create the Private Endpoint for the AMPLS (group "azuremonitor"). The Azure Monitor +## Private DNS Zones are expected to already exist in the infrastructure subscription. +## +resource "azurerm_private_endpoint" "pe_ampls" { + provider = azurerm.workload_subscription + + depends_on = [ + azurerm_monitor_private_link_scoped_service.ampls_app_insights, + azurerm_monitor_private_link_scoped_service.ampls_loganalytics + ] + + name = "ampls-tracing-${random_string.unique.result}-private-endpoint" + location = var.location + resource_group_name = var.resource_group_name_resources + subnet_id = var.subnet_id_private_endpoint + + private_service_connection { + name = "ampls-tracing-private-link-service-connection" + private_connection_resource_id = azurerm_monitor_private_link_scope.ampls.id + subresource_names = [ + "azuremonitor" + ] + is_manual_connection = false + } + + private_dns_zone_group { + name = "ampls-tracing-dns-config" + private_dns_zone_ids = [ + "/subscriptions/${var.subscription_id_infra}/resourceGroups/${var.resource_group_name_dns}/providers/Microsoft.Network/privateDnsZones/privatelink.monitor.azure.com", + "/subscriptions/${var.subscription_id_infra}/resourceGroups/${var.resource_group_name_dns}/providers/Microsoft.Network/privateDnsZones/privatelink.oms.opinsights.azure.com", + "/subscriptions/${var.subscription_id_infra}/resourceGroups/${var.resource_group_name_dns}/providers/Microsoft.Network/privateDnsZones/privatelink.ods.opinsights.azure.com", + "/subscriptions/${var.subscription_id_infra}/resourceGroups/${var.resource_group_name_dns}/providers/Microsoft.Network/privateDnsZones/privatelink.agentsvc.azure-automation.net" + ] + } +} + +## Create the AI Foundry project connection to Application Insights so the agent exports +## its OpenTelemetry traces here +## +resource "azapi_resource" "conn_app_insights" { + provider = azapi.workload_subscription + + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-06-01" + name = azurerm_application_insights.app_insights.name + parent_id = azapi_resource.ai_foundry_project.id + schema_validation_enabled = false + + depends_on = [ + azapi_resource.ai_foundry_project + ] + + body = { + name = azurerm_application_insights.app_insights.name + properties = { + category = "AppInsights" + target = azurerm_application_insights.app_insights.id + authType = "ApiKey" + credentials = { + key = azurerm_application_insights.app_insights.connection_string + } + metadata = { + ApiType = "Azure" + ResourceId = azurerm_application_insights.app_insights.id + location = var.location + } + } + } +} diff --git a/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/README.md b/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/README.md deleted file mode 100644 index f2bea41d3..000000000 --- a/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/README.md +++ /dev/null @@ -1,55 +0,0 @@ -# Hybrid Private Resources Agent Setup - -This folder provides a Terraform implementation for a hybrid setup with a mix of private and public resources. - -## Overview - -This advanced scenario demonstrates: -- Microsoft Foundry with selective private/public access -- Some resources with private endpoints, others public -- Agent setup with hybrid networking -- Complex routing and DNS configuration - -## Status - -**🚧 PARTIALLY IMPLEMENTED** - This complex scenario requires: - -1. Hybrid networking design -2. Selective private endpoint deployment -3. DNS configuration for mixed access -4. Network security group rules -5. Firewall and routing configuration - -## Use Cases - -- Migration from public to private (gradual transition) -- Development environment (public) vs Production (private) -- Selective resource isolation -- Cost optimization (private only where needed) - -## Reference - -For implementation guidance, see the Bicep reference: -- `infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup` - -## Prerequisites - -- Advanced Azure networking knowledge -- Understanding of hybrid networking patterns -- Network security expertise - -## Contributing - -This is a complex scenario. When implementing: -1. Document the reasoning for public vs private choices -2. Include network diagrams -3. Explain security implications -4. Provide migration guidance - -## Documentation - -- [Configure private link for Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-link) -- [azurerm_private_endpoint - Terraform](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/private_endpoint) -- [AzAPI Provider](https://registry.terraform.io/providers/azure/azapi/latest/docs) - -`Tags: Hybrid Networking, Private Endpoints, Advanced` diff --git a/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/example.tfvars b/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/example.tfvars deleted file mode 100644 index 5798fd48a..000000000 --- a/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/example.tfvars +++ /dev/null @@ -1,30 +0,0 @@ -# Example configuration for hybrid private resources agent setup - -# Azure region -location = "eastus2" - -# AI Foundry configuration -ai_services_name_prefix = "foundry" -project_name = "hybrid-agent-project" - -# Network configuration -vnet_address_space = ["10.0.0.0/16"] -subnet_address_prefix = "10.0.1.0/24" - -# Hybrid configuration - mix of public and private -# Scenario: Development setup with some public access for easier testing -ai_foundry_public_access = "Enabled" # Public for easy access -storage_public_access = false # Private for data security -search_public_access = true # Public for development -cosmos_public_access = true # Public for development - -# For production, you might flip these: -# ai_foundry_public_access = "Disabled" -# storage_public_access = false -# search_public_access = false -# cosmos_public_access = false - -# Model configuration -model_name = "gpt-4.1" -model_version = "2025-04-14" -model_capacity = 40 diff --git a/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/main.tf b/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/main.tf deleted file mode 100644 index 85bf290ee..000000000 --- a/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/main.tf +++ /dev/null @@ -1,387 +0,0 @@ -########## Create hybrid infrastructure (mix of private and public resources) -########## - -## Get subscription data -data "azurerm_client_config" "current" {} - -## Create a random string for unique naming -resource "random_string" "unique" { - length = 4 - min_numeric = 4 - numeric = true - special = false - lower = true - upper = false -} - -locals { - account_name = lower("${var.ai_services_name_prefix}${random_string.unique.result}") -} - -## Create a resource group -resource "azurerm_resource_group" "rg" { - name = "rg-aifoundry${random_string.unique.result}" - location = var.location -} - -## Create Virtual Network (always needed for private endpoints) -resource "azurerm_virtual_network" "vnet" { - name = "vnet-aifoundry${random_string.unique.result}" - address_space = var.vnet_address_space - location = var.location - resource_group_name = azurerm_resource_group.rg.name -} - -## Create Subnet for private endpoints -resource "azurerm_subnet" "subnet" { - name = "subnet-private-endpoints" - resource_group_name = azurerm_resource_group.rg.name - virtual_network_name = azurerm_virtual_network.vnet.name - address_prefixes = [var.subnet_address_prefix] -} - -## Create Private DNS Zones (for private resources) -resource "azurerm_private_dns_zone" "ai_foundry" { - count = var.ai_foundry_public_access == "Disabled" ? 1 : 0 - name = "privatelink.cognitiveservices.azure.com" - resource_group_name = azurerm_resource_group.rg.name -} - -resource "azurerm_private_dns_zone" "storage" { - count = var.storage_public_access ? 0 : 1 - name = "privatelink.blob.core.windows.net" - resource_group_name = azurerm_resource_group.rg.name -} - -resource "azurerm_private_dns_zone" "search" { - count = var.search_public_access ? 0 : 1 - name = "privatelink.search.windows.net" - resource_group_name = azurerm_resource_group.rg.name -} - -resource "azurerm_private_dns_zone" "cosmos" { - count = var.cosmos_public_access ? 0 : 1 - name = "privatelink.documents.azure.com" - resource_group_name = azurerm_resource_group.rg.name -} - -## Link DNS zones to VNet -resource "azurerm_private_dns_zone_virtual_network_link" "ai_foundry" { - count = var.ai_foundry_public_access == "Disabled" ? 1 : 0 - name = "vnet-link-ai-foundry" - resource_group_name = azurerm_resource_group.rg.name - private_dns_zone_name = azurerm_private_dns_zone.ai_foundry[0].name - virtual_network_id = azurerm_virtual_network.vnet.id -} - -resource "azurerm_private_dns_zone_virtual_network_link" "storage" { - count = var.storage_public_access ? 0 : 1 - name = "vnet-link-storage" - resource_group_name = azurerm_resource_group.rg.name - private_dns_zone_name = azurerm_private_dns_zone.storage[0].name - virtual_network_id = azurerm_virtual_network.vnet.id -} - -resource "azurerm_private_dns_zone_virtual_network_link" "search" { - count = var.search_public_access ? 0 : 1 - name = "vnet-link-search" - resource_group_name = azurerm_resource_group.rg.name - private_dns_zone_name = azurerm_private_dns_zone.search[0].name - virtual_network_id = azurerm_virtual_network.vnet.id -} - -resource "azurerm_private_dns_zone_virtual_network_link" "cosmos" { - count = var.cosmos_public_access ? 0 : 1 - name = "vnet-link-cosmos" - resource_group_name = azurerm_resource_group.rg.name - private_dns_zone_name = azurerm_private_dns_zone.cosmos[0].name - virtual_network_id = azurerm_virtual_network.vnet.id -} - -## Create Storage Account (public or private based on variable) -resource "azurerm_storage_account" "storage" { - name = "aifoundry${random_string.unique.result}stor" - resource_group_name = azurerm_resource_group.rg.name - location = var.location - account_kind = "StorageV2" - account_tier = "Standard" - account_replication_type = "ZRS" - - shared_access_key_enabled = false - min_tls_version = "TLS1_2" - allow_nested_items_to_be_public = false - public_network_access_enabled = var.storage_public_access - - network_rules { - default_action = var.storage_public_access ? "Allow" : "Deny" - bypass = ["AzureServices"] - } -} - -## Create private endpoint for Storage (if private) -resource "azurerm_private_endpoint" "storage" { - count = var.storage_public_access ? 0 : 1 - name = "pe-storage-${random_string.unique.result}" - location = var.location - resource_group_name = azurerm_resource_group.rg.name - subnet_id = azurerm_subnet.subnet.id - - private_service_connection { - name = "psc-storage" - private_connection_resource_id = azurerm_storage_account.storage.id - is_manual_connection = false - subresource_names = ["blob"] - } - - private_dns_zone_group { - name = "storage-dns-zone-group" - private_dns_zone_ids = [azurerm_private_dns_zone.storage[0].id] - } -} - -## Create AI Search (public or private based on variable) -resource "azurerm_search_service" "search" { - name = replace("aifoundry-${random_string.unique.result}-search", "_", "-") - resource_group_name = azurerm_resource_group.rg.name - location = var.location - sku = "standard" - - local_authentication_enabled = true - authentication_failure_mode = "http401WithBearerChallenge" - public_network_access_enabled = var.search_public_access -} - -## Create private endpoint for Search (if private) -resource "azurerm_private_endpoint" "search" { - count = var.search_public_access ? 0 : 1 - name = "pe-search-${random_string.unique.result}" - location = var.location - resource_group_name = azurerm_resource_group.rg.name - subnet_id = azurerm_subnet.subnet.id - - private_service_connection { - name = "psc-search" - private_connection_resource_id = azurerm_search_service.search.id - is_manual_connection = false - subresource_names = ["searchService"] - } - - private_dns_zone_group { - name = "search-dns-zone-group" - private_dns_zone_ids = [azurerm_private_dns_zone.search[0].id] - } -} - -## Create Cosmos DB (public or private based on variable) -resource "azurerm_cosmosdb_account" "cosmos" { - name = "aifoundry${random_string.unique.result}cosmos" - location = var.location - resource_group_name = azurerm_resource_group.rg.name - offer_type = "Standard" - kind = "GlobalDocumentDB" - public_network_access_enabled = var.cosmos_public_access - is_virtual_network_filter_enabled = !var.cosmos_public_access - - consistency_policy { - consistency_level = "Session" - } - - geo_location { - location = var.location - failover_priority = 0 - } -} - -## Create private endpoint for Cosmos DB (if private) -resource "azurerm_private_endpoint" "cosmos" { - count = var.cosmos_public_access ? 0 : 1 - name = "pe-cosmos-${random_string.unique.result}" - location = var.location - resource_group_name = azurerm_resource_group.rg.name - subnet_id = azurerm_subnet.subnet.id - - private_service_connection { - name = "psc-cosmos" - private_connection_resource_id = azurerm_cosmosdb_account.cosmos.id - is_manual_connection = false - subresource_names = ["Sql"] - } - - private_dns_zone_group { - name = "cosmos-dns-zone-group" - private_dns_zone_ids = [azurerm_private_dns_zone.cosmos[0].id] - } -} - -## Create AI Foundry account (public or private based on variable) -resource "azapi_resource" "ai_foundry" { - type = "Microsoft.CognitiveServices/accounts@2025-04-01-preview" - name = local.account_name - location = var.location - parent_id = azurerm_resource_group.rg.id - - identity { - type = "SystemAssigned" - } - - body = { - kind = "AIServices" - sku = { - name = "S0" - } - properties = { - allowProjectManagement = true - customSubDomainName = local.account_name - publicNetworkAccess = var.ai_foundry_public_access - disableLocalAuth = true - networkAcls = { - defaultAction = var.ai_foundry_public_access == "Enabled" ? "Allow" : "Deny" - virtualNetworkRules = [] - ipRules = [] - } - } - } -} - -## Create private endpoint for AI Foundry (if private) -resource "azurerm_private_endpoint" "ai_foundry" { - count = var.ai_foundry_public_access == "Disabled" ? 1 : 0 - name = "pe-ai-foundry-${random_string.unique.result}" - location = var.location - resource_group_name = azurerm_resource_group.rg.name - subnet_id = azurerm_subnet.subnet.id - - private_service_connection { - name = "psc-ai-foundry" - private_connection_resource_id = azapi_resource.ai_foundry.id - is_manual_connection = false - subresource_names = ["account"] - } - - private_dns_zone_group { - name = "ai-foundry-dns-zone-group" - private_dns_zone_ids = [azurerm_private_dns_zone.ai_foundry[0].id] - } -} - -## Grant AI Foundry access to Storage -resource "azurerm_role_assignment" "storage_blob_data_contributor" { - scope = azurerm_storage_account.storage.id - role_definition_name = "Storage Blob Data Contributor" - principal_id = azapi_resource.ai_foundry.identity[0].principal_id -} - -## Grant AI Foundry access to AI Search -resource "azurerm_role_assignment" "search_index_data_contributor" { - scope = azurerm_search_service.search.id - role_definition_name = "Search Index Data Contributor" - principal_id = azapi_resource.ai_foundry.identity[0].principal_id -} - -resource "azurerm_role_assignment" "search_service_contributor" { - scope = azurerm_search_service.search.id - role_definition_name = "Search Service Contributor" - principal_id = azapi_resource.ai_foundry.identity[0].principal_id -} - -## Grant AI Foundry access to Cosmos DB -resource "azurerm_cosmosdb_sql_role_assignment" "cosmos_contributor" { - resource_group_name = azurerm_resource_group.rg.name - account_name = azurerm_cosmosdb_account.cosmos.name - role_definition_id = "${azurerm_cosmosdb_account.cosmos.id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002" - principal_id = azapi_resource.ai_foundry.identity[0].principal_id - scope = azurerm_cosmosdb_account.cosmos.id -} - -## Wait for role assignments and private endpoints -resource "time_sleep" "wait_for_resources" { - depends_on = [ - azurerm_role_assignment.storage_blob_data_contributor, - azurerm_role_assignment.search_index_data_contributor, - azurerm_role_assignment.search_service_contributor, - azurerm_cosmosdb_sql_role_assignment.cosmos_contributor - ] - create_duration = "60s" -} - -## Create AI Foundry project -resource "azapi_resource" "ai_project" { - type = "Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview" - name = var.project_name - location = var.location - parent_id = azapi_resource.ai_foundry.id - - identity { - type = "SystemAssigned" - } - - body = { - properties = {} - } - - depends_on = [time_sleep.wait_for_resources] -} - -## Create connections -resource "azapi_resource" "storage_connection" { - type = "Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview" - name = "storage-connection" - parent_id = azapi_resource.ai_foundry.id - - body = { - properties = { - category = "AzureBlob" - target = azurerm_storage_account.storage.primary_blob_endpoint - authType = "AccessKey" - isSharedToAll = true - metadata = { - ResourceId = azurerm_storage_account.storage.id - } - } - } - - depends_on = [azapi_resource.ai_project] -} - -resource "azapi_resource" "search_connection" { - type = "Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview" - name = "search-connection" - parent_id = azapi_resource.ai_foundry.id - - body = { - properties = { - category = "CognitiveSearch" - target = "https://${azurerm_search_service.search.name}.search.windows.net" - authType = "AAD" - isSharedToAll = true - metadata = { - ResourceId = azurerm_search_service.search.id - } - } - } - - depends_on = [azapi_resource.ai_project] -} - -## Deploy model -resource "azapi_resource" "model_deployment" { - type = "Microsoft.CognitiveServices/accounts/deployments@2025-04-01-preview" - name = var.model_name - parent_id = azapi_resource.ai_foundry.id - - body = { - sku = { - capacity = var.model_capacity - name = "GlobalStandard" - } - properties = { - model = { - name = var.model_name - format = "OpenAI" - version = var.model_version - } - } - } - - depends_on = [azapi_resource.ai_project] -} diff --git a/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/outputs.tf b/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/outputs.tf deleted file mode 100644 index 180abffc0..000000000 --- a/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/outputs.tf +++ /dev/null @@ -1,44 +0,0 @@ -output "resource_group_name" { - description = "The name of the resource group" - value = azurerm_resource_group.rg.name -} - -output "vnet_id" { - description = "The ID of the virtual network" - value = azurerm_virtual_network.vnet.id -} - -output "ai_foundry_id" { - description = "The ID of the AI Foundry account" - value = azapi_resource.ai_foundry.id -} - -output "ai_project_id" { - description = "The ID of the AI Foundry project" - value = azapi_resource.ai_project.id -} - -output "storage_account_id" { - description = "The ID of the storage account" - value = azurerm_storage_account.storage.id -} - -output "search_service_id" { - description = "The ID of the AI Search service" - value = azurerm_search_service.search.id -} - -output "cosmos_db_id" { - description = "The ID of the Cosmos DB account" - value = azurerm_cosmosdb_account.cosmos.id -} - -output "deployment_summary" { - description = "Summary of public vs private resources" - value = { - ai_foundry_access = var.ai_foundry_public_access - storage_access = var.storage_public_access ? "Public" : "Private" - search_access = var.search_public_access ? "Public" : "Private" - cosmos_access = var.cosmos_public_access ? "Public" : "Private" - } -} diff --git a/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/variables.tf b/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/variables.tf deleted file mode 100644 index e76b4966f..000000000 --- a/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/variables.tf +++ /dev/null @@ -1,75 +0,0 @@ -variable "location" { - description = "The Azure region where resources will be deployed" - type = string - default = "eastus2" -} - -variable "ai_services_name_prefix" { - description = "Prefix for AI Foundry account name" - type = string - default = "foundry" -} - -variable "project_name" { - description = "The name of the project" - type = string - default = "hybrid-agent-project" -} - -variable "vnet_address_space" { - description = "Address space for the virtual network" - type = list(string) - default = ["10.0.0.0/16"] -} - -variable "subnet_address_prefix" { - description = "Address prefix for the subnet" - type = string - default = "10.0.1.0/24" -} - -variable "ai_foundry_public_access" { - description = "Whether AI Foundry should have public access (Enabled/Disabled)" - type = string - default = "Enabled" - validation { - condition = contains(["Enabled", "Disabled"], var.ai_foundry_public_access) - error_message = "Must be Enabled or Disabled" - } -} - -variable "storage_public_access" { - description = "Whether Storage should have public access (true/false)" - type = bool - default = false -} - -variable "search_public_access" { - description = "Whether AI Search should have public access (true/false)" - type = bool - default = true -} - -variable "cosmos_public_access" { - description = "Whether Cosmos DB should have public access (true/false)" - type = bool - default = true -} - -variable "model_name" { - description = "The model to deploy" - type = string - default = "gpt-4.1" -} - -variable "model_version" { - description = "The version of the model" - type = string - default = "2025-04-14" -} - -variable "model_capacity" { - description = "The capacity of the model deployment" - type = number - default = 40 -} diff --git a/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/README.md b/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/README.md new file mode 100644 index 000000000..e213f0748 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/README.md @@ -0,0 +1,468 @@ +--- +description: This set of templates demonstrates how to set up Azure AI Agent Service with virtual network isolation, private network links, and tools behind VNet. +page_type: sample +products: +- azure +- azure-resource-manager +urlFragment: network-secured-agent-tools-terraform +languages: +- hcl +--- + +# Microsoft Foundry: Standard Agent Setup with E2E Network Isolation (Terraform) + +> **NEW** +> For support on deploying the right network isolation template, check out the [GitHub Copilot for Azure skill for private networking](https://github.com/microsoft/GitHub-Copilot-for-Azure/blob/main/plugin/skills/microsoft-foundry/resource/private-network/private-network.md) set-up! + +--- + +## Overview + +This Terraform implementation deploys a network-secured Microsoft Foundry agent environment with private networking, role-based access control (RBAC), and support for tools behind the VNet (MCP servers, OpenAPI tools, Azure Functions, A2A). + +Standard setup supports private network isolation through utilizing **Bring Your Own Virtual Network (BYO VNet)** approach, also known as **custom VNet support with subnet delegation.** + +This implementation gives you full control over the inbound and outbound communication paths for your agent. You can restrict access to only the resources explicitly required by your agent, such as storage accounts, databases, or APIs, while blocking all other traffic by default. This approach ensures that your agent operates within a tightly scoped network boundary, reducing the risk of data leakage or unauthorized access. + +By default, the Foundry resource has **public network access disabled**. + +This is the Terraform equivalent of [Bicep template 19](../../infrastructure-setup-bicep/19-private-network-agent-tools/). + +### When to Use This Template + +Use this template when you need: +- **Full end-to-end network isolation** — All resources behind private endpoints with no public internet access +- **BYO VNet control** — You manage your own virtual network, subnets, and network security groups +- **Standard agent setup with BYO resources** — Customer-managed Storage, Cosmos DB, and AI Search for data residency and compliance +- **Tools behind VNet** — MCP servers, OpenAPI tools, Azure Functions, or A2A agents deployed on the private VNet (requires separate tool deployment — see [MCP Server Deployment](#mcp-server-deployment)) +- **System Assigned Managed Identity** — Simplified identity management with platform-managed credentials + +### IP Range Support + +> Private Class A subnet support (10.x.x.x) is GA and available in: **Australia East, Brazil South, Canada East, East US, East US 2, France Central, Germany West Central, Italy North, Japan East, South Africa North, South Central US, South India, Spain Central, Sweden Central, UAE North, UK South, West Europe, West US, West US 3.** +> +> Private Class B (172.16.x.x) and C (192.168.x.x) subnet support is GA in all regions supported by Microsoft Foundry Agent Service. + +--- + +## Architecture + +``` +┌─────────────────────────────────────────────────────────────────────┐ +│ Secure Access (VPN Gateway / ExpressRoute / Azure Bastion) │ +└──────────────────────────────────┬──────────────────────────────────┘ + │ + ┌──────────────▼──────────────┐ + │ Microsoft Foundry │ + │ (publicNetworkAccess: │ + │ DISABLED) │ + │ │ + │ ┌────────────────────────┐ │ + │ │ Foundry Project │ │ + │ │ (Agent Workspace) │ │ + │ └───────────┬────────────┘ │ + └──────────────┼──────────────┘ + │ Subnet Delegation + ┌──────────────▼──────────────┐ + │ BYO Virtual Network │ + │ (192.168.0.0/16) │ + │ │ + │ ┌──────────────────────┐ │ + │ │ Agent Subnet │ │ + │ │ (192.168.0.0/24) │ │ ◄── Delegated to + │ │ Microsoft.App/envs │ │ Microsoft.App/environments + │ └──────────────────────┘ │ + │ │ + │ ┌──────────────────────┐ │ + │ │ PE Subnet │ │ + │ │ (192.168.1.0/24) │ │ + │ │ │ │ + │ │ ┌────────┐ ┌────────┐ │ │ + │ │ │Storage │ │Cosmos │ │ │ ◄── Private endpoints + │ │ └────────┘ └────────┘ │ │ (no public access) + │ │ ┌────────┐ ┌────────┐ │ │ + │ │ │Search │ │Foundry │ │ │ + │ │ └────────┘ └────────┘ │ │ + │ └──────────────────────┘ │ + │ │ + │ ┌──────────────────────┐ │ + │ │ MCP Subnet │ │ + │ │ (192.168.2.0/24) │ │ + │ │ │ │ + │ │ ┌────────┐ ┌────────┐ │ │ + │ │ │ MCP │ │OpenAPI │ │ │ ◄── Tools behind VNet + │ │ │Servers │ │ Tools │ │ │ + │ │ └────────┘ └────────┘ │ │ + │ │ ┌────────┐ ┌────────┐ │ │ + │ │ │Azure │ │ A2A │ │ │ + │ │ │Funcs │ │Agents │ │ │ + │ │ └────────┘ └────────┘ │ │ + │ └──────────────────────┘ │ + └──────────────────────────────┘ +``` + +--- + +## Prerequisites + +1. **Active Azure subscription with appropriate permissions** + - **Foundry Account Owner**: Needed to create the Microsoft Foundry account and project + - **Owner or Role Based Access Administrator**: Needed to assign RBAC on the Azure resources used by this template + - **Foundry User**: Needed to create and use agents, projects, or evaluation workloads after deployment + +2. **Register Resource Providers** + + Make sure your subscription allows registering resource providers. Subnet delegation requires `Microsoft.App` to be registered. + + ```bash + az provider register --namespace 'Microsoft.KeyVault' + az provider register --namespace 'Microsoft.CognitiveServices' + az provider register --namespace 'Microsoft.Storage' + az provider register --namespace 'Microsoft.Search' + az provider register --namespace 'Microsoft.Network' + az provider register --namespace 'Microsoft.App' + az provider register --namespace 'Microsoft.ContainerService' + ``` + +3. **Network administrator permissions** (if operating in a restricted or enterprise environment) + +4. **Sufficient quota** for all resources in your target Azure region, including model deployment quota. If no BYO resources are provided, this template creates a Foundry resource, project, Cosmos DB, AI Search, and Storage account. + +5. **Azure CLI** installed and configured + +6. **Terraform CLI** v1.10.0 or later. This template uses the AzureRM and AzAPI providers. + +--- + +## Pre-Deployment Steps + +### Networking Requirements + +1. Review network requirements and plan Virtual Network address space (default: `192.168.0.0/16`). + +2. Three subnets are needed: + + | Subnet | Default CIDR | Purpose | Delegation | + |--------|-------------|---------|------------| + | `agent-subnet` | `192.168.0.0/24` | Agent compute (capability host). Recommended size: /24 | `Microsoft.App/environments` | + | `pe-subnet` | `192.168.1.0/24` | Private endpoints for Storage, Cosmos DB, AI Search, AI Foundry | None | + | `mcp-subnet` | `192.168.2.0/24` | MCP servers, OpenAPI tools, Azure Functions, A2A agents | `Microsoft.App/environments` | + +3. Ensure the VNet address space does not overlap with: + - Existing networks in your Azure environment + - Reserved IP ranges: `169.254.0.0/16`, `172.30.0.0/16`, `172.31.0.0/16`, `192.0.2.0/24`, `0.0.0.0/8`, `127.0.0.0/8`, `100.100.0.0/17`, `100.100.192.0/19`, `100.100.224.0/19`, `100.64.0.0/11` + - Peered VNets or on-premises address spaces + +> **Notes:** +> - If you do not provide an existing VNet, the template creates a new one with the default address spaces above. +> - The agent subnet must be exclusively delegated to `Microsoft.App/environments` and cannot be used by any other Azure resources. +> - For Class A IP ranges (10.x.x.x), only the [regions listed above](#ip-range-support) are supported. + +--- + +## Variables + +### Required + +| Variable | Description | +|----------|-------------| +| `location` | Azure region for all resources (see [IP Range Support](#ip-range-support)) | + +### Optional — Resource Configuration + +| Variable | Default | Description | +|----------|---------|-------------| +| `ai_services_name_prefix` | `aifoundry` | Prefix for AI Foundry account name (a random suffix is appended) | +| `project_name` | `agent-project` | Name of the Foundry project | +| `vnet_address_space` | `["192.168.0.0/16"]` | VNet address space (only used when creating a new VNet) | +| `model_name` | `gpt-4.1` | Model to deploy | +| `model_version` | `2025-04-14` | Model version | +| `model_capacity` | `40` | Model deployment capacity (TPM) | +| `enable_container_registry` | `false` | Enable Azure Container Registry with Private Endpoint for hosted agent containers | +| `developer_ip_cidr` | `""` | Developer IP CIDR to allowlist for ACR push access (only used when ACR is enabled) | + +### Optional — BYO (Bring Your Own) Resources + +Leave empty to create new resources. Provide resource IDs to reuse existing ones. + +| Variable | Default | Description | +|----------|---------|-------------| +| `existing_resource_group_name` | `""` | Name of an existing resource group | +| `existing_vnet_id` | `""` | Resource ID of an existing VNet (requires all 3 subnet IDs) | +| `existing_agent_subnet_id` | `""` | Resource ID of an existing agent subnet (delegated to `Microsoft.App/environments`) | +| `existing_pe_subnet_id` | `""` | Resource ID of an existing private endpoint subnet | +| `existing_mcp_subnet_id` | `""` | Resource ID of an existing MCP subnet (delegated to `Microsoft.App/environments`) | +| `existing_storage_account_id` | `""` | Resource ID of an existing Storage Account | +| `existing_cosmosdb_account_id` | `""` | Resource ID of an existing Cosmos DB account | +| `existing_ai_search_id` | `""` | Resource ID of an existing AI Search service | +| `existing_dns_zones_resource_group` | `""` | Resource group containing existing private DNS zones (all 6 zones expected) | +| `existing_dns_zones_subscription_id` | `""` | Subscription ID for DNS zones (only used with `existing_dns_zones_resource_group`) | +| `existing_fabric_workspace_id` | `""` | Resource ID of an existing Fabric workspace for Data Agent PE | + +See `code/example.tfvars` for a complete configuration reference with all variables. + +--- + +## Deploy + +### 1. Set subscription + +```bash +# Linux/macOS +export ARM_SUBSCRIPTION_ID="YOUR_SUBSCRIPTION_ID" + +# Windows PowerShell +$env:ARM_SUBSCRIPTION_ID = "YOUR_SUBSCRIPTION_ID" + +# Windows cmd +set ARM_SUBSCRIPTION_ID=YOUR_SUBSCRIPTION_ID +``` + +### 2. Log in to Azure + +```bash +az login +``` + +### 3. Initialize Terraform + +```bash +cd infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/code +terraform init +``` + +### 4. Configure variables + +Copy the example variables file and set your target region: + +```bash +cp example.tfvars terraform.tfvars +``` + +Edit `terraform.tfvars` — at minimum, set `location` to your target region. For BYO resources, fill in the resource IDs for the existing resources you want to reuse. + +### 5. Plan and apply + +```bash +terraform plan -var-file="terraform.tfvars" -out=tfplan +terraform apply "tfplan" +``` + +Deployment takes approximately **15–25 minutes**. The main time-consuming resources are the capability host provisioning and private endpoint creation. + +### Verify Deployment + +```bash +# Check Terraform outputs +terraform output + +# List private endpoints (should see Storage, Cosmos DB, AI Search, AI Foundry) +az network private-endpoint list \ + --resource-group $(terraform output -raw resource_group_name) \ + --output table +``` + +After connecting to the VNet (via VPN or other method), verify DNS resolution: + +```bash +nslookup .services.ai.azure.com +nslookup .documents.azure.com +nslookup .blob.core.windows.net +``` + +Each should resolve to a private IP in your VNet range (e.g., `192.168.1.x`). + +--- + +## What Gets Deployed + +The template creates the following resources: + +| Resource | Type | Key Configuration | +|----------|------|-------------------| +| Resource Group | `azurerm_resource_group` | `rg-aifoundry` | +| Virtual Network | `azurerm_virtual_network` | 3 subnets (agent, PE, MCP), serialized to avoid locking | +| Storage Account | `azurerm_storage_account` | StorageV2, ZRS, public access disabled, SharedKey disabled | +| Cosmos DB | `azapi_resource` | NoSQL, serverless, public access disabled, local auth disabled | +| AI Search | `azapi_resource` | Standard SKU, public access disabled, AAD auth | +| AI Foundry Account | `azapi_resource` | `networkInjections` for agent + MCP subnets, public access disabled | +| Model Deployment | `azapi_resource` | Configurable model (default: gpt-4.1) | +| Private DNS Zones (6) | `azurerm_private_dns_zone` | Linked to VNet | +| Private Endpoints (4) | `azurerm_private_endpoint` | Storage, Cosmos DB, AI Search, AI Foundry (serialized) | +| Foundry Project | `azapi_resource` | System-assigned managed identity | +| Connections (3) | `azapi_resource` | Cosmos DB, Storage, AI Search (AAD auth) | +| RBAC Assignments (6) | `azapi_resource` | Phase A (before caphost) + Phase B (after caphost) | +| Capability Hosts (2) | `azapi_resource` | Account + Project | +| ACR (optional) | `azurerm_container_registry` | Premium SKU, PE + DNS zone, AcrPull role on project MI | +| Account Purger | `terraform_data` | Purges soft-deleted account on destroy | + +When using BYO resources, any resource with a matching `existing_*` variable is skipped (not created). Data sources reference the existing resource instead. All downstream references (private endpoints, connections, RBAC) work identically. + +### Authentication & Authorization + +This template uses **System Assigned Managed Identity** with the following role assignments on the **Project Managed Identity**: + +| Role | Resource | Phase | +|------|----------|-------| +| Search Index Data Contributor | AI Search | A (before caphost) | +| Search Service Contributor | AI Search | A | +| Storage Blob Data Contributor | Storage Account | A | +| Cosmos DB Operator | Cosmos DB Account | A | +| Cosmos DB SQL Built-in Data Contributor | Cosmos DB Account | B (after caphost) | +| Storage Blob Data Owner (with ABAC condition) | Storage Account | B | + +### BYO Resources + +All agents created using this service are **stateful** — they retain information across interactions. With the standard setup, agent state is stored in customer-managed, single-tenant resources: + +- **BYO File Storage**: Files uploaded by developers or end-users are stored in the customer's Azure Storage account +- **BYO Search**: Vector stores created by the agent use the customer's Azure AI Search resource +- **BYO Thread Storage**: Messages and conversation history are stored in the customer's Azure Cosmos DB account + +--- + +## BYO (Bring Your Own) Deployment Scenarios + +This template supports mixing new and existing resources. Common scenarios: + +### Scenario 1: Create Everything (default) + +Set only `location`. All resources are created fresh: + +```hcl +location = "swedencentral" +``` + +### Scenario 2: BYO Resource Group + +Deploy into an existing resource group: + +```hcl +location = "swedencentral" +existing_resource_group_name = "my-existing-rg" +``` + +### Scenario 3: BYO Networking + Backend Services + +Reuse an existing VNet and backend resources (e.g., from a previous deployment): + +```hcl +location = "swedencentral" +existing_resource_group_name = "my-existing-rg" +existing_vnet_id = "/subscriptions/.../virtualNetworks/my-vnet" +existing_agent_subnet_id = "/subscriptions/.../subnets/agent-subnet" +existing_pe_subnet_id = "/subscriptions/.../subnets/pe-subnet" +existing_mcp_subnet_id = "/subscriptions/.../subnets/mcp-subnet" +existing_storage_account_id = "/subscriptions/.../storageAccounts/mystorage" +existing_cosmosdb_account_id = "/subscriptions/.../databaseAccounts/mycosmosdb" +existing_ai_search_id = "/subscriptions/.../searchServices/mysearch" +existing_dns_zones_resource_group = "my-dns-rg" +``` + +### BYO Requirements + +- **VNet**: All 3 subnet IDs must be provided together. Agent and MCP subnets must be delegated to `Microsoft.App/environments`. +- **Agent subnet**: Must not have an existing Service Association Link (SAL) from another Foundry account. One account per agent subnet. +- **Backend services**: Must have public access disabled. +- **DNS zones**: All 6 private DNS zones must exist in the specified resource group and be linked to the VNet. +- **DNS zone cross-subscription**: Set `existing_dns_zones_subscription_id` if the DNS zones are in a different subscription. The deployment identity needs `Private DNS Zone Contributor` on each referenced zone. + +> **Cosmos DB Connection Note**: The Cosmos DB connection uses `authType: AAD` and includes the `ResourceId` in metadata. This is the only supported authentication type for the Cosmos DB connection used by the Agent Service. + +--- + +## Connecting to a Private Foundry Resource + +When public network access is disabled (the default), you need a secure connection to reach the Foundry resource. Azure provides three methods: + +1. **Azure VPN Gateway** — Connect from your local network to the Azure VNet over an encrypted tunnel +2. **Azure ExpressRoute** — Use a private, dedicated connection from your on-premises infrastructure to Azure +3. **Azure Bastion** — Use a jump box VM on the VNet, accessed securely through the Azure portal + +For detailed setup instructions, see: [Securely connect to Azure AI Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-link?view=foundry#securely-connect-to-foundry). + +## MCP Server Deployment + +To deploy MCP servers on the private VNet after the base infrastructure is deployed: + +```bash +# Create Container Apps environment on mcp-subnet +az containerapp env create \ + --resource-group \ + --name "mcp-env" \ + --location \ + --infrastructure-subnet-resource-id \ + --internal-only true + +# Deploy MCP server +az containerapp create \ + --resource-group \ + --name "my-mcp-server" \ + --environment "mcp-env" \ + --image \ + --target-port 8080 \ + --ingress external \ + --min-replicas 1 +``` + +Then configure a private DNS zone for Container Apps. See the [Bicep 19 TESTING-GUIDE.md](../../infrastructure-setup-bicep/19-private-network-agent-tools/tests/TESTING-GUIDE.md) for details on DNS configuration for tools behind VNet. + +--- + +## Teardown + +### Account Deletion Prerequisites + +Before deleting an Account resource, the associated capability hosts must be removed first. The `terraform destroy` command handles this automatically via dependency ordering and the account purger resource. + +If you need to manually clean up: +1. Delete the **project capability host** first +2. Delete the **account capability host** +3. Delete and [**purge**](https://learn.microsoft.com/en-us/azure/ai-services/recover-purge-resources?tabs=azure-portal#purge-a-deleted-resource) the Foundry account +4. Allow approximately **20 minutes** for all resources to be fully unlinked + +> **Important**: Simply deleting the account is not sufficient — you must also purge it so that the associated capability host deletion is triggered. The service will automatically handle the removal of the capability host and any linked resources in the background. + +--- + +## Limitations / Known Issues + +1. The delegated agent subnet must be exclusively used by a single Foundry account. It cannot be shared across accounts. A **Service Association Link (SAL)** is placed on the subnet during account creation. The SAL has `allowDelete: false` and may take up to 30 minutes to be released after the account is deleted and purged. +2. The Foundry resource and the VNet must be in the same Azure region. BYO backend resources (Storage, Cosmos DB, AI Search) may be in different regions. +3. For the VNet IP range, you may use any Private Class A, B, or C range. Class A (10.x.x.x) is only supported in [specific regions](#ip-range-support). Do not use ranges that overlap with the reserved ranges listed in [Networking Requirements](#networking-requirements). +4. All projects within the same Foundry account share model deployments. Per-project model isolation is not supported. +5. Cosmos DB is deployed as single-region. Multi-region replication must be configured manually post-deployment. +6. When using BYO resources, private endpoints are still created by this template. A random suffix is added to avoid naming collisions with existing private endpoints. + +--- + +## File Structure + +``` +19-private-network-agent-setup-with-tools/ +├── README.md # This file +└── code/ + ├── main.tf # All resources (networking, compute, RBAC, connections) + ├── variables.tf # Input variable definitions + ├── locals.tf # Computed locals (BYO flags, unified references) + ├── data.tf # Data sources (client config, existing resources) + ├── outputs.tf # Output values + ├── providers.tf # Provider configuration + ├── versions.tf # Provider version constraints + └── example.tfvars # Example variable values (copy to terraform.tfvars) +``` + +--- + +## Reference + +- [Bicep template 19](../../infrastructure-setup-bicep/19-private-network-agent-tools/) — The Bicep equivalent this Terraform template is based on +- [Terraform template 15a](../15a-private-network-standard-agent-setup/) — Standard agent private network setup (no MCP subnet) +- [Microsoft Foundry Agent Service networking docs](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/virtual-networks) +- [Models supported by Microsoft Foundry Agent Service](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/concepts/model-region-support) +- [Configure private link for Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-link) +- [Purge a deleted Cognitive Services resource](https://learn.microsoft.com/en-us/azure/ai-services/recover-purge-resources?tabs=azure-portal#purge-a-deleted-resource) +- [AzAPI Provider](https://registry.terraform.io/providers/azure/azapi/latest/docs) +- [azurerm_private_endpoint](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/private_endpoint) + +`Tags: Hybrid Networking, Private Endpoints, Advanced` diff --git a/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/code/data.tf b/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/code/data.tf new file mode 100644 index 000000000..8588c231c --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/code/data.tf @@ -0,0 +1,25 @@ +########## Data sources for BYO (Bring Your Own) resources +########## These are only instantiated when using existing resources (count = 0 by default). +########## + +## Existing Storage Account — needed for primary_blob_endpoint +data "azurerm_storage_account" "existing" { + count = local.create_storage ? 0 : 1 + name = split("/", var.existing_storage_account_id)[8] + resource_group_name = split("/", var.existing_storage_account_id)[4] +} + +## Existing Cosmos DB — needed for endpoint +data "azurerm_cosmosdb_account" "existing" { + count = local.create_cosmos ? 0 : 1 + name = split("/", var.existing_cosmosdb_account_id)[8] + resource_group_name = split("/", var.existing_cosmosdb_account_id)[4] +} + +## Existing AI Search — needed for location metadata in connections +data "azapi_resource" "existing_search" { + count = local.create_search ? 0 : 1 + type = "Microsoft.Search/searchServices@2024-06-01-preview" + resource_id = var.existing_ai_search_id + response_export_values = ["location"] +} diff --git a/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/code/example.tfvars b/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/code/example.tfvars new file mode 100644 index 000000000..312e6bafc --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/code/example.tfvars @@ -0,0 +1,86 @@ +# ============================================================================= +# Azure AI Foundry — Private Network Agent Setup +# ============================================================================= +# Copy this file to terraform.tfvars and fill in the required values. +# cp example.tfvars terraform.tfvars +# +# For BYO (Bring Your Own) resources, uncomment and fill the relevant section. +# Any BYO variable left empty (or commented out) will create a new resource. +# ============================================================================= + +# ============================== +# REQUIRED — Target region +# ============================== +# See README.md for supported regions. Class B/C subnets (192.168.x.x, 172.16.x.x) +# are supported in all regions. Class A (10.x.x.x) is limited to specific regions. +location = "swedencentral" + +# ============================== +# OPTIONAL — Resource configuration +# ============================== +ai_services_name_prefix = "aifoundry" +project_name = "agent-project" +vnet_address_space = ["192.168.0.0/16"] + +# ============================== +# OPTIONAL — Model deployment +# ============================== +model_name = "gpt-4.1" +model_version = "2025-04-14" +model_capacity = 40 + +# ============================== +# BYO — Resource Group +# ============================== +# Provide an existing resource group name. All new resources deploy into this RG. +# Leave empty to create a new one. +existing_resource_group_name = "" + +# ============================== +# BYO — Networking +# ============================== +# Provide an existing VNet and all three subnet IDs together. +# The agent and MCP subnets must be delegated to Microsoft.App/environments. +# The PE subnet must have no delegation. +# Leave empty to create new networking resources. +existing_vnet_id = "" +existing_agent_subnet_id = "" +existing_pe_subnet_id = "" +existing_mcp_subnet_id = "" + +# ============================== +# BYO — Backend Services +# ============================== +# Provide existing resource IDs. Each can be set independently. +# These resources must already have public access disabled and be in the same region. +# Leave empty to create new ones. +existing_storage_account_id = "" +existing_cosmosdb_account_id = "" +existing_ai_search_id = "" + +# ============================== +# BYO — Private DNS Zones +# ============================== +# If your DNS zones are centrally managed (e.g., hub-spoke topology), provide the +# resource group (and optionally subscription) where all 6 zones exist: +# privatelink.cognitiveservices.azure.com, privatelink.openai.azure.com, +# privatelink.services.ai.azure.com, privatelink.blob.core.windows.net, +# privatelink.search.windows.net, privatelink.documents.azure.com +# Leave empty to create new zones. +existing_dns_zones_resource_group = "" +existing_dns_zones_subscription_id = "" + +# ============================== +# BYO — Fabric (optional) +# ============================== +# Provide an existing Fabric workspace ID to create a Data Agent private endpoint. +# Leave empty to skip Fabric integration. +existing_fabric_workspace_id = "" + +# ============================== +# OPTIONAL — Azure Container Registry +# ============================== +# Enable to create a Premium ACR with Private Endpoint for hosted agent containers. +# Set developer_ip_cidr to allow push access from your IP (e.g., your VPN or dev machine). +enable_container_registry = false +developer_ip_cidr = "" diff --git a/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/code/locals.tf b/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/code/locals.tf new file mode 100644 index 000000000..08554af4a --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/code/locals.tf @@ -0,0 +1,78 @@ +########## Local values +########## + +locals { + ## Naming + account_name = lower("${var.ai_services_name_prefix}${random_string.unique.result}") + + ## Subnet CIDRs (used only when creating new subnets) + subnet_agent_address_prefix = cidrsubnet(var.vnet_address_space[0], 8, 0) + subnet_pe_address_prefix = cidrsubnet(var.vnet_address_space[0], 8, 1) + subnet_mcp_address_prefix = cidrsubnet(var.vnet_address_space[0], 8, 2) + + ## Project GUID (derived from project internalId after project creation) + project_id_guid = "${substr(azapi_resource.ai_project.output.properties.internalId, 0, 8)}-${substr(azapi_resource.ai_project.output.properties.internalId, 8, 4)}-${substr(azapi_resource.ai_project.output.properties.internalId, 12, 4)}-${substr(azapi_resource.ai_project.output.properties.internalId, 16, 4)}-${substr(azapi_resource.ai_project.output.properties.internalId, 20, 12)}" + + ########## BYO flags — true when we need to create the resource + ########## + create_rg = var.existing_resource_group_name == "" + create_vnet = var.existing_vnet_id == "" + create_storage = var.existing_storage_account_id == "" + create_cosmos = var.existing_cosmosdb_account_id == "" + create_search = var.existing_ai_search_id == "" + create_fabric = var.existing_fabric_workspace_id != "" + + ## DNS zones — create when no existing resource group provided + create_dns_zones = var.existing_dns_zones_resource_group == "" + dns_zones_sub_id = coalesce(var.existing_dns_zones_subscription_id, data.azurerm_client_config.current.subscription_id) + dns_zones_rg = var.existing_dns_zones_resource_group + + ########## Unified resource references + ########## All downstream code uses these locals instead of direct resource references. + ########## + + ## Resource Group + rg_name = local.create_rg ? azurerm_resource_group.rg[0].name : var.existing_resource_group_name + rg_id = local.create_rg ? azurerm_resource_group.rg[0].id : "/subscriptions/${data.azurerm_client_config.current.subscription_id}/resourceGroups/${var.existing_resource_group_name}" + + ## VNet + vnet_id = local.create_vnet ? azurerm_virtual_network.vnet[0].id : var.existing_vnet_id + vnet_name = local.create_vnet ? azurerm_virtual_network.vnet[0].name : split("/", var.existing_vnet_id)[8] + vnet_rg_name = local.create_vnet ? local.rg_name : split("/", var.existing_vnet_id)[4] + + ## Subnets + subnet_agent_id = var.existing_agent_subnet_id != "" ? var.existing_agent_subnet_id : azurerm_subnet.subnet_agent[0].id + subnet_pe_id = var.existing_pe_subnet_id != "" ? var.existing_pe_subnet_id : azurerm_subnet.subnet_pe[0].id + subnet_mcp_id = var.existing_mcp_subnet_id != "" ? var.existing_mcp_subnet_id : azurerm_subnet.subnet_mcp[0].id + + ## Storage Account + storage_id = local.create_storage ? azurerm_storage_account.storage_account[0].id : var.existing_storage_account_id + storage_name = local.create_storage ? azurerm_storage_account.storage_account[0].name : split("/", var.existing_storage_account_id)[8] + storage_endpoint = local.create_storage ? azurerm_storage_account.storage_account[0].primary_blob_endpoint : data.azurerm_storage_account.existing[0].primary_blob_endpoint + storage_location = local.create_storage ? var.location : data.azurerm_storage_account.existing[0].location + + ## Cosmos DB + cosmos_id = local.create_cosmos ? azurerm_cosmosdb_account.cosmosdb[0].id : var.existing_cosmosdb_account_id + cosmos_name = local.create_cosmos ? azurerm_cosmosdb_account.cosmosdb[0].name : split("/", var.existing_cosmosdb_account_id)[8] + cosmos_rg_name = local.create_cosmos ? local.rg_name : split("/", var.existing_cosmosdb_account_id)[4] + cosmos_endpoint = local.create_cosmos ? azurerm_cosmosdb_account.cosmosdb[0].endpoint : data.azurerm_cosmosdb_account.existing[0].endpoint + cosmos_location = local.create_cosmos ? var.location : data.azurerm_cosmosdb_account.existing[0].location + + ## AI Search + search_id = local.create_search ? azapi_resource.ai_search[0].id : var.existing_ai_search_id + search_name = local.create_search ? azapi_resource.ai_search[0].name : split("/", var.existing_ai_search_id)[8] + search_location = local.create_search ? var.location : data.azapi_resource.existing_search[0].output.location + + ## Fabric (BYO-only, never created — only PE + DNS when workspace ID provided) + fabric_name = local.create_fabric ? split("/", var.existing_fabric_workspace_id)[8] : "" + + ## DNS Zone IDs — construct from subscription/RG/name when BYO, otherwise use created resource + dns_zone_cognitive_services_id = local.create_dns_zones ? azurerm_private_dns_zone.plz_cognitive_services[0].id : "/subscriptions/${local.dns_zones_sub_id}/resourceGroups/${local.dns_zones_rg}/providers/Microsoft.Network/privateDnsZones/privatelink.cognitiveservices.azure.com" + dns_zone_openai_id = local.create_dns_zones ? azurerm_private_dns_zone.plz_openai[0].id : "/subscriptions/${local.dns_zones_sub_id}/resourceGroups/${local.dns_zones_rg}/providers/Microsoft.Network/privateDnsZones/privatelink.openai.azure.com" + dns_zone_ai_services_id = local.create_dns_zones ? azurerm_private_dns_zone.plz_ai_services[0].id : "/subscriptions/${local.dns_zones_sub_id}/resourceGroups/${local.dns_zones_rg}/providers/Microsoft.Network/privateDnsZones/privatelink.services.ai.azure.com" + dns_zone_search_id = local.create_dns_zones ? azurerm_private_dns_zone.plz_ai_search[0].id : "/subscriptions/${local.dns_zones_sub_id}/resourceGroups/${local.dns_zones_rg}/providers/Microsoft.Network/privateDnsZones/privatelink.search.windows.net" + dns_zone_storage_blob_id = local.create_dns_zones ? azurerm_private_dns_zone.plz_storage_blob[0].id : "/subscriptions/${local.dns_zones_sub_id}/resourceGroups/${local.dns_zones_rg}/providers/Microsoft.Network/privateDnsZones/privatelink.blob.core.windows.net" + dns_zone_cosmos_db_id = local.create_dns_zones ? azurerm_private_dns_zone.plz_cosmos_db[0].id : "/subscriptions/${local.dns_zones_sub_id}/resourceGroups/${local.dns_zones_rg}/providers/Microsoft.Network/privateDnsZones/privatelink.documents.azure.com" + dns_zone_fabric_id = local.create_dns_zones && local.create_fabric ? azurerm_private_dns_zone.plz_fabric[0].id : local.create_fabric ? "/subscriptions/${local.dns_zones_sub_id}/resourceGroups/${local.dns_zones_rg}/providers/Microsoft.Network/privateDnsZones/privatelink.fabric.microsoft.com" : "" + dns_zone_acr_id = var.enable_container_registry && local.create_dns_zones ? azurerm_private_dns_zone.plz_acr[0].id : var.enable_container_registry ? "/subscriptions/${local.dns_zones_sub_id}/resourceGroups/${local.dns_zones_rg}/providers/Microsoft.Network/privateDnsZones/privatelink.azurecr.io" : "" +} diff --git a/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/code/main.tf b/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/code/main.tf new file mode 100644 index 000000000..82f072306 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/code/main.tf @@ -0,0 +1,855 @@ +########## Create private network infrastructure for AI Foundry with agent tools +########## + +## Get subscription data +data "azurerm_client_config" "current" {} + +## Create a random string for unique naming +resource "random_string" "unique" { + length = 4 + min_numeric = 4 + numeric = true + special = false + lower = true + upper = false +} + +## Create a resource group +resource "azurerm_resource_group" "rg" { + count = local.create_rg ? 1 : 0 + name = "rg-aifoundry${random_string.unique.result}" + location = var.location +} + +## Create Virtual Network (always needed for private endpoints) +resource "azurerm_virtual_network" "vnet" { + count = local.create_vnet ? 1 : 0 + name = "vnet-aifoundry${random_string.unique.result}" + address_space = var.vnet_address_space + location = var.location + resource_group_name = local.rg_name +} + +## Create Subnet for agent compute (used by networkInjections) +resource "azurerm_subnet" "subnet_agent" { + count = var.existing_agent_subnet_id == "" ? 1 : 0 + name = "agent-subnet" + resource_group_name = local.rg_name + virtual_network_name = local.vnet_name + address_prefixes = [local.subnet_agent_address_prefix] + + delegation { + name = "Microsoft.App/environments" + service_delegation { + name = "Microsoft.App/environments" + actions = [ + "Microsoft.Network/virtualNetworks/subnets/join/action" + ] + } + } +} + +## Create Subnet for private endpoints +resource "azurerm_subnet" "subnet_pe" { + count = var.existing_pe_subnet_id == "" ? 1 : 0 + name = "pe-subnet" + resource_group_name = local.rg_name + virtual_network_name = local.vnet_name + address_prefixes = [local.subnet_pe_address_prefix] + + depends_on = [azurerm_subnet.subnet_agent] +} + +## Create Subnet for MCP/OpenAPI/A2A tool servers +resource "azurerm_subnet" "subnet_mcp" { + count = var.existing_mcp_subnet_id == "" ? 1 : 0 + name = "mcp-subnet" + resource_group_name = local.rg_name + virtual_network_name = local.vnet_name + address_prefixes = [local.subnet_mcp_address_prefix] + + delegation { + name = "Microsoft.App/environments" + service_delegation { + name = "Microsoft.App/environments" + actions = [ + "Microsoft.Network/virtualNetworks/subnets/join/action" + ] + } + } + + depends_on = [azurerm_subnet.subnet_pe] +} + +########## Create resources required to store agent data +########## + +## Create a storage account for agent data +## +resource "azurerm_storage_account" "storage_account" { + count = local.create_storage ? 1 : 0 + name = "aifoundry${random_string.unique.result}storage" + resource_group_name = local.rg_name + location = var.location + + account_kind = "StorageV2" + account_tier = "Standard" + account_replication_type = "ZRS" + + ## Identity configuration + shared_access_key_enabled = false + + ## Network access configuration + min_tls_version = "TLS1_2" + allow_nested_items_to_be_public = false + public_network_access_enabled = false + + network_rules { + default_action = "Deny" + bypass = ["AzureServices"] + } +} + +## Create the Cosmos DB account to store agent threads +## +resource "azurerm_cosmosdb_account" "cosmosdb" { + count = local.create_cosmos ? 1 : 0 + name = "aifoundry${random_string.unique.result}cosmosdb" + location = var.location + resource_group_name = local.rg_name + + # General settings + offer_type = "Standard" + kind = "GlobalDocumentDB" + free_tier_enabled = false + + # Set security-related settings + local_authentication_disabled = true + public_network_access_enabled = false + + # Set high availability and failover settings + automatic_failover_enabled = false + multiple_write_locations_enabled = false + + # Configure consistency settings + consistency_policy { + consistency_level = "Session" + } + + # Configure single location with no zone redundancy to reduce costs + geo_location { + location = var.location + failover_priority = 0 + zone_redundant = false + } +} + +## Create an AI Search instance that will be used to store vector embeddings +## +resource "azapi_resource" "ai_search" { + count = local.create_search ? 1 : 0 + type = "Microsoft.Search/searchServices@2024-06-01-preview" + name = "aifoundry${random_string.unique.result}search" + parent_id = local.rg_id + location = var.location + schema_validation_enabled = false + + body = { + sku = { + name = "standard" + } + + identity = { + type = "SystemAssigned" + } + + properties = { + + # Search-specific properties + replicaCount = 1 + partitionCount = 1 + hostingMode = "Default" + semanticSearch = "disabled" + + # Identity-related controls + disableLocalAuth = false + authOptions = { + aadOrApiKey = { + aadAuthFailureMode = "http401WithBearerChallenge" + } + } + + # Networking-related controls + publicNetworkAccess = "Disabled" + networkRuleSet = { + bypass = "None" + } + } + } +} + +########## Create AI Foundry resource +########## + +## Wait for VNet/subnet propagation before creating AI Foundry. +## The Cognitive Services RP validates the VNet via ARM, which has eventual consistency. +## Without this delay, networkInjections can fail with "virtual network could not be found". +resource "time_sleep" "wait_for_subnet_propagation" { + depends_on = [azurerm_subnet.subnet_agent] + create_duration = "60s" +} + +## Create the AI Foundry resource +## +resource "azapi_resource" "ai_foundry" { + depends_on = [ + azurerm_subnet.subnet_agent, + time_sleep.wait_for_subnet_propagation, + azapi_resource_action.purge_ai_foundry + ] + + type = "Microsoft.CognitiveServices/accounts@2025-04-01-preview" + name = local.account_name + parent_id = local.rg_id + location = var.location + schema_validation_enabled = false + + identity { + type = "SystemAssigned" + } + + body = { + kind = "AIServices" + sku = { + name = "S0" + } + properties = { + # Support both Entra ID and API Key authentication for underlying Cognitive Services account + disableLocalAuth = false + + # Specifies that this is an AI Foundry resource + allowProjectManagement = true + + # Set custom subdomain name for DNS names created for this Foundry resource + customSubDomainName = local.account_name + + # Network-related controls + # Disable public access but allow Trusted Azure Services exception + publicNetworkAccess = "Disabled" + networkAcls = { + defaultAction = "Deny" + bypass = "AzureServices" + virtualNetworkRules = [] + ipRules = [] + } + + # Enable VNet injection for Standard Agents + networkInjections = [ + { + scenario = "agent" + subnetArmId = local.subnet_agent_id + useMicrosoftManagedNetwork = false + } + ] + } + } +} + +## Deploy a model in the AI Foundry resource +## +resource "azapi_resource" "model_deployment" { + depends_on = [ + azapi_resource.ai_foundry + ] + + type = "Microsoft.CognitiveServices/accounts/deployments@2025-04-01-preview" + name = var.model_name + parent_id = azapi_resource.ai_foundry.id + schema_validation_enabled = false + + body = { + sku = { + capacity = var.model_capacity + name = "GlobalStandard" + } + properties = { + model = { + name = var.model_name + format = "OpenAI" + version = var.model_version + } + } + } +} + +########## Create Private DNS Zones, Links, and Private Endpoints +########## + +## Create required Private DNS Zones +## +resource "azurerm_private_dns_zone" "plz_cosmos_db" { + count = local.create_dns_zones ? 1 : 0 + name = "privatelink.documents.azure.com" + resource_group_name = local.rg_name +} + +resource "azurerm_private_dns_zone" "plz_ai_search" { + count = local.create_dns_zones ? 1 : 0 + name = "privatelink.search.windows.net" + resource_group_name = local.rg_name +} + +resource "azurerm_private_dns_zone" "plz_storage_blob" { + count = local.create_dns_zones ? 1 : 0 + name = "privatelink.blob.core.windows.net" + resource_group_name = local.rg_name +} + +resource "azurerm_private_dns_zone" "plz_cognitive_services" { + count = local.create_dns_zones ? 1 : 0 + name = "privatelink.cognitiveservices.azure.com" + resource_group_name = local.rg_name +} + +resource "azurerm_private_dns_zone" "plz_ai_services" { + count = local.create_dns_zones ? 1 : 0 + name = "privatelink.services.ai.azure.com" + resource_group_name = local.rg_name +} + +resource "azurerm_private_dns_zone" "plz_openai" { + count = local.create_dns_zones ? 1 : 0 + name = "privatelink.openai.azure.com" + resource_group_name = local.rg_name +} + +## Create Private DNS Zone Links to link the Private DNS Zones to the virtual network +## +resource "azurerm_private_dns_zone_virtual_network_link" "plz_cosmos_db_link" { + count = local.create_dns_zones ? 1 : 0 + name = "privatelink-documents-azure-com-${random_string.unique.result}-link" + resource_group_name = local.rg_name + private_dns_zone_name = azurerm_private_dns_zone.plz_cosmos_db[0].name + virtual_network_id = local.vnet_id + registration_enabled = false +} + +resource "azurerm_private_dns_zone_virtual_network_link" "plz_ai_search_link" { + count = local.create_dns_zones ? 1 : 0 + depends_on = [azurerm_private_dns_zone_virtual_network_link.plz_cosmos_db_link] + name = "privatelink-search-windows-net-${random_string.unique.result}-link" + resource_group_name = local.rg_name + private_dns_zone_name = azurerm_private_dns_zone.plz_ai_search[0].name + virtual_network_id = local.vnet_id + registration_enabled = false +} + +resource "azurerm_private_dns_zone_virtual_network_link" "plz_storage_blob_link" { + count = local.create_dns_zones ? 1 : 0 + depends_on = [azurerm_private_dns_zone_virtual_network_link.plz_ai_search_link] + name = "privatelink-blob-core-windows-net-${random_string.unique.result}-link" + resource_group_name = local.rg_name + private_dns_zone_name = azurerm_private_dns_zone.plz_storage_blob[0].name + virtual_network_id = local.vnet_id + registration_enabled = false +} + +resource "azurerm_private_dns_zone_virtual_network_link" "plz_cognitive_services_link" { + count = local.create_dns_zones ? 1 : 0 + depends_on = [azurerm_private_dns_zone_virtual_network_link.plz_storage_blob_link] + name = "privatelink-cognitiveservices-azure-com-${random_string.unique.result}-link" + resource_group_name = local.rg_name + private_dns_zone_name = azurerm_private_dns_zone.plz_cognitive_services[0].name + virtual_network_id = local.vnet_id + registration_enabled = false +} + +resource "azurerm_private_dns_zone_virtual_network_link" "plz_ai_services_link" { + count = local.create_dns_zones ? 1 : 0 + depends_on = [azurerm_private_dns_zone_virtual_network_link.plz_cognitive_services_link] + name = "privatelink-services-ai-azure-com-${random_string.unique.result}-link" + resource_group_name = local.rg_name + private_dns_zone_name = azurerm_private_dns_zone.plz_ai_services[0].name + virtual_network_id = local.vnet_id + registration_enabled = false +} + +resource "azurerm_private_dns_zone_virtual_network_link" "plz_openai_link" { + count = local.create_dns_zones ? 1 : 0 + depends_on = [azurerm_private_dns_zone_virtual_network_link.plz_ai_services_link] + name = "privatelink-openai-azure-com-${random_string.unique.result}-link" + resource_group_name = local.rg_name + private_dns_zone_name = azurerm_private_dns_zone.plz_openai[0].name + virtual_network_id = local.vnet_id + registration_enabled = false +} + +## Fabric DNS Zone and VNet Link (only when Fabric workspace is provided) +resource "azurerm_private_dns_zone" "plz_fabric" { + count = local.create_dns_zones && local.create_fabric ? 1 : 0 + name = "privatelink.fabric.microsoft.com" + resource_group_name = local.rg_name +} + +resource "azurerm_private_dns_zone_virtual_network_link" "plz_fabric_link" { + count = local.create_dns_zones && local.create_fabric ? 1 : 0 + depends_on = [azurerm_private_dns_zone_virtual_network_link.plz_openai_link] + name = "privatelink-fabric-microsoft-com-${random_string.unique.result}-link" + resource_group_name = local.rg_name + private_dns_zone_name = azurerm_private_dns_zone.plz_fabric[0].name + virtual_network_id = local.vnet_id + registration_enabled = false +} + +## Create Private Endpoints for resources +## +resource "azurerm_private_endpoint" "pe_storage" { + depends_on = [azurerm_private_dns_zone_virtual_network_link.plz_openai_link] + + name = "${local.storage_name}-${random_string.unique.result}-private-endpoint" + location = var.location + resource_group_name = local.rg_name + subnet_id = local.subnet_pe_id + + private_service_connection { + name = "${local.storage_name}-${random_string.unique.result}-private-link-service-connection" + private_connection_resource_id = local.storage_id + subresource_names = ["blob"] + is_manual_connection = false + } + + private_dns_zone_group { + name = "${local.storage_name}-${random_string.unique.result}-dns-group" + private_dns_zone_ids = [local.dns_zone_storage_blob_id] + } +} + +resource "azurerm_private_endpoint" "pe_cosmos" { + depends_on = [azurerm_private_endpoint.pe_storage] + + name = "${local.cosmos_name}-${random_string.unique.result}-private-endpoint" + location = var.location + resource_group_name = local.rg_name + subnet_id = local.subnet_pe_id + + private_service_connection { + name = "${local.cosmos_name}-${random_string.unique.result}-private-link-service-connection" + private_connection_resource_id = local.cosmos_id + subresource_names = ["Sql"] + is_manual_connection = false + } + + private_dns_zone_group { + name = "${local.cosmos_name}-${random_string.unique.result}-dns-group" + private_dns_zone_ids = [local.dns_zone_cosmos_db_id] + } +} + +resource "azurerm_private_endpoint" "pe_search" { + depends_on = [azurerm_private_endpoint.pe_cosmos] + + name = "${local.search_name}-${random_string.unique.result}-private-endpoint" + location = var.location + resource_group_name = local.rg_name + subnet_id = local.subnet_pe_id + + private_service_connection { + name = "${local.search_name}-${random_string.unique.result}-private-link-service-connection" + private_connection_resource_id = local.search_id + subresource_names = ["searchService"] + is_manual_connection = false + } + + private_dns_zone_group { + name = "${local.search_name}-${random_string.unique.result}-dns-group" + private_dns_zone_ids = [local.dns_zone_search_id] + } +} + +resource "azurerm_private_endpoint" "pe_ai_foundry" { + depends_on = [azurerm_private_endpoint.pe_search] + + name = "${azapi_resource.ai_foundry.name}-private-endpoint" + location = var.location + resource_group_name = local.rg_name + subnet_id = local.subnet_pe_id + + private_service_connection { + name = "${azapi_resource.ai_foundry.name}-private-link-service-connection" + private_connection_resource_id = azapi_resource.ai_foundry.id + subresource_names = ["account"] + is_manual_connection = false + } + + private_dns_zone_group { + name = "${azapi_resource.ai_foundry.name}-dns-group" + private_dns_zone_ids = [ + local.dns_zone_cognitive_services_id, + local.dns_zone_ai_services_id, + local.dns_zone_openai_id + ] + } +} + +## Create Fabric Private Endpoint (only when Fabric workspace is provided) +resource "azurerm_private_endpoint" "pe_fabric" { + count = local.create_fabric ? 1 : 0 + depends_on = [azurerm_private_endpoint.pe_ai_foundry] + + name = "${local.fabric_name}-fabric-private-endpoint" + location = var.location + resource_group_name = local.rg_name + subnet_id = local.subnet_pe_id + + private_service_connection { + name = "${local.fabric_name}-private-link-service-connection" + private_connection_resource_id = var.existing_fabric_workspace_id + subresource_names = ["Fabric"] + is_manual_connection = false + } + + private_dns_zone_group { + name = "${local.fabric_name}-dns-group" + private_dns_zone_ids = [local.dns_zone_fabric_id] + } +} + +## Create AI Foundry project +resource "azapi_resource" "ai_project" { + depends_on = [ + azurerm_private_endpoint.pe_storage, + azurerm_private_endpoint.pe_search, + azurerm_private_endpoint.pe_cosmos, + azurerm_private_endpoint.pe_ai_foundry + ] + type = "Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview" + name = var.project_name + location = var.location + parent_id = azapi_resource.ai_foundry.id + schema_validation_enabled = false + + identity { + type = "SystemAssigned" + } + + body = { + sku = { + name = "S0" + } + properties = { + description = "AI Foundry project with private network agent tools" + displayName = var.project_name + } + } + + response_export_values = [ + "identity.principalId", + "properties.internalId" + ] +} + +## Wait 10 seconds for the AI Foundry project system-assigned managed identity to replicate through Entra ID +resource "time_sleep" "wait_project_identities" { + depends_on = [ + azapi_resource.ai_project + ] + create_duration = "10s" +} + +## Create project-level connections (AAD auth) +resource "azapi_resource" "conn_cosmosdb" { + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview" + name = local.cosmos_name + parent_id = azapi_resource.ai_project.id + schema_validation_enabled = false + + depends_on = [azapi_resource.ai_project] + + body = { + name = local.cosmos_name + properties = { + category = "CosmosDb" + target = local.cosmos_endpoint + authType = "AAD" + metadata = { + ApiType = "Azure" + ResourceId = local.cosmos_id + location = local.cosmos_location + } + } + } +} + +resource "azapi_resource" "conn_storage" { + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview" + name = local.storage_name + parent_id = azapi_resource.ai_project.id + schema_validation_enabled = false + + depends_on = [azapi_resource.ai_project] + + body = { + name = local.storage_name + properties = { + category = "AzureStorageAccount" + target = local.storage_endpoint + authType = "AAD" + metadata = { + ApiType = "Azure" + ResourceId = local.storage_id + location = local.storage_location + } + } + } +} + +resource "azapi_resource" "conn_aisearch" { + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview" + name = local.search_name + parent_id = azapi_resource.ai_project.id + schema_validation_enabled = false + + depends_on = [azapi_resource.ai_project] + + body = { + name = local.search_name + properties = { + category = "CognitiveSearch" + target = "https://${local.search_name}.search.windows.net" + authType = "AAD" + metadata = { + ApiType = "Azure" + ResourceId = local.search_id + location = local.search_location + } + } + } +} + +## Create the necessary role assignments for the AI Foundry project over the resources used to store agent data +resource "azurerm_role_assignment" "cosmosdb_operator_ai_foundry_project" { + depends_on = [ + time_sleep.wait_project_identities + ] + name = uuidv5("dns", "${azapi_resource.ai_project.name}${azapi_resource.ai_project.output.identity.principalId}${local.rg_name}cosmosdboperator") + scope = local.cosmos_id + role_definition_name = "Cosmos DB Operator" + principal_id = azapi_resource.ai_project.output.identity.principalId +} + +resource "azurerm_role_assignment" "storage_blob_data_contributor_ai_foundry_project" { + depends_on = [ + time_sleep.wait_project_identities + ] + name = uuidv5("dns", "${azapi_resource.ai_project.name}${azapi_resource.ai_project.output.identity.principalId}${local.storage_name}storageblobdatacontributor") + scope = local.storage_id + role_definition_name = "Storage Blob Data Contributor" + principal_id = azapi_resource.ai_project.output.identity.principalId +} + +resource "azurerm_role_assignment" "search_index_data_contributor_ai_foundry_project" { + depends_on = [ + time_sleep.wait_project_identities + ] + name = uuidv5("dns", "${azapi_resource.ai_project.name}${azapi_resource.ai_project.output.identity.principalId}${local.search_name}searchindexdatacontributor") + scope = local.search_id + role_definition_name = "Search Index Data Contributor" + principal_id = azapi_resource.ai_project.output.identity.principalId +} + +resource "azurerm_role_assignment" "search_service_contributor_ai_foundry_project" { + depends_on = [ + time_sleep.wait_project_identities + ] + name = uuidv5("dns", "${azapi_resource.ai_project.name}${azapi_resource.ai_project.output.identity.principalId}${local.search_name}searchservicecontributor") + scope = local.search_id + role_definition_name = "Search Service Contributor" + principal_id = azapi_resource.ai_project.output.identity.principalId +} + +## Pause 60 seconds to allow for role assignments to propagate +resource "time_sleep" "wait_rbac" { + depends_on = [ + azurerm_role_assignment.cosmosdb_operator_ai_foundry_project, + azurerm_role_assignment.storage_blob_data_contributor_ai_foundry_project, + azurerm_role_assignment.search_index_data_contributor_ai_foundry_project, + azurerm_role_assignment.search_service_contributor_ai_foundry_project + ] + create_duration = "60s" +} + +## Create the AI Foundry project capability host +## +resource "azapi_resource" "ai_foundry_project_capability_host" { + depends_on = [ + azapi_resource.conn_cosmosdb, + azapi_resource.conn_storage, + azapi_resource.conn_aisearch, + time_sleep.wait_rbac + ] + type = "Microsoft.CognitiveServices/accounts/projects/capabilityHosts@2025-04-01-preview" + name = "caphostproj" + parent_id = azapi_resource.ai_project.id + schema_validation_enabled = false + + body = { + properties = { + capabilityHostKind = "Agents" + vectorStoreConnections = [ + local.search_name + ] + storageConnections = [ + local.storage_name + ] + threadStorageConnections = [ + local.cosmos_name + ] + } + } +} + +## Create the necessary data plane role assignments to the CosmosDb account created by the AI Foundry Project +## +resource "azurerm_cosmosdb_sql_role_assignment" "cosmosdb_db_sql_role_aifp" { + depends_on = [ + azapi_resource.ai_foundry_project_capability_host + ] + name = uuidv5("dns", "${azapi_resource.ai_project.name}${azapi_resource.ai_project.output.identity.principalId}cosmosdb_dbsqlrole") + resource_group_name = local.cosmos_rg_name + account_name = local.cosmos_name + scope = local.cosmos_id + role_definition_id = "${local.cosmos_id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002" + principal_id = azapi_resource.ai_project.output.identity.principalId +} + +## Create the necessary data plane role assignments to the Azure Storage Account containers created by the AI Foundry Project +## +resource "azurerm_role_assignment" "storage_blob_data_owner_ai_foundry_project" { + depends_on = [ + azapi_resource.ai_foundry_project_capability_host + ] + name = uuidv5("dns", "${azapi_resource.ai_project.name}${azapi_resource.ai_project.output.identity.principalId}${local.storage_name}storageblobdataowner") + scope = local.storage_id + role_definition_name = "Storage Blob Data Owner" + principal_id = azapi_resource.ai_project.output.identity.principalId + condition_version = "2.0" + condition = <<-EOT + ( + ( + !(ActionMatches{'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/tags/read'}) + AND !(ActionMatches{'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/filter/action'}) + AND !(ActionMatches{'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/tags/write'}) + ) + OR + (@Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringStartsWithIgnoreCase '${local.project_id_guid}' + AND @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringLikeIgnoreCase '*-azureml-agent') + ) + EOT +} + +########## Optional: Azure Container Registry with Private Endpoint +########## + +resource "azurerm_container_registry" "acr" { + count = var.enable_container_registry ? 1 : 0 + + name = "acr${random_string.unique.result}" + resource_group_name = local.rg_name + location = var.location + sku = "Premium" + admin_enabled = false + public_network_access_enabled = var.developer_ip_cidr != "" ? true : false + + dynamic "network_rule_set" { + for_each = var.developer_ip_cidr != "" ? [1] : [] + content { + default_action = "Deny" + ip_rule { + action = "Allow" + ip_range = var.developer_ip_cidr + } + } + } +} + +resource "azurerm_private_dns_zone" "plz_acr" { + count = var.enable_container_registry && local.create_dns_zones ? 1 : 0 + name = "privatelink.azurecr.io" + resource_group_name = local.rg_name +} + +resource "azurerm_private_dns_zone_virtual_network_link" "plz_acr_link" { + count = var.enable_container_registry && local.create_dns_zones ? 1 : 0 + + depends_on = [ + azurerm_private_dns_zone_virtual_network_link.plz_openai_link + ] + + name = "privatelink-azurecr-io-${random_string.unique.result}-link" + resource_group_name = local.rg_name + private_dns_zone_name = azurerm_private_dns_zone.plz_acr[0].name + virtual_network_id = local.vnet_id + registration_enabled = false +} + +resource "azurerm_private_endpoint" "pe_acr" { + count = var.enable_container_registry ? 1 : 0 + + depends_on = [ + azurerm_private_dns_zone_virtual_network_link.plz_acr_link, + azurerm_private_endpoint.pe_ai_foundry + ] + + name = "acr${random_string.unique.result}-private-endpoint" + location = var.location + resource_group_name = local.rg_name + subnet_id = local.subnet_pe_id + + private_service_connection { + name = "acr${random_string.unique.result}-private-link-service-connection" + private_connection_resource_id = azurerm_container_registry.acr[0].id + subresource_names = ["registry"] + is_manual_connection = false + } + + private_dns_zone_group { + name = "acr${random_string.unique.result}-dns-group" + private_dns_zone_ids = [local.dns_zone_acr_id] + } +} + +## Grant the project identity AcrPull on the container registry +resource "azurerm_role_assignment" "acr_pull_project" { + count = var.enable_container_registry ? 1 : 0 + + depends_on = [ + azapi_resource.ai_project, + azurerm_container_registry.acr + ] + + name = uuidv5("dns", "${azapi_resource.ai_project.name}${azapi_resource.ai_project.output.identity.principalId}acr${random_string.unique.result}acrpull") + scope = azurerm_container_registry.acr[0].id + role_definition_name = "AcrPull" + principal_id = azapi_resource.ai_project.output.identity.principalId +} + +########## Destroy-time resources +########## + +## Added AI Foundry account purger to avoid running into InUseSubnetCannotBeDeleted-lock caused by the agent subnet delegation. +## The azapi_resource_action.purge_ai_foundry (only gets executed during destroy) purges the AI foundry account removing /subnets/snet-agent/serviceAssociationLinks/legionservicelink so the agent subnet can get properly removed. + +resource "azapi_resource_action" "purge_ai_foundry" { + method = "DELETE" + resource_id = "/subscriptions/${data.azurerm_client_config.current.subscription_id}/providers/Microsoft.CognitiveServices/locations/${var.location}/resourceGroups/${local.rg_name}/deletedAccounts/${local.account_name}" + type = "Microsoft.CognitiveServices/locations/resourceGroups/deletedAccounts@2021-04-30" + when = "destroy" + + depends_on = [time_sleep.purge_ai_foundry_cooldown] +} + +resource "time_sleep" "purge_ai_foundry_cooldown" { + destroy_duration = "900s" # 10-15m is enough time to let the backend remove the /subnets/snet-agent/serviceAssociationLinks/legionservicelink + + depends_on = [azurerm_subnet.subnet_agent] +} \ No newline at end of file diff --git a/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/code/outputs.tf b/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/code/outputs.tf new file mode 100644 index 000000000..707b58bd4 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/code/outputs.tf @@ -0,0 +1,29 @@ +output "resource_group_name" { + description = "The name of the resource group" + value = local.rg_name +} + +output "vnet_id" { + description = "The ID of the virtual network" + value = local.vnet_id +} + +output "ai_foundry_id" { + description = "The ID of the AI Foundry account" + value = azapi_resource.ai_foundry.id +} + +output "storage_account_id" { + description = "The ID of the storage account" + value = local.storage_id +} + +output "search_service_id" { + description = "The ID of the AI Search service" + value = local.search_id +} + +output "cosmos_db_id" { + description = "The ID of the Cosmos DB account" + value = local.cosmos_id +} diff --git a/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/providers.tf b/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/code/providers.tf similarity index 100% rename from infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/providers.tf rename to infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/code/providers.tf diff --git a/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/code/variables.tf b/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/code/variables.tf new file mode 100644 index 000000000..8a6947f02 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/code/variables.tf @@ -0,0 +1,124 @@ +variable "location" { + description = "The Azure region where resources will be deployed" + type = string +} + +variable "ai_services_name_prefix" { + description = "Prefix for AI Foundry account name" + type = string + default = "aifoundry" +} + +variable "project_name" { + description = "The name of the project" + type = string + default = "hybrid-agent-project" +} + +variable "vnet_address_space" { + description = "Address space for the virtual network" + type = list(string) + default = ["192.168.0.0/16"] +} + +variable "model_name" { + description = "The model to deploy" + type = string + default = "gpt-4.1" +} + +variable "model_version" { + description = "The version of the model" + type = string + default = "2025-04-14" +} + +variable "model_capacity" { + description = "The capacity of the model deployment" + type = number + default = 40 +} + +########## BYO (Bring Your Own) resource variables +########## Leave empty to create new resources. Provide resource IDs to use existing ones. + +variable "existing_resource_group_name" { + description = "Name of an existing resource group. Leave empty to create a new one." + type = string + default = "" +} + +variable "existing_vnet_id" { + description = "Resource ID of an existing VNet. Leave empty to create a new one. When provided, existing subnet IDs must also be provided." + type = string + default = "" +} + +variable "existing_agent_subnet_id" { + description = "Resource ID of an existing agent subnet (must be delegated to Microsoft.App/environments). Leave empty to create a new one." + type = string + default = "" +} + +variable "existing_pe_subnet_id" { + description = "Resource ID of an existing private endpoint subnet. Leave empty to create a new one." + type = string + default = "" +} + +variable "existing_mcp_subnet_id" { + description = "Resource ID of an existing MCP subnet (must be delegated to Microsoft.App/environments). Leave empty to create a new one." + type = string + default = "" +} + +variable "existing_storage_account_id" { + description = "Resource ID of an existing Storage Account. Leave empty to create a new one." + type = string + default = "" +} + +variable "existing_cosmosdb_account_id" { + description = "Resource ID of an existing Cosmos DB account. Leave empty to create a new one." + type = string + default = "" +} + +variable "existing_ai_search_id" { + description = "Resource ID of an existing AI Search service. Leave empty to create a new one." + type = string + default = "" +} + +variable "existing_dns_zones_resource_group" { + description = "Resource group containing existing private DNS zones. Leave empty to create new zones. When provided, all 6 zones are expected to exist in this RG." + type = string + default = "" +} + +variable "existing_dns_zones_subscription_id" { + description = "Subscription ID where existing private DNS zones are located. Leave empty to use the current subscription. Only used when existing_dns_zones_resource_group is set." + type = string + default = "" +} + +variable "existing_fabric_workspace_id" { + description = "Resource ID of an existing Fabric workspace for Data Agent private endpoint. Leave empty to skip Fabric integration." + type = string + default = "" +} + +########## Optional: Azure Container Registry +########## Enable to create a Premium ACR with Private Endpoint for hosted agent containers. + +variable "enable_container_registry" { + description = "Enable Azure Container Registry with Private Endpoint for hosted agent containers" + type = bool + default = false +} + +variable "developer_ip_cidr" { + description = "Optional developer IP CIDR to allowlist for ACR push access (e.g., 203.0.113.0/26). Only used when enable_container_registry is true. When empty, public access remains disabled." + type = string + default = "" +} diff --git a/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/versions.tf b/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/code/versions.tf similarity index 100% rename from infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/versions.tf rename to infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/code/versions.tf diff --git a/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/tools/README.md b/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/tools/README.md new file mode 100644 index 000000000..bb7fa96c1 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/tools/README.md @@ -0,0 +1,210 @@ +# Tools Behind VNet (Terraform) + +Deploy agent tools on the private VNet after the base Template 19 infrastructure is up. Each tool runs on the MCP or integration subnet and is accessible to Foundry agents through the DataProxy. + +These tools are the Terraform equivalents of the [Bicep 19 tool deployments](../../../infrastructure-setup-bicep/19-private-network-agent-tools/). + +## Available Tools + +| Tool | Infra Template | App Code | Description | +|------|----------------|----------|-------------| +| **[Azure Function](azure-function/)** | Terraform (`main.tf`) | [Shared](../../../infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/) | Python Function App with VNet Integration + storage private endpoints | +| **MCP Server** | CLI (see below) | [Shared](../../../infrastructure-setup-bicep/19-private-network-agent-tools/mcp-http-server/) | Multi-auth MCP server on Container Apps | +| **OpenAPI Server** | CLI (see below) | [Shared](../../../infrastructure-setup-bicep/19-private-network-agent-tools/openapi-server/) | FastAPI calculator on Container Apps | +| **A2A Server** | CLI (see below) | [Shared](../../../infrastructure-setup-bicep/19-private-network-agent-tools/a2a-server/) | Agent-to-Agent server on Container Apps | + +## Azure Function (Terraform) + +The Azure Function has its own Terraform template since it requires infrastructure (App Service Plan, Storage PEs, VNet Integration subnet). See [azure-function/](azure-function/) for full details. + +```bash +cd azure-function +cp example.tfvars terraform.tfvars +# Edit terraform.tfvars with your base deployment outputs +terraform init +terraform plan -var-file="terraform.tfvars" -out=tfplan +terraform apply tfplan +``` + +## Container App Tools (CLI) + +MCP, OpenAPI, and A2A servers run as Container Apps on the MCP subnet. These are deployed via Azure CLI after the base infrastructure is up — no additional Terraform template is needed. + +### 1. Build and Push Container Images + +Build images from the shared Bicep 19 app code and push to ACR. + +```bash +BICEP_TOOLS="../../../infrastructure-setup-bicep/19-private-network-agent-tools" +ACR_NAME="" # e.g. acr0500 +``` + +**Option A — `az acr build` (remote build)** + +> **Note**: `az acr build` will fail if your ACR has `defaultAction: Deny` because the +> remote build agent's IP is not in the allowlist. Use Option B instead. + +```bash +# OpenAPI server +az acr build --registry $ACR_NAME --image openapi-server:latest $BICEP_TOOLS/openapi-server + +# A2A server +az acr build --registry $ACR_NAME --image a2a-server:latest $BICEP_TOOLS/a2a-server + +# MCP server (import pre-built image) +az acr import --name $ACR_NAME \ + --source retrievaltestacr.azurecr.io/multi-auth-mcp/api-multi-auth-mcp-env:latest \ + --image multi-auth-mcp:latest +``` + +**Option B — Local `docker build` + `docker push` (when ACR has firewall rules)** + +```bash +az acr login --name $ACR_NAME + +# OpenAPI server +docker build -t $ACR_NAME.azurecr.io/openapi-server:latest $BICEP_TOOLS/openapi-server +docker push $ACR_NAME.azurecr.io/openapi-server:latest + +# A2A server +docker build -t $ACR_NAME.azurecr.io/a2a-server:latest $BICEP_TOOLS/a2a-server +docker push $ACR_NAME.azurecr.io/a2a-server:latest + +# MCP server (import pre-built image — works even with firewall) +az acr import --name $ACR_NAME \ + --source retrievaltestacr.azurecr.io/multi-auth-mcp/api-multi-auth-mcp-env:latest \ + --image multi-auth-mcp:latest +``` + +### 2. Create Container Apps Environment + +```bash +cd ../code +RG_NAME=$(terraform output -raw resource_group_name) +LOCATION="" # must match the base Template 19 deployment + +# Construct the MCP subnet ID from the VNet ID (mcp_subnet_id is not a TF output) +VNET_ID=$(terraform output -raw vnet_id) +MCP_SUBNET_ID="${VNET_ID}/subnets/mcp-subnet" # if using BYO networking, replace with your MCP subnet resource ID + +# Create an internal-only Container Apps environment on the MCP subnet +az containerapp env create \ + --resource-group $RG_NAME \ + --name "mcp-env" \ + --location $LOCATION \ + --infrastructure-subnet-resource-id $MCP_SUBNET_ID \ + --internal-only true +``` + +The environment gets a static IP and a custom domain (e.g. `..azurecontainerapps.io`). Note these for the DNS step. + +```bash +# Get environment domain and static IP +az containerapp env show -g $RG_NAME -n mcp-env \ + --query "{domain:properties.defaultDomain, staticIp:properties.staticIp}" -o table +``` + +### 3. Deploy Container Apps + +Each app needs `--registry-server` and `--registry-identity system` for managed identity-based ACR pull. + +> **Note**: The correct flag is `--registry-identity system`, not `--registry-identity system-environment`. + +```bash +ACR_NAME="" + +# OpenAPI server +az containerapp create \ + --resource-group $RG_NAME \ + --name "openapi-server" \ + --environment "mcp-env" \ + --image $ACR_NAME.azurecr.io/openapi-server:latest \ + --registry-server $ACR_NAME.azurecr.io \ + --registry-identity system \ + --target-port 8080 \ + --ingress external \ + --min-replicas 1 + +# MCP server +az containerapp create \ + --resource-group $RG_NAME \ + --name "mcp-server" \ + --environment "mcp-env" \ + --image $ACR_NAME.azurecr.io/multi-auth-mcp:latest \ + --registry-server $ACR_NAME.azurecr.io \ + --registry-identity system \ + --target-port 8080 \ + --ingress external \ + --min-replicas 1 \ + --env-vars PORT=8080 + +# A2A server +az containerapp create \ + --resource-group $RG_NAME \ + --name "a2a-server" \ + --environment "mcp-env" \ + --image $ACR_NAME.azurecr.io/a2a-server:latest \ + --registry-server $ACR_NAME.azurecr.io \ + --registry-identity system \ + --target-port 8080 \ + --ingress external \ + --min-replicas 1 +``` + +### 4. Configure Private DNS for Container Apps + +Since the environment is `--internal-only`, its FQDNs resolve only inside the VNet. Create a private DNS zone so the DataProxy (and VPN clients) can reach them. + +```bash +# Get the environment domain +ENV_DOMAIN=$(az containerapp env show -g $RG_NAME -n mcp-env \ + --query "properties.defaultDomain" -o tsv) +ENV_STATIC_IP=$(az containerapp env show -g $RG_NAME -n mcp-env \ + --query "properties.staticIp" -o tsv) +VNET_NAME=$(az network vnet list -g $RG_NAME --query "[0].name" -o tsv) + +# Create private DNS zone for the Container Apps domain +az network private-dns zone create \ + --resource-group $RG_NAME \ + --name $ENV_DOMAIN + +# Link DNS zone to VNet +az network private-dns zone vnet-link create \ + --resource-group $RG_NAME \ + --zone-name $ENV_DOMAIN \ + --name "mcp-env-link" \ + --virtual-network $VNET_NAME \ + --registration-enabled false + +# Wildcard A record → all *.domain resolves to the environment static IP +az network private-dns record-set a add-record \ + --resource-group $RG_NAME \ + --zone-name $ENV_DOMAIN \ + --record-set-name "*" \ + --ipv4-address $ENV_STATIC_IP +``` + +After this, Container App FQDNs (e.g. `mcp-server.`) resolve to the environment's private IP from within the VNet. + +## Testing + +End-to-end test scripts for all tools are in the [Bicep 19 tests/ directory](../../../infrastructure-setup-bicep/19-private-network-agent-tools/tests/). These tests work with both Bicep and Terraform deployments — they only need the endpoint URLs. + +```bash +cd ../../../infrastructure-setup-bicep/19-private-network-agent-tools/tests + +# Test Azure Function as OpenAPI tool +python test_azure_function_agents_v2.py --test all --retry 3 + +# Test MCP tools +python test_mcp_tools_agents_v2.py --test all --retry 3 + +# Test OpenAPI tools +python test_openapi_tool_agents_v2.py --test all --retry 3 +``` + +## Reference + +- [Bicep 19 tools](../../../infrastructure-setup-bicep/19-private-network-agent-tools/) — Shared app code and Bicep templates +- [Base TF 19 deployment](../) — Deploy the base infrastructure first +- [Microsoft Foundry Agent tools documentation](https://learn.microsoft.com/azure/ai-foundry/agents/how-to/virtual-networks) diff --git a/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/tools/azure-function/README.md b/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/tools/azure-function/README.md new file mode 100644 index 000000000..e540e9526 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/tools/azure-function/README.md @@ -0,0 +1,144 @@ +# Azure Function Behind VNet — Calculator with Private Storage (Terraform) + +A minimal Azure Function that demonstrates **VNet Integration**: the function performs arithmetic AND stores results in an **Azure Blob Storage account** with private endpoints. The storage account is initially deployed with public access enabled (required for the Functions runtime file share), but traffic routes through private endpoints via VNet Integration. + +This is the Terraform equivalent of the [Bicep `deploy-function.bicep`](../../../../infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/deploy-function.bicep). + +## Key Concepts + +> **"Azure Functions behind a VNet"** means the Function App uses **VNet Integration** +> for outbound traffic, letting it reach private resources (databases, storage, APIs) +> that only the VNet can access. The function itself remains publicly accessible +> (`publicNetworkAccess: Enabled`) — the "private" part is what the function can *reach*, +> not who can *call* it. + +### `publicNetworkAccess` Must Be `Enabled` + +When a Function App is used as an **OpenAPI tool** with the Foundry DataProxy, setting `publicNetworkAccess: Disabled` causes `403 Ip Forbidden`. The DataProxy resolves DNS at the Foundry infrastructure level, not through your VNet's private DNS zones. + +> Use [App Service access restrictions](https://learn.microsoft.com/azure/app-service/app-service-ip-restrictions) if you need to limit inbound traffic to specific IP ranges. + +## Architecture + +``` +Agent (Foundry) + │ + │ OpenApiTool call → DataProxy + ▼ +Azure Function App (publicNetworkAccess: Enabled) + │ POST /api/calculate + │ + ├─ Compute: 12 × 8 = 96 ← works without VNet + │ + └─ Store result in Blob ← requires VNet Integration + │ + │ outbound via VNet Integration + ▼ + Storage Account (publicNetworkAccess: Enabled, with Private Endpoints) + └─ calculation-history/20260331T150000_multiply_12_8.json +``` + +> **Note:** Storage is deployed with public access enabled (required for the Functions +> runtime file share during provisioning). You can restrict to `default_action = "Deny"` +> after deployment — restart the Function App afterward to avoid 503 errors. + +## What Gets Deployed + +| Resource | Type | Purpose | +|----------|------|---------| +| Integration Subnet | `azurerm_subnet` | Delegated to `Microsoft.Web/serverFarms` for outbound VNet Integration | +| Storage Account | `azurerm_storage_account` | Functions runtime backing store (Blob + Queue + File) | +| Storage File Share | `azurerm_storage_share` | Content share for `WEBSITE_CONTENTSHARE` (required with `WEBSITE_CONTENTOVERVNET`) | +| Storage PEs (3) | `azurerm_private_endpoint` | Blob, Queue, File private endpoints | +| Storage DNS Zones (2) | `azurerm_private_dns_zone` | `privatelink.queue/file.core.windows.net` (blob zone reused from base deployment) | +| App Service Plan | `azurerm_service_plan` | Elastic Premium EP1 (Linux, required for VNet features) | +| Function App | `azurerm_linux_function_app` | Python 3.11, VNet Integration enabled | +| Function PE | `azurerm_private_endpoint` | Inbound private access from VNet callers | +| Function DNS Zone | `azurerm_private_dns_zone` | `privatelink.azurewebsites.net` | + +## Prerequisites + +- Base TF 19 infrastructure deployed (VNet, subnets, DNS zones) +- `resource_group_name` and `vnet_name` from the base deployment outputs +- An available address prefix for the integration subnet (default: `192.168.5.0/24`) + +## Deploy + +### 1. Configure variables + +```bash +cd infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/tools/azure-function +cp example.tfvars terraform.tfvars +``` + +Edit `terraform.tfvars` with your base deployment's resource group and VNet names. + +### 2. Deploy infrastructure + +```bash +terraform init +terraform plan -var-file="terraform.tfvars" -out=tfplan +terraform apply tfplan +``` + +### 3. Deploy function code + +The function app code is shared with the Bicep template. Copy from the Bicep 19 directory and deploy: + +```bash +# Copy function code from the shared Bicep 19 location +FUNC_CODE_DIR="../../../../infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server" + +# Get the Function App name from Terraform output +FUNC_APP_NAME=$(terraform output -raw function_app_name) + +# Deploy the function code +cd $FUNC_CODE_DIR +func azure functionapp publish $FUNC_APP_NAME +``` + +### 4. Verify + +```bash +# Health check — should show storage reachable via VNet +curl "https://$(terraform output -raw function_app_hostname)/api/healthz" + +# Test calculation with private storage write +curl -X POST "https://$(terraform output -raw function_app_hostname)/api/calculate" \ + -H "Content-Type: application/json" \ + -d '{"operation": "multiply", "a": 6, "b": 7}' +``` + +The `storage.stored: true` field in the response proves VNet Integration is working. + +## Function App Code + +The function app code (Python) is shared across Bicep and Terraform deployments: + +| File | Location | Description | +|------|----------|-------------| +| `function_app.py` | [Bicep 19 azure-function-server/](../../../../infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/function_app.py) | Calculate + store in private blob, history, healthz | +| `host.json` | [Bicep 19 azure-function-server/](../../../../infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/host.json) | Functions host configuration | +| `requirements.txt` | [Bicep 19 azure-function-server/](../../../../infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/requirements.txt) | `azure-functions`, `azure-storage-blob` | +| `calculator_openapi.json` | [Bicep 19 azure-function-server/](../../../../infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/calculator_openapi.json) | OpenAPI 3.1 spec for the calculator API | + +## Troubleshooting + +### `403 Ip Forbidden` when used as OpenAPI tool + +Keep `publicNetworkAccess: Enabled` on the Function App. The DataProxy resolves DNS at the Foundry level, not through VNet private DNS zones. + +### `503 Application Error` after restricting storage + +If you lock down the storage account (`default_action = "Deny"`) after deployment, **restart the Function App**. The runtime needs to re-establish connections through VNet Integration. + +### Storage file share creation fails with 403 + +The Function App creation needs to create a file share. If storage has `default_action = "Deny"`, this fails. Deploy with `"Allow"` first, then restrict after the Function App is created. + +## Reference + +- [Bicep 19 Azure Function server](../../../../infrastructure-setup-bicep/19-private-network-agent-tools/azure-function-server/) — Shared function app code and Bicep template +- [Bicep 19 Testing Guide](../../../../infrastructure-setup-bicep/19-private-network-agent-tools/tests/TESTING-GUIDE.md) — End-to-end testing for tools behind VNet +- [Azure Functions VNet Integration](https://learn.microsoft.com/azure/azure-functions/functions-networking-options#virtual-network-integration) +- [App Service access restrictions](https://learn.microsoft.com/azure/app-service/app-service-ip-restrictions) diff --git a/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/tools/azure-function/example.tfvars b/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/tools/azure-function/example.tfvars new file mode 100644 index 000000000..4a29a131d --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/tools/azure-function/example.tfvars @@ -0,0 +1,15 @@ +# ============================================================================= +# Azure Function on Private VNet — Example Configuration +# +# Deploy after the base TF 19 infrastructure is up. +# Fill in the resource_group_name and vnet_name from the base deployment outputs. +# ============================================================================= + +location = "swedencentral" +resource_group_name = "rg-aifoundryXXXX" # from: terraform output resource_group_name +vnet_name = "vnet-aifoundryXXXX" # from: terraform output vnet_id (extract name) +pe_subnet_name = "pe-subnet" + +# Integration subnet for Function App outbound VNet Integration +integration_subnet_name = "func-integration-subnet" +integration_subnet_prefix = "192.168.5.0/24" diff --git a/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/tools/azure-function/main.tf b/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/tools/azure-function/main.tf new file mode 100644 index 000000000..5977690de --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/19-private-network-agent-setup-with-tools/tools/azure-function/main.tf @@ -0,0 +1,373 @@ +############################################################################## +# Azure Function App with VNet Integration and Private Endpoint +# +# Deploys a minimal Python Azure Function with VNet networking: +# - VNet Integration for outbound traffic (function can reach private resources) +# - Private Endpoint for inbound access from VNet callers +# - Private DNS zone for privatelink.azurewebsites.net +# - Storage Private Endpoints (Blob + Queue + File — required for Functions runtime) +# +# IMPORTANT: publicNetworkAccess is set to 'Enabled' because the Foundry DataProxy +# resolves DNS at the infrastructure level, not through VNet private DNS zones. +# Setting it to 'Disabled' causes 403 Ip Forbidden errors when the Function is used +# as an OpenAPI tool. Use App Service access restrictions for inbound IP filtering. +# +# Usage: +# terraform init +# terraform plan -var="vnet_name=" -var="pe_subnet_name=" -out=tfplan +# terraform apply tfplan +############################################################################## + +terraform { + required_version = ">= 1.10.0, < 2.0.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.37" + } + random = { + source = "hashicorp/random" + version = "~> 3.7" + } + } +} + +provider "azurerm" { + features {} + resource_provider_registrations = "none" +} + +############################################################################### +# Variables +############################################################################### + +variable "location" { + type = string + description = "Azure region for all resources." + default = "swedencentral" +} + +variable "resource_group_name" { + type = string + description = "Name of the existing resource group (from the base TF 19 deployment)." +} + +variable "vnet_name" { + type = string + description = "Name of the existing VNet (from the base TF 19 deployment)." +} + +variable "pe_subnet_name" { + type = string + description = "Name of the existing private endpoint subnet." + default = "pe-subnet" +} + +variable "integration_subnet_name" { + type = string + description = "Name for the Function App VNet Integration subnet (delegated to Microsoft.Web/serverFarms)." + default = "func-integration-subnet" +} + +variable "integration_subnet_prefix" { + type = string + description = "Address prefix for the integration subnet." + default = "192.168.5.0/24" +} + +variable "base_name" { + type = string + description = "Base name prefix for Function App resources. A random suffix is appended." + default = "functest" +} + +############################################################################### +# Data Sources — existing resources from the base deployment +############################################################################### + +data "azurerm_resource_group" "rg" { + name = var.resource_group_name +} + +data "azurerm_virtual_network" "vnet" { + name = var.vnet_name + resource_group_name = var.resource_group_name +} + +data "azurerm_subnet" "pe" { + name = var.pe_subnet_name + virtual_network_name = var.vnet_name + resource_group_name = var.resource_group_name +} + +resource "random_string" "suffix" { + length = 4 + lower = true + upper = false + special = false + numeric = true +} + +locals { + name_prefix = "${var.base_name}${random_string.suffix.result}" +} + +############################################################################### +# Integration Subnet (delegated to Microsoft.Web/serverFarms) +############################################################################### + +resource "azurerm_subnet" "func_integration" { + name = var.integration_subnet_name + resource_group_name = var.resource_group_name + virtual_network_name = var.vnet_name + address_prefixes = [var.integration_subnet_prefix] + + delegation { + name = "Microsoft.Web.serverFarms" + + service_delegation { + name = "Microsoft.Web/serverFarms" + actions = ["Microsoft.Network/virtualNetworks/subnets/action"] + } + } +} + +############################################################################### +# Storage Account (required by Functions runtime) +# +# NOTE: Storage must be accessible during Function App creation (file share). +# Deploy with default_action = "Allow" first, then restrict after Function App +# is created. After restricting, restart the Function App to avoid 503 errors. +############################################################################### + +resource "azurerm_storage_account" "func" { + name = "${substr(local.name_prefix, 0, 20)}stor" + resource_group_name = var.resource_group_name + location = var.location + account_tier = "Standard" + account_replication_type = "LRS" + account_kind = "StorageV2" + https_traffic_only_enabled = true + allow_nested_items_to_be_public = false + public_network_access_enabled = true + + network_rules { + default_action = "Allow" + } +} + +############################################################################### +# Storage Private Endpoints (Blob + Queue + File — required for Functions) +# +# The Functions runtime needs all three: Blob for triggers/bindings, +# Queue for internal messaging, File for the content share (WEBSITE_CONTENTSHARE). +############################################################################### + +resource "azurerm_private_endpoint" "storage_blob" { + name = "${local.name_prefix}-blob-pe" + location = var.location + resource_group_name = var.resource_group_name + subnet_id = data.azurerm_subnet.pe.id + + private_service_connection { + name = "blob" + private_connection_resource_id = azurerm_storage_account.func.id + subresource_names = ["blob"] + is_manual_connection = false + } + + private_dns_zone_group { + name = "default" + private_dns_zone_ids = [data.azurerm_private_dns_zone.storage_blob.id] + } +} + +resource "azurerm_private_endpoint" "storage_queue" { + name = "${local.name_prefix}-queue-pe" + location = var.location + resource_group_name = var.resource_group_name + subnet_id = data.azurerm_subnet.pe.id + + private_service_connection { + name = "queue" + private_connection_resource_id = azurerm_storage_account.func.id + subresource_names = ["queue"] + is_manual_connection = false + } + + private_dns_zone_group { + name = "default" + private_dns_zone_ids = [azurerm_private_dns_zone.storage_queue.id] + } +} + +resource "azurerm_private_endpoint" "storage_file" { + name = "${local.name_prefix}-file-pe" + location = var.location + resource_group_name = var.resource_group_name + subnet_id = data.azurerm_subnet.pe.id + + private_service_connection { + name = "file" + private_connection_resource_id = azurerm_storage_account.func.id + subresource_names = ["file"] + is_manual_connection = false + } + + private_dns_zone_group { + name = "default" + private_dns_zone_ids = [azurerm_private_dns_zone.storage_file.id] + } +} + +############################################################################### +# Storage File Share (required for WEBSITE_CONTENTSHARE over VNet) +############################################################################### + +resource "azurerm_storage_share" "func_content" { + name = "${local.name_prefix}-content" + storage_account_id = azurerm_storage_account.func.id + quota = 1 +} + +############################################################################### +# App Service Plan (Elastic Premium for VNet features) +############################################################################### + +resource "azurerm_service_plan" "func" { + name = "${local.name_prefix}-plan" + location = var.location + resource_group_name = var.resource_group_name + os_type = "Linux" + sku_name = "EP1" +} + +############################################################################### +# Function App +# +# publicNetworkAccess must be 'Enabled' for DataProxy compatibility. +# The DataProxy resolves DNS at the Foundry infrastructure level, not through +# VNet private DNS zones. 'Disabled' causes 403 Ip Forbidden errors. +############################################################################### + +resource "azurerm_linux_function_app" "func" { + name = "${local.name_prefix}-func" + location = var.location + resource_group_name = var.resource_group_name + service_plan_id = azurerm_service_plan.func.id + storage_account_name = azurerm_storage_account.func.name + storage_account_access_key = azurerm_storage_account.func.primary_access_key + virtual_network_subnet_id = azurerm_subnet.func_integration.id + public_network_access_enabled = true + + site_config { + application_stack { + python_version = "3.11" + } + + vnet_route_all_enabled = true + } + + app_settings = { + FUNCTIONS_WORKER_RUNTIME = "python" + FUNCTIONS_EXTENSION_VERSION = "~4" + WEBSITE_CONTENTOVERVNET = "1" + WEBSITE_VNET_ROUTE_ALL = "1" + WEBSITE_CONTENTSHARE = azurerm_storage_share.func_content.name + } +} + +############################################################################### +# Function App Private Endpoint + DNS +############################################################################### + +resource "azurerm_private_dns_zone" "azurewebsites" { + name = "privatelink.azurewebsites.net" + resource_group_name = var.resource_group_name +} + +resource "azurerm_private_dns_zone_virtual_network_link" "azurewebsites" { + name = "${var.vnet_name}-link" + resource_group_name = var.resource_group_name + private_dns_zone_name = azurerm_private_dns_zone.azurewebsites.name + virtual_network_id = data.azurerm_virtual_network.vnet.id + registration_enabled = false +} + +resource "azurerm_private_endpoint" "func" { + name = "${local.name_prefix}-func-pe" + location = var.location + resource_group_name = var.resource_group_name + subnet_id = data.azurerm_subnet.pe.id + + private_service_connection { + name = "sites" + private_connection_resource_id = azurerm_linux_function_app.func.id + subresource_names = ["sites"] + is_manual_connection = false + } + + private_dns_zone_group { + name = "default" + private_dns_zone_ids = [azurerm_private_dns_zone.azurewebsites.id] + } +} + +############################################################################### +# Storage DNS Zones +# +# Blob zone is expected to exist from the base TF 19 deployment (with VNet link). +# Queue and File zones are created here (only needed for Functions runtime). +############################################################################### + +data "azurerm_private_dns_zone" "storage_blob" { + name = "privatelink.blob.core.windows.net" + resource_group_name = var.resource_group_name +} + +resource "azurerm_private_dns_zone" "storage_queue" { + name = "privatelink.queue.core.windows.net" + resource_group_name = var.resource_group_name +} + +resource "azurerm_private_dns_zone_virtual_network_link" "storage_queue" { + name = "${var.vnet_name}-queue-link" + resource_group_name = var.resource_group_name + private_dns_zone_name = azurerm_private_dns_zone.storage_queue.name + virtual_network_id = data.azurerm_virtual_network.vnet.id + registration_enabled = false +} + +resource "azurerm_private_dns_zone" "storage_file" { + name = "privatelink.file.core.windows.net" + resource_group_name = var.resource_group_name +} + +resource "azurerm_private_dns_zone_virtual_network_link" "storage_file" { + name = "${var.vnet_name}-file-link" + resource_group_name = var.resource_group_name + private_dns_zone_name = azurerm_private_dns_zone.storage_file.name + virtual_network_id = data.azurerm_virtual_network.vnet.id + registration_enabled = false +} + +############################################################################### +# Outputs +############################################################################### + +output "function_app_name" { + value = azurerm_linux_function_app.func.name +} + +output "function_app_hostname" { + value = azurerm_linux_function_app.func.default_hostname +} + +output "function_private_endpoint_id" { + value = azurerm_private_endpoint.func.id +} + +output "function_app_resource_id" { + value = azurerm_linux_function_app.func.id +} diff --git a/samples/csharp/hosted-agents/README.md b/samples/csharp/hosted-agents/README.md index 545fb95bf..ea6d34dc6 100644 --- a/samples/csharp/hosted-agents/README.md +++ b/samples/csharp/hosted-agents/README.md @@ -222,7 +222,8 @@ azd up azd down ``` -### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open any sample directly from the extension without cloning this repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. @@ -238,6 +239,8 @@ Or, if you've already cloned this repository: The extension builds the container image in ACR (or uploads the ZIP), creates the agent version, and assigns required RBAC roles automatically. +
+ ### Other ways to invoke your agent | Method | When to use | diff --git a/samples/csharp/hosted-agents/agent-framework/README.md b/samples/csharp/hosted-agents/agent-framework/README.md index f88a9952a..a8f96ecc4 100644 --- a/samples/csharp/hosted-agents/agent-framework/README.md +++ b/samples/csharp/hosted-agents/agent-framework/README.md @@ -130,7 +130,8 @@ Or in PowerShell: (Invoke-WebRequest -Uri http://localhost:8088/responses -Method POST -ContentType "application/json" -Body '{"input": "Hello!"}').Content ``` -### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. @@ -149,6 +150,8 @@ With the agent running on `http://localhost:8088/`: 2. The Inspector auto-connects to the running agent. 3. Send messages from the Inspector to chat with the agent and watch the streamed responses. +
+ ### Using `dotnet run` #### Prerequisites diff --git a/samples/csharp/hosted-agents/agent-framework/agent-skills/README.md b/samples/csharp/hosted-agents/agent-framework/agent-skills/README.md index 604fc2c0d..ecd4350e4 100644 --- a/samples/csharp/hosted-agents/agent-framework/agent-skills/README.md +++ b/samples/csharp/hosted-agents/agent-framework/agent-skills/README.md @@ -50,7 +50,7 @@ See [Program.cs](Program.cs) for the full implementation. Before running this sample, ensure you have: -1. **Azure Developer CLI (`azd`)** (recommended) +1. **Azure Developer CLI (`azd`)** - [Install azd](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) (1.25 or later) and the unified Foundry CLI extension: `azd ext install microsoft.foundry` - Authenticated: `azd auth login` @@ -62,7 +62,7 @@ Before running this sample, ensure you have: - Download from [https://dotnet.microsoft.com/download](https://dotnet.microsoft.com/download) > [!NOTE] -> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd-recommended-for-cli-workflows) on how to target it. +> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd) on how to target it. ### Required RBAC @@ -102,15 +102,18 @@ dotnet restore ### Running the Sample -The recommended way to run and test hosted agents locally is with the Azure Developer CLI (`azd`) or the Foundry VS Code extension. +Run and test hosted agents locally with the Azure Developer CLI (`azd`) or the Foundry VS Code extension. -#### Using the Foundry VS Code Extension +
+

Using the Foundry VS Code Extension

The [Foundry VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository — it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Follow the [VS Code quickstart](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) for a full step-by-step walkthrough. -#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) (recommended for CLI workflows) +
+ +#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) No cloning required. Create a new folder, point `azd` at the manifest on GitHub, and it sets up the sample and generates Bicep infrastructure, `agent.yaml`, and env config automatically: @@ -169,7 +172,7 @@ curl -sS -X POST http://localhost:8088/responses \ Because skills are loaded on demand, the canary token in a response also proves the model actually invoked `load_skill` for the matching skill — not just saw its name in the advertised list. -#### Without `azd` +#### Manual setup If running without `azd`, set environment variables manually (see [Environment Variables](#environment-variables)), then: @@ -212,7 +215,7 @@ For the full deployment guide, see [Azure AI Foundry hosted agents](https://aka. ### Images built on Apple Silicon or other ARM64 machines do not work on our service -We **recommend deploying with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. +**Deploy with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures. diff --git a/samples/csharp/hosted-agents/agent-framework/azure-search-rag/README.md b/samples/csharp/hosted-agents/agent-framework/azure-search-rag/README.md index d23ef4865..2313c6c01 100644 --- a/samples/csharp/hosted-agents/agent-framework/azure-search-rag/README.md +++ b/samples/csharp/hosted-agents/agent-framework/azure-search-rag/README.md @@ -23,7 +23,7 @@ See [Program.cs](Program.cs) for the full implementation. Before running this sample, ensure you have: -1. **Azure Developer CLI (`azd`)** (recommended) +1. **Azure Developer CLI (`azd`)** - [Install azd](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) (1.25 or later) and the unified Foundry CLI extension: `azd ext install microsoft.foundry` - Authenticated: `azd auth login` @@ -35,7 +35,7 @@ Before running this sample, ensure you have: - Download from [https://dotnet.microsoft.com/download](https://dotnet.microsoft.com/download) > [!NOTE] -> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project, model deployment, or Azure AI Search service to get started, `azd provision` creates them all for you. If you already have some of these, see the [note below](#using-azd-recommended-for-cli-workflows) on how to target them. +> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project, model deployment, or Azure AI Search service to get started, `azd provision` creates them all for you. If you already have some of these, see the [note below](#using-azd) on how to target them. ### Environment Variables @@ -73,19 +73,22 @@ dotnet restore ### Running the Sample -The recommended way to run and test hosted agents locally is with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. +Run and test hosted agents locally with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. -#### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) (recommended for CLI workflows) +
+ +#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) No cloning required. Create a new folder, point `azd` at the manifest on GitHub, and it sets up the sample, generates Bicep infrastructure, `agent.yaml`, and env config: @@ -128,7 +131,7 @@ Tracking issue: [Azure-Samples/azd-ai-starter-basic — make storage optional in > `azd ai agent init -m /samples/csharp/hosted-agents/agent-framework/azure-search-rag/agent.manifest.yaml` > [!NOTE] -> If you already have a Foundry project, model deployment, and Azure AI Search service, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually, see [Without `azd`](#without-azd). +> If you already have a Foundry project, model deployment, and Azure AI Search service, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually, see [Manual setup](#manual-setup). The agent starts on `http://localhost:8088/`. To invoke it: @@ -152,7 +155,7 @@ curl -sS -X POST http://localhost:8088/responses \ -d '{"input": "How do I clean my tent?", "stream": false}' | jq . ``` -#### Without `azd` +#### Manual setup If running without `azd`, set environment variables manually (see [Environment Variables](#environment-variables)), then: @@ -335,7 +338,7 @@ Wait ~3 minutes for AAD propagation before invoking the agent. ### Images built on Apple Silicon or other ARM64 machines do not work on our service -We **recommend deploying with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. +**Deploy with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures. diff --git a/samples/csharp/hosted-agents/agent-framework/file-tools/README.md b/samples/csharp/hosted-agents/agent-framework/file-tools/README.md index 8f5604182..c64c26777 100644 --- a/samples/csharp/hosted-agents/agent-framework/file-tools/README.md +++ b/samples/csharp/hosted-agents/agent-framework/file-tools/README.md @@ -32,7 +32,7 @@ See [Program.cs](Program.cs) for the full implementation. Before running this sample, ensure you have: -1. **Azure Developer CLI (`azd`)** (recommended) +1. **Azure Developer CLI (`azd`)** - [Install azd](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) (1.25 or later) and the unified Foundry CLI extension: `azd ext install microsoft.foundry` - Authenticated: `azd auth login` @@ -44,7 +44,7 @@ Before running this sample, ensure you have: - Download from [https://dotnet.microsoft.com/download](https://dotnet.microsoft.com/download) > [!NOTE] -> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd-recommended-for-cli-workflows) on how to target it. +> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd) on how to target it. ### Environment Variables @@ -80,19 +80,22 @@ dotnet restore ### Running the Sample -The recommended way to run and test hosted agents locally is with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. +Run and test hosted agents locally with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. -#### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) (recommended for CLI workflows) +
+ +#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) No cloning required. Create a new folder, point `azd` at the manifest on GitHub, and it sets up the sample and generates Bicep infrastructure, `agent.yaml`, and env config automatically: @@ -116,7 +119,7 @@ azd ai agent run > `azd ai agent init -m /samples/csharp/hosted-agents/agent-framework/file-tools/agent.manifest.yaml` > [!NOTE] -> If you already have a Foundry project and model deployment, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Without `azd`](#without-azd). +> If you already have a Foundry project and model deployment, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Manual setup](#manual-setup). The agent starts on `http://localhost:8088/`. @@ -147,7 +150,7 @@ azd ai agent invoke --local "Read the file at the path '../../../etc/passwd' fro The agent's tool schema only accepts a `fileName` (no `path`), and the `Path.GetFileName` + `StartsWith(root)` defence in depth rejects anything that resolves outside the tool's root. The agent will refuse and explain that only the bundled files are available. -#### Without `azd` +#### Manual setup If running without `azd`, set environment variables manually (see [Environment Variables](#environment-variables)), then: @@ -208,7 +211,7 @@ Drop additional text files into [`resources/`](./resources/). The csproj ` [!NOTE] -> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started, `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd-recommended-for-cli-workflows) on how to target it. +> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started, `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd) on how to target it. ### Environment Variables @@ -71,19 +71,22 @@ dotnet restore ### Running the Sample -The recommended way to run and test hosted agents locally is with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. +Run and test hosted agents locally with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. -#### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) (recommended for CLI workflows) +
+ +#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) No cloning required. Create a new folder, point `azd` at the manifest on GitHub, and it sets up the sample, generates Bicep infrastructure, `agent.yaml`, and env config: @@ -131,7 +134,7 @@ curl -sS -X POST http://localhost:8088/responses \ Memory extraction is asynchronous server-side, expect a few seconds between the teaching turn and the recall turn. -#### Without `azd` +#### Manual setup If running without `azd`, set environment variables manually (see [Environment Variables](#environment-variables)), then: @@ -181,7 +184,7 @@ For the full deployment guide, see [Azure AI Foundry hosted agents](https://aka. ### Images built on Apple Silicon or other ARM64 machines do not work on our service -We **recommend deploying with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. +**Deploy with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures. diff --git a/samples/csharp/hosted-agents/agent-framework/foundry-toolbox-mcp-skills/README.md b/samples/csharp/hosted-agents/agent-framework/foundry-toolbox-mcp-skills/README.md index de75f137f..b907c2bd8 100644 --- a/samples/csharp/hosted-agents/agent-framework/foundry-toolbox-mcp-skills/README.md +++ b/samples/csharp/hosted-agents/agent-framework/foundry-toolbox-mcp-skills/README.md @@ -22,7 +22,7 @@ See [Program.cs](Program.cs) for the full implementation. Before running this sample, ensure you have: -1. **Azure Developer CLI (`azd`)** (recommended) +1. **Azure Developer CLI (`azd`)** - [Install azd](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) and the AI agent extension: `azd ext install azure.ai.agents` - Authenticated: `azd auth login` diff --git a/samples/csharp/hosted-agents/agent-framework/invocations-echo-agent/README.md b/samples/csharp/hosted-agents/agent-framework/invocations-echo-agent/README.md index 47fbb4482..b3a0a88f8 100644 --- a/samples/csharp/hosted-agents/agent-framework/invocations-echo-agent/README.md +++ b/samples/csharp/hosted-agents/agent-framework/invocations-echo-agent/README.md @@ -14,7 +14,7 @@ See [Program.cs](Program.cs) and [EchoAIAgent.cs](EchoAIAgent.cs) for the full i Before running this sample, ensure you have: -1. **Azure Developer CLI (`azd`)** (recommended) +1. **Azure Developer CLI (`azd`)** - [Install azd](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) (1.25 or later) and the unified Foundry CLI extension: `azd ext install microsoft.foundry` - Authenticated: `azd auth login` @@ -52,19 +52,22 @@ dotnet restore ### Running the Sample -The recommended way to run and test hosted agents locally is with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. +Run and test hosted agents locally with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. -#### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) (recommended for CLI workflows) +
+ +#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) No cloning required. Create a new folder, point `azd` at the manifest on GitHub, and it sets up the sample and generates Bicep infrastructure, `agent.yaml`, and env config automatically: @@ -88,7 +91,7 @@ azd ai agent run > `azd ai agent init -m /samples/csharp/hosted-agents/agent-framework/invocations-echo-agent/agent.manifest.yaml` > [!NOTE] -> If you already have a Foundry project, add `-p ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Without `azd`](#without-azd). +> If you already have a Foundry project, add `-p ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Manual setup](#manual-setup). The agent starts on `http://localhost:8088/`. To invoke it: @@ -132,7 +135,7 @@ curl -X POST "http://localhost:8088/invocations?agent_session_id=9370b9d4-cd13-4 -d '{"message": "How are you?"}' ``` -#### Without `azd` +#### Manual setup If running without `azd`, set environment variables manually if needed (see [Environment Variables](#environment-variables)), then: @@ -191,7 +194,7 @@ For the full deployment guide, see [Azure AI Foundry hosted agents](https://aka. ### Images built on Apple Silicon or other ARM64 machines do not work on our service -We **recommend deploying with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. +**Deploy with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures. diff --git a/samples/csharp/hosted-agents/agent-framework/local-tools/README.md b/samples/csharp/hosted-agents/agent-framework/local-tools/README.md index 5af430e29..06ea037a7 100644 --- a/samples/csharp/hosted-agents/agent-framework/local-tools/README.md +++ b/samples/csharp/hosted-agents/agent-framework/local-tools/README.md @@ -14,7 +14,7 @@ See [Program.cs](Program.cs) for the full implementation. Before running this sample, ensure you have: -1. **Azure Developer CLI (`azd`)** (recommended) +1. **Azure Developer CLI (`azd`)** - [Install azd](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) (1.25 or later) and the unified Foundry CLI extension: `azd ext install microsoft.foundry` - Authenticated: `azd auth login` @@ -26,7 +26,7 @@ Before running this sample, ensure you have: - Download from [https://dotnet.microsoft.com/download](https://dotnet.microsoft.com/download) > [!NOTE] -> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd-recommended-for-cli-workflows) on how to target it. +> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd) on how to target it. ### Environment Variables @@ -60,19 +60,22 @@ dotnet restore ### Running the Sample -The recommended way to run and test hosted agents locally is with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. +Run and test hosted agents locally with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. -#### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) (recommended for CLI workflows) +
+ +#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) No cloning required. Create a new folder, point `azd` at the manifest on GitHub, and it sets up the sample and generates Bicep infrastructure, `agent.yaml`, and env config automatically: @@ -96,7 +99,7 @@ azd ai agent run > `azd ai agent init -m /samples/csharp/hosted-agents/agent-framework/local-tools/agent.manifest.yaml` > [!NOTE] -> If you already have a Foundry project and model deployment, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Without `azd`](#without-azd). +> If you already have a Foundry project and model deployment, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Manual setup](#manual-setup). The agent starts on `http://localhost:8088/`. To invoke it: @@ -112,7 +115,7 @@ curl -sS -X POST http://localhost:8088/responses \ -d '{"input": "Find hotels in Seattle for Dec 20-25 under $200/night", "stream": false}' | jq . ``` -#### Without `azd` +#### Manual setup If running without `azd`, set environment variables manually (see [Environment Variables](#environment-variables)), then: @@ -165,7 +168,7 @@ For the full deployment guide, see [Azure AI Foundry hosted agents](https://aka. ### Images built on Apple Silicon or other ARM64 machines do not work on our service -We **recommend deploying with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. +**Deploy with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures. diff --git a/samples/csharp/hosted-agents/agent-framework/mcp-tools/README.md b/samples/csharp/hosted-agents/agent-framework/mcp-tools/README.md index 3aafe0c80..775150a0a 100644 --- a/samples/csharp/hosted-agents/agent-framework/mcp-tools/README.md +++ b/samples/csharp/hosted-agents/agent-framework/mcp-tools/README.md @@ -14,7 +14,7 @@ See [Program.cs](Program.cs) for the full implementation. Before running this sample, ensure you have: -1. **Azure Developer CLI (`azd`)** (recommended) +1. **Azure Developer CLI (`azd`)** - [Install azd](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) (1.25 or later) and the unified Foundry CLI extension: `azd ext install microsoft.foundry` - Authenticated: `azd auth login` @@ -26,7 +26,7 @@ Before running this sample, ensure you have: - Download from [https://dotnet.microsoft.com/download](https://dotnet.microsoft.com/download) > [!NOTE] -> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd-recommended-for-cli-workflows) on how to target it. +> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd) on how to target it. ### Environment Variables @@ -60,19 +60,22 @@ dotnet restore ### Running the Sample -The recommended way to run and test hosted agents locally is with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. +Run and test hosted agents locally with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. -#### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) (recommended for CLI workflows) +
+ +#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) No cloning required. Create a new folder, point `azd` at the manifest on GitHub, and it sets up the sample and generates Bicep infrastructure, `agent.yaml`, and env config automatically: @@ -96,7 +99,7 @@ azd ai agent run > `azd ai agent init -m /samples/csharp/hosted-agents/agent-framework/mcp-tools/agent.manifest.yaml` > [!NOTE] -> If you already have a Foundry project and model deployment, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Without `azd`](#without-azd). +> If you already have a Foundry project and model deployment, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Manual setup](#manual-setup). The agent starts on `http://localhost:8088/`. To invoke it: @@ -118,7 +121,7 @@ curl -sS -X POST http://localhost:8088/responses \ -d '{"input": "Find a C# code sample for creating an Azure Blob Storage container", "stream": false}' | jq . ``` -#### Without `azd` +#### Manual setup If running without `azd`, set environment variables manually (see [Environment Variables](#environment-variables)), then: @@ -171,7 +174,7 @@ For the full deployment guide, see [Azure AI Foundry hosted agents](https://aka. ### Images built on Apple Silicon or other ARM64 machines do not work on our service -We **recommend deploying with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. +**Deploy with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures. diff --git a/samples/csharp/hosted-agents/agent-framework/simple-agent/README.md b/samples/csharp/hosted-agents/agent-framework/simple-agent/README.md index 89ea615d8..1e698f17f 100644 --- a/samples/csharp/hosted-agents/agent-framework/simple-agent/README.md +++ b/samples/csharp/hosted-agents/agent-framework/simple-agent/README.md @@ -14,7 +14,7 @@ See [Program.cs](Program.cs) for the full implementation. Before running this sample, ensure you have: -1. **Azure Developer CLI (`azd`)** (recommended) +1. **Azure Developer CLI (`azd`)** - [Install azd](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) (1.25 or later) and the unified Foundry CLI extension: `azd ext install microsoft.foundry` - Authenticated: `azd auth login` @@ -26,7 +26,7 @@ Before running this sample, ensure you have: - Download from [https://dotnet.microsoft.com/download](https://dotnet.microsoft.com/download) > [!NOTE] -> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd-recommended-for-cli-workflows) on how to target it. +> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd) on how to target it. ### Environment Variables @@ -60,19 +60,22 @@ dotnet restore ### Running the Sample -The recommended way to run and test hosted agents locally is with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. +Run and test hosted agents locally with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. -#### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) (recommended for CLI workflows) +
+ +#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) No cloning required. Create a new folder, point `azd` at the manifest on GitHub, and it sets up the sample and generates Bicep infrastructure, `agent.yaml`, and env config automatically: @@ -96,7 +99,7 @@ azd ai agent run > `azd ai agent init -m /samples/csharp/hosted-agents/agent-framework/simple-agent/agent.manifest.yaml` > [!NOTE] -> If you already have a Foundry project and model deployment, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Without `azd`](#without-azd). +> If you already have a Foundry project and model deployment, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Manual setup](#manual-setup). The agent starts on `http://localhost:8088/`. To invoke it: @@ -112,7 +115,7 @@ curl -sS -X POST http://localhost:8088/responses \ -d '{"input": "Hello! What can you help me with?", "stream": false}' | jq . ``` -#### Without `azd` +#### Manual setup If running without `azd`, set environment variables manually (see [Environment Variables](#environment-variables)), then: @@ -165,7 +168,7 @@ For the full deployment guide, see [Azure AI Foundry hosted agents](https://aka. ### Images built on Apple Silicon or other ARM64 machines do not work on our service -We **recommend deploying with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. +**Deploy with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures. diff --git a/samples/csharp/hosted-agents/agent-framework/text-search-rag/README.md b/samples/csharp/hosted-agents/agent-framework/text-search-rag/README.md index 762727743..3138ea64f 100644 --- a/samples/csharp/hosted-agents/agent-framework/text-search-rag/README.md +++ b/samples/csharp/hosted-agents/agent-framework/text-search-rag/README.md @@ -14,7 +14,7 @@ See [Program.cs](Program.cs) for the full implementation. Before running this sample, ensure you have: -1. **Azure Developer CLI (`azd`)** (recommended) +1. **Azure Developer CLI (`azd`)** - [Install azd](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) (1.25 or later) and the unified Foundry CLI extension: `azd ext install microsoft.foundry` - Authenticated: `azd auth login` @@ -26,7 +26,7 @@ Before running this sample, ensure you have: - Download from [https://dotnet.microsoft.com/download](https://dotnet.microsoft.com/download) > [!NOTE] -> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd-recommended-for-cli-workflows) on how to target it. +> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd) on how to target it. ### Environment Variables @@ -60,19 +60,22 @@ dotnet restore ### Running the Sample -The recommended way to run and test hosted agents locally is with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. +Run and test hosted agents locally with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. -#### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) (recommended for CLI workflows) +
+ +#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) No cloning required. Create a new folder, point `azd` at the manifest on GitHub, and it sets up the sample and generates Bicep infrastructure, `agent.yaml`, and env config automatically: @@ -96,7 +99,7 @@ azd ai agent run > `azd ai agent init -m /samples/csharp/hosted-agents/agent-framework/text-search-rag/agent.manifest.yaml` > [!NOTE] -> If you already have a Foundry project and model deployment, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Without `azd`](#without-azd). +> If you already have a Foundry project and model deployment, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Manual setup](#manual-setup). The agent starts on `http://localhost:8088/`. To invoke it: @@ -120,7 +123,7 @@ curl -sS -X POST http://localhost:8088/responses \ -d '{"input": "How do I clean my tent?", "stream": false}' | jq . ``` -#### Without `azd` +#### Manual setup If running without `azd`, set environment variables manually (see [Environment Variables](#environment-variables)), then: @@ -173,7 +176,7 @@ For the full deployment guide, see [Azure AI Foundry hosted agents](https://aka. ### Images built on Apple Silicon or other ARM64 machines do not work on our service -We **recommend deploying with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. +**Deploy with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures. diff --git a/samples/csharp/hosted-agents/agent-framework/workflows/README.md b/samples/csharp/hosted-agents/agent-framework/workflows/README.md index a8784dfa4..1cc1f40e3 100644 --- a/samples/csharp/hosted-agents/agent-framework/workflows/README.md +++ b/samples/csharp/hosted-agents/agent-framework/workflows/README.md @@ -14,7 +14,7 @@ See [Program.cs](Program.cs) for the full implementation. Before running this sample, ensure you have: -1. **Azure Developer CLI (`azd`)** (recommended) +1. **Azure Developer CLI (`azd`)** - [Install azd](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) (1.25 or later) and the unified Foundry CLI extension: `azd ext install microsoft.foundry` - Authenticated: `azd auth login` @@ -26,7 +26,7 @@ Before running this sample, ensure you have: - Download from [https://dotnet.microsoft.com/download](https://dotnet.microsoft.com/download) > [!NOTE] -> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd-recommended-for-cli-workflows) on how to target it. +> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd) on how to target it. ### Environment Variables @@ -60,19 +60,22 @@ dotnet restore ### Running the Sample -The recommended way to run and test hosted agents locally is with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. +Run and test hosted agents locally with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. -#### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) (recommended for CLI workflows) +
+ +#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) No cloning required. Create a new folder, point `azd` at the manifest on GitHub, and it sets up the sample and generates Bicep infrastructure, `agent.yaml`, and env config automatically: @@ -96,7 +99,7 @@ azd ai agent run > `azd ai agent init -m /samples/csharp/hosted-agents/agent-framework/workflows/agent.manifest.yaml` > [!NOTE] -> If you already have a Foundry project and model deployment, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Without `azd`](#without-azd). +> If you already have a Foundry project and model deployment, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Manual setup](#manual-setup). The agent starts on `http://localhost:8088/`. To invoke it: @@ -114,7 +117,7 @@ curl -sS -X POST http://localhost:8088/responses \ Expected output: three lines showing the text in French, Spanish, then back in English. -#### Without `azd` +#### Manual setup If running without `azd`, set environment variables manually (see [Environment Variables](#environment-variables)), then: @@ -167,7 +170,7 @@ For the full deployment guide, see [Azure AI Foundry hosted agents](https://aka. ### Images built on Apple Silicon or other ARM64 machines do not work on our service -We **recommend deploying with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. +**Deploy with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures. diff --git a/samples/csharp/hosted-agents/bring-your-own/invocations/HelloWorld/README.md b/samples/csharp/hosted-agents/bring-your-own/invocations/HelloWorld/README.md index b33146b40..181d3aa25 100644 --- a/samples/csharp/hosted-agents/bring-your-own/invocations/HelloWorld/README.md +++ b/samples/csharp/hosted-agents/bring-your-own/invocations/HelloWorld/README.md @@ -38,7 +38,7 @@ The hosted agent can be developed and deployed to Microsoft Foundry using the [A Before running this sample, ensure you have: -1. **Azure Developer CLI (`azd`)** (recommended) +1. **Azure Developer CLI (`azd`)** - [Install azd](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) (1.25 or later) and the unified Foundry CLI extension: `azd ext install microsoft.foundry` - Authenticated: `azd auth login` @@ -50,7 +50,7 @@ Before running this sample, ensure you have: - Download from [https://dotnet.microsoft.com/download](https://dotnet.microsoft.com/download) > [!NOTE] -> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd-recommended-for-cli-workflows) on how to target it. +> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd) on how to target it. ### Environment Variables @@ -75,19 +75,22 @@ export AZURE_AI_MODEL_DEPLOYMENT_NAME="" ### Running the Sample -The recommended way to run and test hosted agents locally is with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. +Run and test hosted agents locally with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. -#### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) (recommended for CLI workflows) +
+ +#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) No cloning required. Create a new folder, point `azd` at the manifest on GitHub, and it sets up the sample and generates Bicep infrastructure, `agent.yaml`, and env config automatically: @@ -111,7 +114,7 @@ azd ai agent run > `azd ai agent init -m /samples/dotnet/hosted-agents/bring-your-own/invocations/HelloWorld/agent.manifest.yaml` > [!NOTE] -> If you already have a Foundry project and model deployment, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Without `azd`](#without-azd). +> If you already have a Foundry project and model deployment, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Manual setup](#manual-setup). The agent starts on `http://localhost:8088/`. To invoke it: @@ -138,7 +141,7 @@ curl -sS -N -X POST "http://localhost:8088/invocations?agent_session_id=chat-001 Each response is a stream of SSE events: `token` events with incremental text, followed by a `done` event with the complete reply. -#### Without `azd` +#### Manual setup If running without `azd`, set environment variables manually (see [Environment Variables](#environment-variables)), then: @@ -191,7 +194,7 @@ For the full deployment guide, see [Azure AI Foundry hosted agents](https://aka. ### Images built on Apple Silicon or other ARM64 machines do not work on our service -We **recommend deploying with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. +**Deploy with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures. diff --git a/samples/csharp/hosted-agents/bring-your-own/invocations/human-in-the-loop/README.md b/samples/csharp/hosted-agents/bring-your-own/invocations/human-in-the-loop/README.md index 0f867a947..8db9d62f9 100644 --- a/samples/csharp/hosted-agents/bring-your-own/invocations/human-in-the-loop/README.md +++ b/samples/csharp/hosted-agents/bring-your-own/invocations/human-in-the-loop/README.md @@ -56,16 +56,19 @@ The agent starts on `http://localhost:8088/`. azd ai agent run ``` -### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. +
+ ## Invoke with azd ### Local diff --git a/samples/csharp/hosted-agents/bring-your-own/invocations/notetaking-agent/README.md b/samples/csharp/hosted-agents/bring-your-own/invocations/notetaking-agent/README.md index ecfe5c545..462f0f13b 100644 --- a/samples/csharp/hosted-agents/bring-your-own/invocations/notetaking-agent/README.md +++ b/samples/csharp/hosted-agents/bring-your-own/invocations/notetaking-agent/README.md @@ -18,7 +18,7 @@ Notes are stored per session in `notes_{session_id}.jsonl` files, demonstrating - Azure CLI installed and authenticated (`az login`) - Foundry project with a deployed model (e.g., `gpt-4.1-mini`) -### Using `azd` (Recommended) +### Using `azd` ```bash azd ai agent run @@ -26,17 +26,20 @@ azd ai agent run The agent starts on `http://localhost:8088/`. -### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -### Without `azd` +
+ +### Manual setup ```bash dotnet build diff --git a/samples/csharp/hosted-agents/bring-your-own/responses/HelloWorld/README.md b/samples/csharp/hosted-agents/bring-your-own/responses/HelloWorld/README.md index d8e909738..948f8c433 100644 --- a/samples/csharp/hosted-agents/bring-your-own/responses/HelloWorld/README.md +++ b/samples/csharp/hosted-agents/bring-your-own/responses/HelloWorld/README.md @@ -36,7 +36,7 @@ The hosted agent can be developed and deployed to Microsoft Foundry using the [A Before running this sample, ensure you have: -1. **Azure Developer CLI (`azd`)** (recommended) +1. **Azure Developer CLI (`azd`)** - [Install azd](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) (1.25 or later) and the unified Foundry CLI extension: `azd ext install microsoft.foundry` - Authenticated: `azd auth login` @@ -48,7 +48,7 @@ Before running this sample, ensure you have: - Download from [https://dotnet.microsoft.com/download](https://dotnet.microsoft.com/download) > [!NOTE] -> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd-recommended-for-cli-workflows) on how to target it. +> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd) on how to target it. ### Environment Variables @@ -84,19 +84,22 @@ dotnet restore ### Running the Sample -The recommended way to run and test hosted agents locally is with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. +Run and test hosted agents locally with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. -#### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) (recommended for CLI workflows) +
+ +#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) No cloning required. Create a new folder, point `azd` at the manifest on GitHub, and it sets up the sample and generates Bicep infrastructure, `agent.yaml`, and env config automatically: @@ -120,7 +123,7 @@ azd ai agent run > `azd ai agent init -m /samples/dotnet/hosted-agents/bring-your-own/responses/HelloWorld/agent.manifest.yaml` > [!NOTE] -> If you already have a Foundry project and model deployment, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Without `azd`](#without-azd). +> If you already have a Foundry project and model deployment, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Manual setup](#manual-setup). The agent starts on `http://localhost:8088/`. To invoke it: @@ -136,7 +139,7 @@ curl -sS -X POST http://localhost:8088/responses \ -d '{"input": "What is Microsoft Foundry?", "stream": false}' | jq . ``` -#### Without `azd` +#### Manual setup If running without `azd`, set environment variables manually (see [Environment Variables](#environment-variables)), then: @@ -189,7 +192,7 @@ For the full deployment guide, see [Azure AI Foundry hosted agents](https://aka. ### Images built on Apple Silicon or other ARM64 machines do not work on our service -We **recommend deploying with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. +**Deploy with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures. diff --git a/samples/csharp/hosted-agents/bring-your-own/responses/background-agent/README.md b/samples/csharp/hosted-agents/bring-your-own/responses/background-agent/README.md index f67689f42..205d8da4d 100644 --- a/samples/csharp/hosted-agents/bring-your-own/responses/background-agent/README.md +++ b/samples/csharp/hosted-agents/bring-your-own/responses/background-agent/README.md @@ -25,7 +25,7 @@ The handler itself stays simple — background mode, polling, and cancellation a | `FOUNDRY_PROJECT_ENDPOINT` | Azure AI Foundry project endpoint (auto-injected when deployed) | | `AZURE_AI_MODEL_DEPLOYMENT_NAME` | Azure OpenAI model deployment name (e.g., `gpt-4.1-mini`) | -### Using `azd` (Recommended) +### Using `azd` ```bash azd ai agent run @@ -33,17 +33,20 @@ azd ai agent run The agent starts on `http://localhost:8088/`. -### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -### Without `azd` +
+ +### Manual setup ```bash export FOUNDRY_PROJECT_ENDPOINT="https://your-resource.services.ai.azure.com/api/projects/your-project" @@ -150,7 +153,7 @@ background-agent/ ### Images built on Apple Silicon or other ARM64 machines do not work on our service -We **recommend deploying with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. +**Deploy with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures. diff --git a/samples/csharp/hosted-agents/bring-your-own/responses/env-vars-agent/README.md b/samples/csharp/hosted-agents/bring-your-own/responses/env-vars-agent/README.md index 27d387fb2..5bdafa98d 100644 --- a/samples/csharp/hosted-agents/bring-your-own/responses/env-vars-agent/README.md +++ b/samples/csharp/hosted-agents/bring-your-own/responses/env-vars-agent/README.md @@ -56,7 +56,7 @@ Built with [Azure.AI.AgentServer.Responses](https://www.nuget.org/packages/Azure - Foundry project with a deployed model (e.g., `gpt-4.1-mini`) - (For deployment) the project's hosted-agent feature enabled, plus the two connections referenced above (`dummy-api-key` ApiKey, `dummy-custom-keys` CustomKeys) -### Using `azd` (Recommended) +### Using `azd` ```bash azd ai agent run @@ -64,7 +64,7 @@ azd ai agent run The agent starts on `http://localhost:8088/`. -### Without `azd` +### Manual setup ```bash dotnet build diff --git a/samples/csharp/hosted-agents/bring-your-own/responses/notetaking-agent/README.md b/samples/csharp/hosted-agents/bring-your-own/responses/notetaking-agent/README.md index 32bebeb84..80d8e741c 100644 --- a/samples/csharp/hosted-agents/bring-your-own/responses/notetaking-agent/README.md +++ b/samples/csharp/hosted-agents/bring-your-own/responses/notetaking-agent/README.md @@ -18,7 +18,7 @@ Notes are stored per session in `notes_{session_id}.jsonl` files, demonstrating - Azure CLI installed and authenticated (`az login`) - Foundry project with a deployed model (e.g., `gpt-4.1-mini`) -### Using `azd` (Recommended) +### Using `azd` ```bash azd ai agent run @@ -26,17 +26,20 @@ azd ai agent run The agent starts on `http://localhost:8088/`. -### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -### Without `azd` +
+ +### Manual setup ```bash dotnet build diff --git a/samples/python/hosted-agents/README.md b/samples/python/hosted-agents/README.md index 4da9e9bbe..f9ec32362 100644 --- a/samples/python/hosted-agents/README.md +++ b/samples/python/hosted-agents/README.md @@ -159,6 +159,12 @@ See [`langgraph/README.md`](langgraph/) for the full list and the local-run guid | ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | | **[Chat](langgraph/invocations/01-langgraph-chat/)** | Minimal LangGraph agent with local tools; session state via `agent_session_id` (URL param / `x-agent-session-id` header) backed by a LangGraph checkpointer. | +### Agent-to-Agent (A2A) + +| Sample | What it shows | +| ----------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| **[A2A Delegation](langgraph/a2a/)** | Two LangGraph Responses agents — a `concierge` that delegates math questions over A2A to a `math-expert` that publishes an incoming A2A endpoint + agent card, wired through a `RemoteA2A` connection and an `a2a_preview` Toolbox loaded over MCP. | + --- ## Bring Your Own Framework samples diff --git a/samples/python/hosted-agents/agent-framework/README.md b/samples/python/hosted-agents/agent-framework/README.md index 796447253..8513d9dcd 100644 --- a/samples/python/hosted-agents/agent-framework/README.md +++ b/samples/python/hosted-agents/agent-framework/README.md @@ -133,7 +133,8 @@ Or in PowerShell: (Invoke-WebRequest -Uri http://localhost:8088/responses -Method POST -ContentType "application/json" -Body '{"input": "Hello!"}').Content ``` -### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. @@ -152,6 +153,8 @@ With the agent running on `http://localhost:8088/`: 2. The Inspector auto-connects to the running agent. 3. Send messages from the Inspector to chat with the agent and watch the streamed responses. +
+ ### Using `python` #### Prerequisites diff --git a/samples/python/hosted-agents/bring-your-own/invocations/ag-ui/README.md b/samples/python/hosted-agents/bring-your-own/invocations/ag-ui/README.md index f1615c83a..c9bd14767 100644 --- a/samples/python/hosted-agents/bring-your-own/invocations/ag-ui/README.md +++ b/samples/python/hosted-agents/bring-your-own/invocations/ag-ui/README.md @@ -27,7 +27,7 @@ A minimal getting-started agent implementing the [AG-UI protocol](https://docs.a - Python 3.10+ - A Foundry project with a deployed model -### Using `azd` (Recommended) +### Using `azd` ```bash azd ai agent run @@ -35,17 +35,20 @@ azd ai agent run The agent starts on `http://localhost:8088/`. -### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -### Without `azd` +
+ +### Manual setup ```bash pip install -r requirements.txt diff --git a/samples/python/hosted-agents/bring-your-own/invocations/claude-agent-sdk/README.md b/samples/python/hosted-agents/bring-your-own/invocations/claude-agent-sdk/README.md index 49083fefe..dbb1283d6 100644 --- a/samples/python/hosted-agents/bring-your-own/invocations/claude-agent-sdk/README.md +++ b/samples/python/hosted-agents/bring-your-own/invocations/claude-agent-sdk/README.md @@ -30,7 +30,7 @@ This sample is configured for **Microsoft Foundry** mode by default - `az login` - A Foundry resource with Claude model deployments -### Using `azd` (Recommended) +### Using `azd` ```bash @@ -39,17 +39,20 @@ azd ai agent run This sample sets `ANTHROPIC_MODEL=claude-opus-4-7` in YAML, you can change the model here. -### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -### Without `azd` +
+ +### Manual setup ```bash pip install -r requirements.txt diff --git a/samples/python/hosted-agents/bring-your-own/invocations/github-copilot/README.md b/samples/python/hosted-agents/bring-your-own/invocations/github-copilot/README.md index 5b69ada92..1261889e4 100644 --- a/samples/python/hosted-agents/bring-your-own/invocations/github-copilot/README.md +++ b/samples/python/hosted-agents/bring-your-own/invocations/github-copilot/README.md @@ -40,7 +40,7 @@ Create one at [github.com/settings/personal-access-tokens/new](https://github.co > **Note:** Classic tokens (`ghp_`) are not supported. Use a fine-grained PAT (`github_pat_`), OAuth token (`gho_`), or GitHub App user token (`ghu_`). -### Using `azd` (Recommended) +### Using `azd` Create a local `.env` file from the sample template and set `GITHUB_TOKEN`: @@ -63,17 +63,20 @@ azd ai agent run The agent starts on `http://localhost:8088/`. -### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -### Without `azd` +
+ +### Manual setup ```bash pip install -r requirements.txt @@ -213,7 +216,7 @@ Instructions for Copilot when this skill is active. ### Images built on Apple Silicon or other ARM64 machines do not work on our service -We **recommend deploying with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. +**Deploy with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures. diff --git a/samples/python/hosted-agents/bring-your-own/invocations/hello-world/README.md b/samples/python/hosted-agents/bring-your-own/invocations/hello-world/README.md index a98e5efa5..7547bcf53 100644 --- a/samples/python/hosted-agents/bring-your-own/invocations/hello-world/README.md +++ b/samples/python/hosted-agents/bring-your-own/invocations/hello-world/README.md @@ -38,7 +38,7 @@ The hosted agent can be developed and deployed to Microsoft Foundry using the [A Before running this sample, ensure you have: -1. **Azure Developer CLI (`azd`)** (recommended) +1. **Azure Developer CLI (`azd`)** - [Install azd](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) (1.25 or later) and the unified Foundry CLI extension: `azd ext install microsoft.foundry` - Authenticated: `azd auth login` @@ -49,7 +49,7 @@ Before running this sample, ensure you have: - Verify your version: `python --version` > [!NOTE] -> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd-recommended-for-cli-workflows) on how to target it. +> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd) on how to target it. ### Environment Variables @@ -85,19 +85,22 @@ pip install -r requirements.txt ### Running the Sample -The recommended way to run and test hosted agents locally is with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. +Run and test hosted agents locally with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. -#### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) (recommended for CLI workflows) +
+ +#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) No cloning required. Create a new folder, point `azd` at the manifest on GitHub, and it sets up the sample and generates Bicep infrastructure, `agent.yaml`, and env config automatically: @@ -121,7 +124,7 @@ azd ai agent run > `azd ai agent init -m /samples/python/hosted-agents/bring-your-own/invocations/hello-world/agent.manifest.yaml` > [!NOTE] -> If you already have a Foundry project and model deployment, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Without `azd`](#without-azd). +> If you already have a Foundry project and model deployment, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Manual setup](#manual-setup). The agent starts on `http://localhost:8088/`. To invoke it: @@ -148,7 +151,7 @@ curl -sS -N -X POST "http://localhost:8088/invocations?agent_session_id=chat-001 Each response is a stream of SSE events: `token` events with incremental text, followed by a `done` event with the complete reply. -#### Without `azd` +#### Manual setup If running without `azd`, set environment variables manually (see [Environment Variables](#environment-variables)), then: @@ -201,7 +204,7 @@ For the full deployment guide, see [Azure AI Foundry hosted agents](https://aka. ### Images built on Apple Silicon or other ARM64 machines do not work on our service -We **recommend deploying with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. +**Deploy with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures. diff --git a/samples/python/hosted-agents/bring-your-own/invocations/human-in-the-loop/README.md b/samples/python/hosted-agents/bring-your-own/invocations/human-in-the-loop/README.md index fc8e0285e..ca6e0eee8 100644 --- a/samples/python/hosted-agents/bring-your-own/invocations/human-in-the-loop/README.md +++ b/samples/python/hosted-agents/bring-your-own/invocations/human-in-the-loop/README.md @@ -42,7 +42,7 @@ GET http://localhost:8088/invocations/docs/openapi.json - Azure CLI installed and authenticated (`az login`) - Azure OpenAI resource with a deployed model -### Using `azd` (Recommended) +### Using `azd` ```bash azd ai agent run @@ -50,17 +50,20 @@ azd ai agent run The agent starts on `http://localhost:8088/`. -### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -### Without `azd` +
+ +### Manual setup ```bash pip install -r requirements.txt diff --git a/samples/python/hosted-agents/bring-your-own/invocations/langgraph-chat/README.md b/samples/python/hosted-agents/bring-your-own/invocations/langgraph-chat/README.md index f7cd03261..4f8c105dd 100644 --- a/samples/python/hosted-agents/bring-your-own/invocations/langgraph-chat/README.md +++ b/samples/python/hosted-agents/bring-your-own/invocations/langgraph-chat/README.md @@ -41,23 +41,26 @@ and Azure OpenAI, hosted via the **invocations** protocol. ## Running locally -### Using `azd` (Recommended) +### Using `azd` ```bash azd ai agent run ``` -### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -### Without `azd` +
+ +### Manual setup ```bash cp .env.example .env # then edit values (skip if .env already exists) @@ -141,7 +144,7 @@ For the full deployment guide, see [Azure AI Foundry hosted agents](https://aka. ### Images built on Apple Silicon or other ARM64 machines do not work on our service -We **recommend deploying with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. +**Deploy with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures. diff --git a/samples/python/hosted-agents/bring-your-own/invocations/notetaking-agent/README.md b/samples/python/hosted-agents/bring-your-own/invocations/notetaking-agent/README.md index c3448f98b..c6a81bc85 100644 --- a/samples/python/hosted-agents/bring-your-own/invocations/notetaking-agent/README.md +++ b/samples/python/hosted-agents/bring-your-own/invocations/notetaking-agent/README.md @@ -25,23 +25,26 @@ A note-taking agent built with `azure-ai-agentserver-invocations` and Azure Open ## Run Locally -### Using `azd` (Recommended) +### Using `azd` ```bash azd ai agent run ``` -### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -### Without `azd` +
+ +### Manual setup ```bash # Install dependencies diff --git a/samples/python/hosted-agents/bring-your-own/invocations/toolbox/README.md b/samples/python/hosted-agents/bring-your-own/invocations/toolbox/README.md index 97c4a8514..fd87ce3ce 100644 --- a/samples/python/hosted-agents/bring-your-own/invocations/toolbox/README.md +++ b/samples/python/hosted-agents/bring-your-own/invocations/toolbox/README.md @@ -43,7 +43,7 @@ The hosted agent can be developed and deployed to Microsoft Foundry using the [A Before running this sample, ensure you have: -1. **Azure Developer CLI (`azd`)** (recommended) +1. **Azure Developer CLI (`azd`)** - [Install azd](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) (1.25 or later) and the unified Foundry CLI extension bundle: `azd ext install microsoft.foundry` (if you previously installed `azure.ai.agents` or `azure.ai.toolboxes`, run `azd ext uninstall ` first). - Authenticated: `azd auth login` @@ -103,7 +103,7 @@ Set `TOOLBOX_ENDPOINT` in `.env` for local dev, or via `azd env set TOOLBOX_ENDP ### Running the Sample -#### Using `azd` (Recommended) +#### Using `azd` ```bash azd ai agent run @@ -111,17 +111,20 @@ azd ai agent run The agent starts on `http://localhost:8088`. -#### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -#### Without `azd` +
+ +#### Manual setup ```bash cp .env.example .env # skip if .env already exists @@ -236,7 +239,7 @@ Each scenario includes a complete `agent.manifest.yaml` example with parameter d ### Images built on Apple Silicon or other ARM64 machines do not work on our service -We **recommend deploying with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. +**Deploy with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures. diff --git a/samples/python/hosted-agents/bring-your-own/responses/background-agent/README.md b/samples/python/hosted-agents/bring-your-own/responses/background-agent/README.md index 9c22b965c..c5ffc3ee6 100644 --- a/samples/python/hosted-agents/bring-your-own/responses/background-agent/README.md +++ b/samples/python/hosted-agents/bring-your-own/responses/background-agent/README.md @@ -16,7 +16,7 @@ The agent receives a request via `POST /responses` with `"background": true`. Th - Azure CLI installed and authenticated (`az login`) - Foundry project with a deployed model -### Using `azd` (Recommended) +### Using `azd` ```bash azd ai agent run @@ -24,17 +24,20 @@ azd ai agent run The agent starts on `http://localhost:8088/`. -### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -### Without `azd` +
+ +### Manual setup ```bash pip install -r requirements.txt @@ -128,7 +131,7 @@ For the full deployment guide, see [Azure AI Foundry hosted agents](https://aka. ### Images built on Apple Silicon or other ARM64 machines do not work on our service -We **recommend deploying with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. +**Deploy with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures. diff --git a/samples/python/hosted-agents/bring-your-own/responses/bring-your-own-toolbox/README.md b/samples/python/hosted-agents/bring-your-own/responses/bring-your-own-toolbox/README.md index e34c7455f..6007e279c 100644 --- a/samples/python/hosted-agents/bring-your-own/responses/bring-your-own-toolbox/README.md +++ b/samples/python/hosted-agents/bring-your-own/responses/bring-your-own-toolbox/README.md @@ -43,7 +43,7 @@ The hosted agent can be developed and deployed to Microsoft Foundry using the [A Before running this sample, ensure you have: -1. **Azure Developer CLI (`azd`)** (recommended) +1. **Azure Developer CLI (`azd`)** - [Install azd](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) (1.25 or later) and the unified Foundry CLI extension bundle: `azd ext install microsoft.foundry` (if you previously installed `azure.ai.agents` or `azure.ai.toolboxes`, run `azd ext uninstall ` first). - Authenticated: `azd auth login` @@ -103,7 +103,7 @@ Set `TOOLBOX_ENDPOINT` in `.env` for local dev, or via `azd env set TOOLBOX_ENDP ### Running the Sample -#### Using `azd` (Recommended) +#### Using `azd` ```bash azd ai agent run @@ -111,17 +111,20 @@ azd ai agent run The agent starts on `http://localhost:8088`. -#### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -#### Without `azd` +
+ +#### Manual setup ```bash cp .env.example .env # skip if .env already exists @@ -275,7 +278,7 @@ For the full deployment guide, see [Azure AI Foundry hosted agents](https://aka. ### Images built on Apple Silicon or other ARM64 machines do not work on our service -We **recommend deploying with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. +**Deploy with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures. diff --git a/samples/python/hosted-agents/bring-your-own/responses/env-vars-agent/README.md b/samples/python/hosted-agents/bring-your-own/responses/env-vars-agent/README.md index 681118314..5a9051901 100755 --- a/samples/python/hosted-agents/bring-your-own/responses/env-vars-agent/README.md +++ b/samples/python/hosted-agents/bring-your-own/responses/env-vars-agent/README.md @@ -63,7 +63,7 @@ Built with `azure-ai-agentserver-responses` (BYO — no Agent Framework). The mo - Foundry project with a deployed model (e.g., `gpt-4.1-mini`) - (For deployment) the project's hosted-agent feature enabled, plus the two connections referenced above (`dummy-api-key` ApiKey, `dummy-custom-keys` CustomKeys) -### Using `azd` (Recommended) +### Using `azd` ```bash azd ai agent run @@ -71,7 +71,7 @@ azd ai agent run The agent starts on `http://localhost:8088/`. -### Without `azd` +### Manual setup ```bash pip install -r requirements.txt diff --git a/samples/python/hosted-agents/bring-your-own/responses/hello-world/README.md b/samples/python/hosted-agents/bring-your-own/responses/hello-world/README.md index 2ac35ad67..7ce857772 100644 --- a/samples/python/hosted-agents/bring-your-own/responses/hello-world/README.md +++ b/samples/python/hosted-agents/bring-your-own/responses/hello-world/README.md @@ -36,7 +36,7 @@ The hosted agent can be developed and deployed to Microsoft Foundry using the [A Before running this sample, ensure you have: -1. **Azure Developer CLI (`azd`)** (recommended) +1. **Azure Developer CLI (`azd`)** - [Install azd](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) (1.25 or later) and the unified Foundry CLI extension: `azd ext install microsoft.foundry` - Authenticated: `azd auth login` @@ -47,7 +47,7 @@ Before running this sample, ensure you have: - Verify your version: `python --version` > [!NOTE] -> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd-recommended-for-cli-workflows) on how to target it. +> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd) on how to target it. ### Environment Variables @@ -84,19 +84,22 @@ pip install -r requirements.txt ### Running the Sample -The recommended way to run and test hosted agents locally is with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. +Run and test hosted agents locally with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. -#### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) (recommended for CLI workflows) +
+ +#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) No cloning required. Create a new folder, point `azd` at the manifest on GitHub, and it sets up the sample and generates Bicep infrastructure, `agent.yaml`, and env config automatically: @@ -120,7 +123,7 @@ azd ai agent run > `azd ai agent init -m /samples/python/hosted-agents/bring-your-own/responses/hello-world/agent.manifest.yaml` > [!NOTE] -> If you already have a Foundry project and model deployment, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Without `azd`](#without-azd). +> If you already have a Foundry project and model deployment, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Manual setup](#manual-setup). The agent starts on `http://localhost:8088/`. To invoke it: @@ -136,7 +139,7 @@ curl -sS -X POST http://localhost:8088/responses \ -d '{"input": "What is Microsoft Foundry?", "stream": false}' | jq . ``` -#### Without `azd` +#### Manual setup If running without `azd`, set environment variables manually (see [Environment Variables](#environment-variables)), then: @@ -189,7 +192,7 @@ For the full deployment guide, see [Azure AI Foundry hosted agents](https://aka. ### Images built on Apple Silicon or other ARM64 machines do not work on our service -We **recommend deploying with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. +**Deploy with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures. diff --git a/samples/python/hosted-agents/bring-your-own/responses/langgraph-chat/README.md b/samples/python/hosted-agents/bring-your-own/responses/langgraph-chat/README.md index 910a94d61..00ff2b745 100644 --- a/samples/python/hosted-agents/bring-your-own/responses/langgraph-chat/README.md +++ b/samples/python/hosted-agents/bring-your-own/responses/langgraph-chat/README.md @@ -47,23 +47,26 @@ via `previous_response_id` — no need for an in-memory session store. ## Running locally -### Using `azd` (Recommended) +### Using `azd` ```bash azd ai agent run ``` -### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -### Without `azd` +
+ +### Manual setup ```bash cp .env.example .env # then edit values (skip if .env already exists) @@ -141,7 +144,7 @@ For the full deployment guide, see [Azure AI Foundry hosted agents](https://aka. ### Images built on Apple Silicon or other ARM64 machines do not work on our service -We **recommend deploying with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. +**Deploy with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures. diff --git a/samples/python/hosted-agents/bring-your-own/responses/notetaking-agent/README.md b/samples/python/hosted-agents/bring-your-own/responses/notetaking-agent/README.md index 4ae1be3f4..d9a567ba5 100644 --- a/samples/python/hosted-agents/bring-your-own/responses/notetaking-agent/README.md +++ b/samples/python/hosted-agents/bring-your-own/responses/notetaking-agent/README.md @@ -25,23 +25,26 @@ A note-taking agent built with `azure-ai-agentserver-responses` and Azure OpenAI ## Run Locally -### Using `azd` (Recommended) +### Using `azd` ```bash azd ai agent run ``` -### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -### Without `azd` +
+ +### Manual setup ```bash # Copy and edit environment file @@ -155,7 +158,7 @@ For the full deployment guide, see [Azure AI Foundry hosted agents](https://aka. ### Images built on Apple Silicon or other ARM64 machines do not work on our service -We **recommend deploying with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. +**Deploy with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures. diff --git a/samples/python/hosted-agents/bring-your-own/responses/openai-agents-sdk/README.md b/samples/python/hosted-agents/bring-your-own/responses/openai-agents-sdk/README.md index c08abe10e..1c86a7dff 100644 --- a/samples/python/hosted-agents/bring-your-own/responses/openai-agents-sdk/README.md +++ b/samples/python/hosted-agents/bring-your-own/responses/openai-agents-sdk/README.md @@ -29,7 +29,7 @@ Authentication uses `DefaultAzureCredential` via `AIProjectClient` — the same - A Microsoft Foundry project with a model deployment (e.g. `gpt-4o-mini`) - Azure CLI logged in (`az login`) or another credential supported by `DefaultAzureCredential` -### Using `azd` (Recommended) +### Using `azd` `azd ai agent run` automatically injects `FOUNDRY_PROJECT_ENDPOINT` and starts the agent: @@ -39,17 +39,20 @@ azd ai agent run The agent starts on `http://localhost:8088/`. -### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -### Without `azd` +
+ +### Manual setup ```bash pip install -r requirements.txt @@ -181,7 +184,7 @@ Ensure you are using `curl -N` or another streaming-capable HTTP client. The age ### Images built on Apple Silicon or other ARM64 machines do not work on our service -We **recommend deploying with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. +**Deploy with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures. diff --git a/samples/python/hosted-agents/bring-your-own/voicelive/foreground-background-agents-responses-voicelive/README.md b/samples/python/hosted-agents/bring-your-own/voicelive/foreground-background-agents-responses-voicelive/README.md index 7d97768fd..8f1e61a8a 100644 --- a/samples/python/hosted-agents/bring-your-own/voicelive/foreground-background-agents-responses-voicelive/README.md +++ b/samples/python/hosted-agents/bring-your-own/voicelive/foreground-background-agents-responses-voicelive/README.md @@ -53,7 +53,7 @@ A two-agent architecture where the **Router** handles all user interaction (fast Before running this sample, ensure you have: -1. **Azure Developer CLI (`azd`)** (recommended) +1. **Azure Developer CLI (`azd`)** - [Install azd](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) and the AI agent extension: `azd ext install azure.ai.agents` - Authenticated: `azd auth login` @@ -64,7 +64,7 @@ Before running this sample, ensure you have: - Verify your version: `python --version` > [!NOTE] -> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd-recommended-for-cli-workflows) on how to target it. +> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd) on how to target it. ### Environment Variables @@ -101,15 +101,18 @@ pip install -r requirements.txt ### Running the Sample -The recommended way to run and test hosted agents locally is with the Azure Developer CLI (`azd`) or the Foundry VS Code extension. +Run and test hosted agents locally with the Azure Developer CLI (`azd`) or the Foundry VS Code extension. -#### Using the Foundry VS Code Extension +
+

Using the Foundry VS Code Extension

The [Foundry VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository — it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Follow the [VS Code quickstart](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) for a full step-by-step walkthrough. -#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) (recommended for CLI workflows) +
+ +#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) No cloning required. Create a new folder, point `azd` at the manifest on GitHub, and it sets up the sample and generates Bicep infrastructure, `agent.yaml`, and env config automatically: @@ -133,7 +136,7 @@ azd ai agent run > `azd ai agent init -m /samples/python/hosted-agents/bring-your-own/voicelive/foreground-background-agents-responses-voicelive/agent.manifest.yaml` > [!NOTE] -> If you already have a Foundry project and model deployment, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Without `azd`](#without-azd). +> If you already have a Foundry project and model deployment, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Manual setup](#manual-setup). The agent starts on `http://localhost:8088/`. To invoke it: @@ -141,7 +144,7 @@ The agent starts on `http://localhost:8088/`. To invoke it: azd ai agent invoke --local "Check the fiber repair procedure" ``` -#### Without `azd` +#### Manual setup If running without `azd`, set environment variables manually (see [Environment Variables](#environment-variables)), then: diff --git a/samples/python/hosted-agents/bring-your-own/voicelive/handoff-langgraph-responses-voicelive/README.md b/samples/python/hosted-agents/bring-your-own/voicelive/handoff-langgraph-responses-voicelive/README.md index 395fb46b5..2faddf7b2 100644 --- a/samples/python/hosted-agents/bring-your-own/voicelive/handoff-langgraph-responses-voicelive/README.md +++ b/samples/python/hosted-agents/bring-your-own/voicelive/handoff-langgraph-responses-voicelive/README.md @@ -39,7 +39,7 @@ curl -sS -N -X POST http://localhost:8088/responses \ -d '{"input": "I need a refund for order 12345", "stream": true}' ``` -## Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) (recommended for CLI workflows) +## Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) No cloning required. Create a new folder, point azd at the manifest on GitHub, and it sets up the sample and generates Bicep infrastructure, agent.yaml, and env config automatically: diff --git a/samples/python/hosted-agents/bring-your-own/voicelive/hello-world-invocations-voicelive/README.md b/samples/python/hosted-agents/bring-your-own/voicelive/hello-world-invocations-voicelive/README.md index d10cfa732..22b2b2bec 100644 --- a/samples/python/hosted-agents/bring-your-own/voicelive/hello-world-invocations-voicelive/README.md +++ b/samples/python/hosted-agents/bring-your-own/voicelive/hello-world-invocations-voicelive/README.md @@ -38,7 +38,7 @@ The hosted agent can be developed and deployed to Microsoft Foundry using the [A Before running this sample, ensure you have: -1. **Azure Developer CLI (`azd`)** (recommended) +1. **Azure Developer CLI (`azd`)** - [Install azd](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) (1.25 or later) and the unified Foundry CLI extension: `azd ext install microsoft.foundry` - Authenticated: `azd auth login` @@ -49,7 +49,7 @@ Before running this sample, ensure you have: - Verify your version: `python --version` > [!NOTE] -> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd-recommended-for-cli-workflows) on how to target it. +> You do **not** need an existing [Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-foundry?view=foundry) project or model deployment to get started — `azd provision` creates them for you. If you already have a project, see the [note below](#using-azd) on how to target it. ### Environment Variables @@ -85,19 +85,22 @@ pip install -r requirements.txt ### Running the Sample -The recommended way to run and test hosted agents locally is with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. +Run and test hosted agents locally with the Azure Developer CLI (`azd`) or the Foundry Toolkit VS Code extension. -#### Using the Foundry Toolkit VS Code Extension +
+

Using the Foundry Toolkit VS Code Extension

The [Foundry Toolkit VS Code extension](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=vscode) has a built-in sample gallery. You can open this sample directly from the extension without cloning the repository, it scaffolds the project into a new workspace, generates `agent.yaml`, `.env`, and `.vscode/tasks.json` + `launch.json` automatically, and configures a one-click **F5** debug experience. Chat with a running agent using the **Agent Inspector**: -1. Start the agent locally first using **Using `azd`** or **Without `azd`** above. The agent listens on `http://localhost:8088/`. +1. Start the agent locally first using **Using `azd`** or **Manual setup** above. The agent listens on `http://localhost:8088/`. 2. Open the Command Palette (`Ctrl+Shift+P`) and run **Foundry Toolkit: Open Agent Inspector**. 3. The Inspector auto-connects to the running agent. Send messages to chat with the agent and watch the streamed responses. -#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) (recommended for CLI workflows) +
+ +#### Using [`azd`](https://learn.microsoft.com/en-us/azure/foundry/agents/quickstarts/quickstart-hosted-agent?view=foundry&pivots=azd) No cloning required. Create a new folder, point `azd` at the manifest on GitHub, and it sets up the sample and generates Bicep infrastructure, `agent.yaml`, and env config automatically: @@ -121,7 +124,7 @@ azd ai agent run > `azd ai agent init -m /samples/python/hosted-agents/bring-your-own/voicelive/hello-world-invocations-voicelive/agent.manifest.yaml` > [!NOTE] -> If you already have a Foundry project and model deployment, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Without `azd`](#without-azd). +> If you already have a Foundry project and model deployment, add `-p -d ` to `azd ai agent init` to target existing resources. You can also skip provisioning entirely and configure env vars manually — see [Manual setup](#manual-setup). The agent starts on `http://localhost:8088/`. To invoke it: @@ -148,7 +151,7 @@ curl -sS -N -X POST "http://localhost:8088/invocations?agent_session_id=chat-001 Each response is a stream of SSE events: `output_audio_transcription.delta` events with incremental text, an `output_audio_transcription.done` event with the complete reply, followed by a `done` event. -#### Without `azd` +#### Manual setup If running without `azd`, set environment variables manually (see [Environment Variables](#environment-variables)), then: @@ -201,7 +204,7 @@ For the full deployment guide, see [Azure AI Foundry hosted agents](https://aka. ### Images built on Apple Silicon or other ARM64 machines do not work on our service -We **recommend deploying with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. +**Deploy with `azd deploy`**, which uses ACR remote build and always produces images with the correct architecture. If you choose to **build locally**, and your machine is **not `linux/amd64`** (for example, an Apple Silicon Mac), the image will **not be compatible with our service**, causing runtime failures. diff --git a/samples/python/hosted-agents/bring-your-own/voicelive/hotel-booking-invocations-voicelive/README.md b/samples/python/hosted-agents/bring-your-own/voicelive/hotel-booking-invocations-voicelive/README.md index eef5ace28..72c537aef 100644 --- a/samples/python/hosted-agents/bring-your-own/voicelive/hotel-booking-invocations-voicelive/README.md +++ b/samples/python/hosted-agents/bring-your-own/voicelive/hotel-booking-invocations-voicelive/README.md @@ -36,7 +36,7 @@ Depending on session state, the agent emits: Before running this sample, ensure you have: -1. **Azure Developer CLI (`azd`)** (recommended) +1. **Azure Developer CLI (`azd`)** - [Install azd](https://learn.microsoft.com/en-us/azure/developer/azure-developer-cli/install-azd) and the AI agent extension: `azd ext install azure.ai.agents` - Authenticated: `azd auth login` 2. **Python 3.10 or later** @@ -76,7 +76,7 @@ curl -sS -N -X POST "http://localhost:8088/invocations" \ Use the same `agent_session_id` across turns to keep conversation and booking state. -## Using `azd` (recommended) +## Using `azd` No cloning required. Create a new folder, initialize from the manifest, then provision and run: diff --git a/samples/python/hosted-agents/langgraph/README.md b/samples/python/hosted-agents/langgraph/README.md index 32c9d7729..6fbfdb11b 100644 --- a/samples/python/hosted-agents/langgraph/README.md +++ b/samples/python/hosted-agents/langgraph/README.md @@ -22,6 +22,12 @@ This directory contains samples that demonstrate how to use [LangGraph](https:// | --- | ----------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | | 1 | [Chat](invocations/01-langgraph-chat/) | A minimal LangGraph agent with two local tools, demonstrating session state via `agent_session_id` (URL param / `x-agent-session-id` response header) backed by a LangGraph checkpointer. | +### Agent-to-Agent (A2A) + +| Sample | Description | +| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [A2A delegation](a2a/) | Two LangGraph Responses agents — a `concierge` that **delegates** math questions over A2A to a `math-expert` that publishes an incoming A2A endpoint + agent card. Wired with a `RemoteA2A` connection and an `a2a_preview` Toolbox loaded over MCP. | + ## Running the Agent Host Locally ### Using `azd` diff --git a/samples/python/hosted-agents/langgraph/a2a/README.md b/samples/python/hosted-agents/langgraph/a2a/README.md new file mode 100644 index 000000000..9c5c5eb9a --- /dev/null +++ b/samples/python/hosted-agents/langgraph/a2a/README.md @@ -0,0 +1,130 @@ +# LangGraph Agent-to-Agent (A2A) on Foundry hosted agents + +A two-agent [LangGraph](https://langchain-ai.github.io/langgraph/) sample that +demonstrates the **agent-to-agent (A2A)** delegation pattern on Microsoft +Foundry hosted agents, over the **Responses protocol**. + +It shows how one hosted agent can call another. + +## The two agents + +| Folder | Agent | Role | +| --- | --- | --- | +| [`a2a-executor/`](a2a-executor/README.md) | `math-expert` | **Math expert.** A Responses agent that *also* publishes an **incoming A2A** endpoint + agent card (declared in `agent.yaml`). | +| [`a2a-caller/`](a2a-caller/README.md) | `concierge` | **Concierge.** A Responses agent that **delegates** math questions to the executor through a Foundry **Toolbox** `a2a_preview` tool loaded over MCP. | + +### How A2A is wired + +- **Executor (incoming A2A)** — the agent *code* is a plain LangGraph Responses + agent. A2A is added **declaratively** in + [`a2a-executor/agent.yaml`](a2a-executor/agent.yaml) via `agent_endpoint` + (adds the `a2a` protocol) + `agent_card` (the discovery document). `azd deploy` + applies both at agent create time — no setup script. +- **Caller (outgoing A2A)** — declares a `RemoteA2A` **connection** pointing at + the executor's A2A endpoint plus a **toolbox** with an `a2a_preview` tool, both + in [`a2a-caller/agent.manifest.yaml`](a2a-caller/agent.manifest.yaml). At + runtime the container loads the toolbox over MCP + (`langchain-mcp-adapters` → `MultiServerMCPClient`) and hands the + `math_expert` tool to LangGraph. + +## Scaffolding into an azd project + +Each agent is self-contained (its `agent.manifest.yaml` sits next to its code), +so you scaffold a fresh azd project from these manifests with +`azd ai agent init -m `. Because the caller's A2A connection must point +at the executor's *live* endpoint, the executor is **provisioned and deployed +first** — then the caller is added and pointed at the real endpoint. + +### Step 1 — Scaffold, provision, and deploy the executor + +From an **empty directory**: + +```bash +# Creates azure.yaml + infra + src/math-expert +azd ai agent init -m https://github.com/microsoft-foundry/foundry-samples/blob/main/samples/python/hosted-agents/langgraph/a2a/a2a-executor/agent.manifest.yaml + +# Provision the Foundry project and deploy the executor +cd math-expert # init roots the project in this subfolder +azd up +``` + +> [!NOTE] +> If you've already cloned this repository, pass a local path to the manifest instead: +> `azd ai agent init -m /samples/python/hosted-agents/langgraph/a2a/a2a-executor/agent.manifest.yaml` + +### Step 2 — Capture the executor's A2A endpoint + +```bash +endpoint=$(azd env get-value FOUNDRY_PROJECT_ENDPOINT) +executorA2A="$endpoint/agents/math-expert/endpoint/protocols/a2a/" +echo "$executorA2A" # copy this value +``` + +Or in PowerShell: + +```powershell +$endpoint = azd env get-value FOUNDRY_PROJECT_ENDPOINT +$executorA2A = "$endpoint/agents/math-expert/endpoint/protocols/a2a/" +$executorA2A # copy this value +``` + +### Step 3 — Add the caller and point it at the real endpoint + +```bash +# Run from inside the same project root (where azure.yaml is located) created in Step 1 +azd ai agent init -m https://github.com/microsoft-foundry/foundry-samples/blob/main/samples/python/hosted-agents/langgraph/a2a/a2a-caller/agent.manifest.yaml +``` + +At the `a2a_executor_endpoint` prompt, paste the `$executorA2A` value from +Step 2. Because the executor already exists, this is a real, working URL. + +```bash +# Deploy the caller (its connection target is already correct) +azd up +``` + +`azd ai agent init` copies each agent's source into `src//` and adds +a service to `azure.yaml`, translating the manifest `resources` (model, +connection, toolbox) into `azure.yaml` config. + +## Try it + +```bash +azd ai agent invoke concierge '{"input":"What is 15 multiplied by 23?"}' +# -> "15 multiplied by 23 is 345." (computed by the remote math expert) +``` + +Verify the executor's published A2A card: + +```bash +tok=$(az account get-access-token --resource "https://ai.azure.com" --query accessToken -o tsv) +ep=$(azd env get-value FOUNDRY_PROJECT_ENDPOINT) +curl -s -H "Authorization: Bearer $tok" \ + "$ep/agents/math-expert/endpoint/protocols/a2a/agentCard/v1.0" | jq +``` + +Or in PowerShell: + +```powershell +$tok = az account get-access-token --resource "https://ai.azure.com" --query accessToken -o tsv +$ep = azd env get-value FOUNDRY_PROJECT_ENDPOINT +Invoke-RestMethod -Uri "$ep/agents/math-expert/endpoint/protocols/a2a/agentCard/v1.0" ` + -Headers @{ Authorization = "Bearer $tok" } | ConvertTo-Json -Depth 8 +``` + +## Requirements + +- azd `azure.ai.agents` extension **>= 0.1.30** (declarative `agent_endpoint` / + `agent_card`). Validated with **0.1.37-preview**. +- A region with Foundry hosted agents + Responses (e.g. `northcentralus`). + +## A2A protocol version + +Foundry serves both A2A **v1.0** (recommended) and **v0.3** on the same base +path (`…/endpoint/protocols/a2a`); the agent card you fetch selects the version. +The executor authors its `agent_card` once and Foundry projects it into both the +v1.0 and v0.3 card shapes. This sample's caller targets **v1.0** — its connection +`AgentCardPath` and toolbox `agent_card_path` point at `agentCard/v1.0`, so the +delegation tool negotiates v1.0 end to end. To target v0.3 instead, change both +to `agentCard/v0.3`. Note that A2A v1.0 uses the **JSONRPC** transport. See +[Enable incoming A2A on a Foundry agent](https://learn.microsoft.com/en-us/azure/foundry/agents/how-to/enable-agent-to-agent-endpoint). diff --git a/samples/python/hosted-agents/langgraph/a2a/a2a-caller/.dockerignore b/samples/python/hosted-agents/langgraph/a2a/a2a-caller/.dockerignore new file mode 100644 index 000000000..b709ec79b --- /dev/null +++ b/samples/python/hosted-agents/langgraph/a2a/a2a-caller/.dockerignore @@ -0,0 +1,26 @@ +**/__pycache__/ +**/*.py[cod] +**/*.egg-info/ +.eggs/ + +# Virtual environments +.venv/ +venv/ +env/ + +# IDE settings +.vscode/ +.idea/ + +# Version control +.git/ +.gitignore + +# Docker files +.dockerignore + +# Docs +README.md + +# Local environment (never bake credentials into the image) +.env diff --git a/samples/python/hosted-agents/langgraph/a2a/a2a-caller/.env.example b/samples/python/hosted-agents/langgraph/a2a/a2a-caller/.env.example new file mode 100644 index 000000000..58ed0bb13 --- /dev/null +++ b/samples/python/hosted-agents/langgraph/a2a/a2a-caller/.env.example @@ -0,0 +1,9 @@ +# Foundry project endpoint — auto-injected in hosted containers. +# Only set manually if running without `azd ai agent run`. +FOUNDRY_PROJECT_ENDPOINT= + +# Model deployment name — must match a deployment in your Foundry project. +AZURE_AI_MODEL_DEPLOYMENT_NAME= + +# Name of the Foundry Toolbox that exposes the A2A delegation tool. +TOOLBOX_NAME="a2a-delegation-tools" diff --git a/samples/python/hosted-agents/langgraph/a2a/a2a-caller/Dockerfile b/samples/python/hosted-agents/langgraph/a2a/a2a-caller/Dockerfile new file mode 100644 index 000000000..af141eabd --- /dev/null +++ b/samples/python/hosted-agents/langgraph/a2a/a2a-caller/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.13-slim +WORKDIR /app +COPY . user_agent/ +WORKDIR /app/user_agent +RUN pip install --upgrade pip && if [ -f requirements.txt ]; then pip install -r requirements.txt; fi +EXPOSE 8088 +CMD ["python", "main.py"] diff --git a/samples/python/hosted-agents/langgraph/a2a/a2a-caller/README.md b/samples/python/hosted-agents/langgraph/a2a/a2a-caller/README.md new file mode 100644 index 000000000..42b080cc5 --- /dev/null +++ b/samples/python/hosted-agents/langgraph/a2a/a2a-caller/README.md @@ -0,0 +1,125 @@ +# LangGraph A2A Caller — Concierge (Responses) + +A [LangGraph](https://langchain-ai.github.io/langgraph/) concierge agent hosted +on Foundry over the **Responses protocol** using +[`langchain_azure_ai.agents.hosting`](https://github.com/langchain-ai/langchain-azure/tree/main/libs/azure-ai/langchain_azure_ai/agents/hosting), +that **delegates specialist questions to a remote A2A agent** — the sibling +[`a2a-executor`](../a2a-executor/README.md) math expert. + +This is the **caller** half of the A2A pair. + +## How it works + +### LangGraph agent + +The agent is built with `langchain.agents.create_agent(model, tools=[...])` and +a concierge system prompt. The LLM decides when to call the delegation tool. See +[main.py](main.py). + +### Delegation over A2A (the key part) + +Delegation tools are loaded at startup from a Foundry **Toolbox** over MCP using +[`langchain-mcp-adapters`](https://github.com/langchain-ai/langchain-mcp-adapters): + +```python +client = MultiServerMCPClient({ + "a2a-delegation": { + "transport": "streamable_http", + "url": f"{project_endpoint}/toolboxes/{toolbox_name}/mcp?api-version=v1", + "headers": {"Foundry-Features": "Toolboxes=V1Preview"}, + "auth": _ToolboxAuth(token_provider), # fresh Entra token per request + } +}) +tools = await client.get_tools() +``` + +The toolbox itself is declared **declaratively** in +[agent.manifest.yaml](agent.manifest.yaml), not in code: + +```yaml +resources: + - kind: connection + name: math-expert-a2a + category: RemoteA2A + authType: UserEntraToken + audience: https://ai.azure.com + target: "{{ a2a_executor_endpoint }}" # the executor's A2A endpoint + metadata: + AgentCardPath: /agentCard/v1.0 + - kind: toolbox + name: a2a-delegation-tools + tools: + - type: a2a_preview + name: math_expert + project_connection_id: math-expert-a2a + agent_card_path: agentCard/v1.0 +``` + +`azd` provisions the `RemoteA2A` connection + toolbox; the running container +loads the toolbox's `math_expert` tool over MCP and hands it to LangGraph. + +### Agent hosting + +The compiled graph is hosted with `ResponsesHostServer`, which exposes the +OpenAI-compatible Responses endpoint at `/responses`. Conversation state is +managed server-side by the platform via `previous_response_id`. + +## Scaffolding into an azd project + +The caller depends on the executor's *live* A2A endpoint, so the executor is +provisioned **first**, then the caller is pointed at the real endpoint. This +keeps everything within documented azd behavior — no placeholder, no two-phase +re-provision, no hook. Full steps are in the [parent README](../README.md); +in short: + +```bash +# 1. Scaffold + deploy the executor +azd ai agent init -m https://github.com/microsoft-foundry/foundry-samples/blob/main/samples/python/hosted-agents/langgraph/a2a/a2a-executor/agent.manifest.yaml +cd math-expert +azd up + +# 2. Capture its real A2A endpoint +endpoint=$(azd env get-value FOUNDRY_PROJECT_ENDPOINT) +echo "$endpoint/agents/math-expert/endpoint/protocols/a2a/" + +# 3. Add the caller; paste the value above at the a2a_executor_endpoint prompt +azd ai agent init -m https://github.com/microsoft-foundry/foundry-samples/blob/main/samples/python/hosted-agents/langgraph/a2a/a2a-caller/agent.manifest.yaml +azd up +``` + +Or in PowerShell: + +```powershell +# 1. Scaffold + deploy the executor +azd ai agent init -m https://github.com/microsoft-foundry/foundry-samples/blob/main/samples/python/hosted-agents/langgraph/a2a/a2a-executor/agent.manifest.yaml +cd math-expert +azd up + +# 2. Capture its real A2A endpoint +$endpoint = azd env get-value FOUNDRY_PROJECT_ENDPOINT +"$endpoint/agents/math-expert/endpoint/protocols/a2a/" + +# 3. Add the caller; paste the value above at the a2a_executor_endpoint prompt +azd ai agent init -m https://github.com/microsoft-foundry/foundry-samples/blob/main/samples/python/hosted-agents/langgraph/a2a/a2a-caller/agent.manifest.yaml +azd up +``` + +The caller manifest declares an `a2a_executor_endpoint` parameter. At init, +`azd ai agent init` writes whatever you type into the connection `target` in +`azure.yaml` **verbatim** — so paste the executor's *already-provisioned* +endpoint (Step 2). Because it already exists, it is a real, working URL. + +> **Why provision-first?** The executor's A2A endpoint contains the Foundry +> account name, a non-deterministic resource token that does not exist until the +> project is provisioned. azd also does **not** expand `${...}` inside +> `config.connections.target`. Standing the executor up first means the value +> you paste is real, so no placeholder or two-phase patch is needed. + +## Try it + +After both `azd up` runs complete: + +```bash +azd ai agent invoke concierge '{"input":"What is 15 multiplied by 23?"}' +# -> "15 multiplied by 23 is 345." (computed by the remote math expert) +``` diff --git a/samples/python/hosted-agents/langgraph/a2a/a2a-caller/agent.manifest.yaml b/samples/python/hosted-agents/langgraph/a2a/a2a-caller/agent.manifest.yaml new file mode 100644 index 000000000..d05e11c92 --- /dev/null +++ b/samples/python/hosted-agents/langgraph/a2a/a2a-caller/agent.manifest.yaml @@ -0,0 +1,64 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml +name: concierge +displayName: "LangGraph A2A Caller — Concierge (Responses)" +description: > + A LangGraph concierge agent hosted on Foundry over the Responses protocol that + delegates tasks to a remote Foundry-hosted A2A executor agent through a Foundry + Toolbox A2A connection, loaded over MCP via langchain-mcp-adapters. +metadata: + tags: + - AI Agent Hosting + - Responses Protocol + - LangGraph + - LangChain + - A2A + - MCP + - Python +template: + name: concierge + kind: hosted + protocols: + - protocol: responses + version: 1.0.0 + environment_variables: + # FOUNDRY_PROJECT_ENDPOINT is injected by the platform (hosted) and + # translated by azd (local) — do NOT declare it here. + # + # Model deployment name — resolved from the resources section below. + - name: AZURE_AI_MODEL_DEPLOYMENT_NAME + value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}" + # Name of the Foundry Toolbox to load delegation tools from over MCP. + - name: TOOLBOX_NAME + value: "a2a-delegation-tools" +parameters: + properties: + - name: a2a_executor_endpoint + secret: false + description: > + URL of the executor's incoming A2A endpoint. Provision and deploy the + executor FIRST (see this sample's README), then paste the value of + `azd env get-value FOUNDRY_PROJECT_ENDPOINT` with the agent path appended: + /agents/math-expert/endpoint/protocols/a2a/ + Because the executor already exists, this is a real, working URL — no + placeholder, no two-phase patch, and no hook are needed. +resources: + - kind: model + id: gpt-4.1-mini + name: AZURE_AI_MODEL_DEPLOYMENT_NAME + - kind: connection + name: math-expert-a2a + category: RemoteA2A + authType: UserEntraToken + audience: https://ai.azure.com + target: "{{ a2a_executor_endpoint }}" + metadata: + AgentCardPath: /agentCard/v1.0 + - kind: toolbox + name: a2a-delegation-tools + description: Toolbox exposing the math-expert A2A executor agent. + tools: + - type: a2a_preview + name: math_expert + description: Delegates arithmetic and math questions to the math-expert A2A agent. + project_connection_id: math-expert-a2a + agent_card_path: agentCard/v1.0 diff --git a/samples/python/hosted-agents/langgraph/a2a/a2a-caller/agent.yaml b/samples/python/hosted-agents/langgraph/a2a/a2a-caller/agent.yaml new file mode 100644 index 000000000..8da9c9576 --- /dev/null +++ b/samples/python/hosted-agents/langgraph/a2a/a2a-caller/agent.yaml @@ -0,0 +1,14 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml +kind: hosted +name: concierge +protocols: + - protocol: responses + version: 1.0.0 +resources: + cpu: "0.5" + memory: 1Gi +environment_variables: + - name: AZURE_AI_MODEL_DEPLOYMENT_NAME + value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} + - name: TOOLBOX_NAME + value: a2a-delegation-tools diff --git a/samples/python/hosted-agents/langgraph/a2a/a2a-caller/main.py b/samples/python/hosted-agents/langgraph/a2a/a2a-caller/main.py new file mode 100644 index 000000000..7414f8d2e --- /dev/null +++ b/samples/python/hosted-agents/langgraph/a2a/a2a-caller/main.py @@ -0,0 +1,119 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""LangGraph concierge agent that delegates to a remote A2A agent (Responses protocol). + +Hosts a LangGraph agent built with `langchain.agents.create_agent` on Foundry +over the Responses protocol, using +`langchain_azure_ai.agents.hosting.ResponsesHostServer`. + +Delegation tools are loaded at startup from a Foundry **Toolbox** over MCP via +`langchain_mcp_adapters.client.MultiServerMCPClient`. The toolbox (declared in +agent.manifest.yaml) exposes an `a2a_preview` tool that proxies calls to a +remote A2A-compatible agent — the sibling `a2a-executor` math expert — through a +`RemoteA2A` connection. The LLM decides when to delegate. + +Conversation state is managed server-side by the platform via +`previous_response_id` — no application-side session storage is needed. +""" + +from __future__ import annotations + +import asyncio +import os + +import httpx +from azure.identity import DefaultAzureCredential, get_bearer_token_provider +from dotenv import load_dotenv +from langchain.agents import create_agent +from langchain_core.tools import BaseTool +from langchain_mcp_adapters.client import MultiServerMCPClient + +from langchain_azure_ai.agents.hosting import ResponsesHostServer +from langchain_azure_ai.chat_models import AzureAIOpenAIApiChatModel + +load_dotenv() + +_AZURE_AI_SCOPE = "https://ai.azure.com/.default" + + +# ── Chat model ─────────────────────────────────────────────────────── +def _build_chat_model() -> AzureAIOpenAIApiChatModel: + return AzureAIOpenAIApiChatModel( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + credential=DefaultAzureCredential(), + model=os.environ.get("AZURE_AI_MODEL_DEPLOYMENT_NAME", "gpt-4o"), + ) + + +# ── Toolbox (A2A delegation) over MCP ───────────────────────────────── +class _ToolboxAuth(httpx.Auth): + """Injects a fresh Entra bearer token on every toolbox MCP request. + + The toolbox MCP endpoint is authenticated with a short-lived Entra token. + Using an `httpx.Auth` (rather than a static `Authorization` header) means + the token is re-minted per request, so long-lived agents don't fail once + the initial token expires. + """ + + def __init__(self, token_provider) -> None: + self._get_token = token_provider + + def auth_flow(self, request): + request.headers["Authorization"] = f"Bearer {self._get_token()}" + yield request + + +def _toolbox_mcp_url() -> str: + project_endpoint = os.environ["FOUNDRY_PROJECT_ENDPOINT"].rstrip("/") + toolbox_name = os.environ["TOOLBOX_NAME"] + return f"{project_endpoint}/toolboxes/{toolbox_name}/mcp?api-version=v1" + + +async def _load_toolbox_tools() -> list[BaseTool]: + credential = DefaultAzureCredential() + token_provider = get_bearer_token_provider(credential, _AZURE_AI_SCOPE) + + client = MultiServerMCPClient( + { + "a2a-delegation": { + "transport": "streamable_http", + "url": _toolbox_mcp_url(), + "headers": {"Foundry-Features": "Toolboxes=V1Preview"}, + "auth": _ToolboxAuth(token_provider), + } + } + ) + tools = await client.get_tools() + print( + f"Loaded {len(tools)} delegation tool(s) from toolbox " + f"'{os.environ['TOOLBOX_NAME']}':" + ) + for t in tools: + print(f" - {t.name}") + return tools + + +_INSTRUCTIONS = ( + "You are a friendly concierge agent. When the user asks a question that is " + "best answered by a specialist, delegate the request to the remote agent " + "exposed through the A2A delegation tool, then summarize the result back to " + "the user in a concise, friendly tone. If no remote skill is relevant, answer " + "directly." +) + + +# ── Entrypoint ─────────────────────────────────────────────────────── +def main() -> None: + tools = asyncio.run(_load_toolbox_tools()) + graph = create_agent( + _build_chat_model(), + tools=tools, + system_prompt=_INSTRUCTIONS, + ) + + port = int(os.environ.get("PORT", "8088")) + ResponsesHostServer(graph).run(port=port) + + +if __name__ == "__main__": + main() diff --git a/samples/python/hosted-agents/langgraph/a2a/a2a-caller/requirements.txt b/samples/python/hosted-agents/langgraph/a2a/a2a-caller/requirements.txt new file mode 100644 index 000000000..03fa03f85 --- /dev/null +++ b/samples/python/hosted-agents/langgraph/a2a/a2a-caller/requirements.txt @@ -0,0 +1,5 @@ +langchain-azure-ai[hosting]>=1.2.4 +langchain +langchain-mcp-adapters +httpx +python-dotenv diff --git a/samples/python/hosted-agents/langgraph/a2a/a2a-executor/.dockerignore b/samples/python/hosted-agents/langgraph/a2a/a2a-executor/.dockerignore new file mode 100644 index 000000000..b709ec79b --- /dev/null +++ b/samples/python/hosted-agents/langgraph/a2a/a2a-executor/.dockerignore @@ -0,0 +1,26 @@ +**/__pycache__/ +**/*.py[cod] +**/*.egg-info/ +.eggs/ + +# Virtual environments +.venv/ +venv/ +env/ + +# IDE settings +.vscode/ +.idea/ + +# Version control +.git/ +.gitignore + +# Docker files +.dockerignore + +# Docs +README.md + +# Local environment (never bake credentials into the image) +.env diff --git a/samples/python/hosted-agents/langgraph/a2a/a2a-executor/.env.example b/samples/python/hosted-agents/langgraph/a2a/a2a-executor/.env.example new file mode 100644 index 000000000..31a0a85fa --- /dev/null +++ b/samples/python/hosted-agents/langgraph/a2a/a2a-executor/.env.example @@ -0,0 +1,6 @@ +# Foundry project endpoint — auto-injected in hosted containers. +# Only set manually if running without `azd ai agent run`. +FOUNDRY_PROJECT_ENDPOINT= + +# Model deployment name — must match a deployment in your Foundry project. +AZURE_AI_MODEL_DEPLOYMENT_NAME= diff --git a/samples/python/hosted-agents/langgraph/a2a/a2a-executor/Dockerfile b/samples/python/hosted-agents/langgraph/a2a/a2a-executor/Dockerfile new file mode 100644 index 000000000..af141eabd --- /dev/null +++ b/samples/python/hosted-agents/langgraph/a2a/a2a-executor/Dockerfile @@ -0,0 +1,7 @@ +FROM python:3.13-slim +WORKDIR /app +COPY . user_agent/ +WORKDIR /app/user_agent +RUN pip install --upgrade pip && if [ -f requirements.txt ]; then pip install -r requirements.txt; fi +EXPOSE 8088 +CMD ["python", "main.py"] diff --git a/samples/python/hosted-agents/langgraph/a2a/a2a-executor/README.md b/samples/python/hosted-agents/langgraph/a2a/a2a-executor/README.md new file mode 100644 index 000000000..51b4455ae --- /dev/null +++ b/samples/python/hosted-agents/langgraph/a2a/a2a-executor/README.md @@ -0,0 +1,88 @@ +# LangGraph A2A Executor — Math Expert (Responses) + +A [LangGraph](https://langchain-ai.github.io/langgraph/) math-expert agent +hosted on Foundry over the **Responses protocol** using +[`langchain_azure_ai.agents.hosting`](https://github.com/langchain-ai/langchain-azure/tree/main/libs/azure-ai/langchain_azure_ai/agents/hosting), +that **also exposes an incoming A2A endpoint** so other Foundry agents can call +it agent-to-agent. + +This is the **executor** half of the A2A pair. The +[`a2a-caller`](../a2a-caller/README.md) concierge delegates math questions to +this agent through Foundry's A2A gateway. + +## How it works + +### LangGraph agent + +The agent is built with `langchain.agents.create_agent(model, tools=[...])`, +which returns a compiled LangGraph runnable implementing the standard ReAct loop. +It registers one local tool, `calculator`, and a math-expert system prompt. See +[main.py](main.py). + +### Agent hosting + +The compiled graph is hosted with `ResponsesHostServer`, which exposes the +OpenAI-compatible Responses endpoint at `/responses` and handles conversation +history, streaming lifecycle events, and tool-call surfacing automatically. +Conversation state is managed server-side by the platform via +`previous_response_id`. + +### Incoming A2A (the key part) + +The agent **code** is a plain Responses agent. A2A is added **declaratively** in +[agent.yaml](agent.yaml) / [agent.manifest.yaml](agent.manifest.yaml): + +```yaml +agent_endpoint: + protocols: + - responses + - a2a +agent_card: + description: A math expert that performs arithmetic operations and explains the steps + version: 1.0.0 + skills: + - id: arithmetic-and-math-expert + name: Arithmetic and math expert + description: Performs arithmetic operations ... + tags: [math] + examples: + - What is 15 multiplied by 23? +``` + +`azd deploy` applies both at agent **create** time (requires azd `azure.ai.agents` +extension >= 0.1.30) — no out-of-band PATCH or setup script is needed. After +deploy, the agent card is served at: + +``` +/agents/math-expert/endpoint/protocols/a2a/agentCard/v1.0 +``` + +> **A2A protocol versions.** You author `agent_card` once; Foundry serves both +> A2A **v1.0** (recommended for new integrations) and **v0.3** on the same base +> path, projecting your card into each shape. The card *version* selects the +> protocol version — fetch `agentCard/v1.0` or `agentCard/v0.3` accordingly. This +> sample's caller targets v1.0. Note: A2A v1.0 uses the **JSONRPC** transport. + +## Scaffolding into an azd project + +From an empty directory: + +```bash +azd ai agent init -m https://github.com/microsoft-foundry/foundry-samples/blob/main/samples/python/hosted-agents/langgraph/a2a/a2a-executor/agent.manifest.yaml +``` + +> [!NOTE] +> If you've already cloned this repository, pass a local path to the manifest instead: +> `azd ai agent init -m /samples/python/hosted-agents/langgraph/a2a/a2a-executor/agent.manifest.yaml` + +This copies the agent source into `src/math-expert/` and +wires it into `azure.yaml`. Add the caller next (see the +[parent README](../README.md)), then `azd up`. + +## Try it + +After `azd up` (see the [parent README](../README.md)): + +```bash +azd ai agent invoke math-expert '{"input":"What is 15 multiplied by 23?"}' +``` diff --git a/samples/python/hosted-agents/langgraph/a2a/a2a-executor/agent.manifest.yaml b/samples/python/hosted-agents/langgraph/a2a/a2a-executor/agent.manifest.yaml new file mode 100644 index 000000000..4831791ff --- /dev/null +++ b/samples/python/hosted-agents/langgraph/a2a/a2a-executor/agent.manifest.yaml @@ -0,0 +1,51 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/AgentManifest.yaml +name: math-expert +displayName: "LangGraph A2A Executor — Math Expert (Responses)" +description: > + A LangGraph math-expert agent hosted on Foundry over the Responses protocol + using langchain_azure_ai.agents.hosting. Incoming A2A is declared natively via + `agent_endpoint` + `agent_card`, so `azd deploy` turns it on at agent create + time and other Foundry agents can reach it through Foundry's A2A endpoint. +metadata: + tags: + - AI Agent Hosting + - Responses Protocol + - LangGraph + - LangChain + - A2A + - Incoming A2A + - Python +template: + name: math-expert + kind: hosted + protocols: + - protocol: responses + version: 1.0.0 + environment_variables: + # FOUNDRY_PROJECT_ENDPOINT is injected by the platform (hosted) and + # translated by azd (local) — do NOT declare it here. + # + # Model deployment name — resolved from the resources section below. + - name: AZURE_AI_MODEL_DEPLOYMENT_NAME + value: "{{AZURE_AI_MODEL_DEPLOYMENT_NAME}}" + # Incoming A2A: declared natively so `azd deploy` exposes this agent over A2A + # (in addition to Responses) and publishes the agent card at create time. + agent_endpoint: + protocols: + - responses + - a2a + agent_card: + description: A math expert that performs arithmetic operations and explains the steps + version: 1.0.0 + skills: + - id: arithmetic-and-math-expert + name: Arithmetic and math expert + description: Performs arithmetic operations (addition, subtraction, multiplication, division, exponentiation) and returns concise numeric answers. + tags: + - math + examples: + - What is 15 multiplied by 23? +resources: + - kind: model + id: gpt-4.1-mini + name: AZURE_AI_MODEL_DEPLOYMENT_NAME diff --git a/samples/python/hosted-agents/langgraph/a2a/a2a-executor/agent.yaml b/samples/python/hosted-agents/langgraph/a2a/a2a-executor/agent.yaml new file mode 100644 index 000000000..21bd02472 --- /dev/null +++ b/samples/python/hosted-agents/langgraph/a2a/a2a-executor/agent.yaml @@ -0,0 +1,30 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/microsoft/AgentSchema/refs/heads/main/schemas/v1.0/ContainerAgent.yaml +kind: hosted +name: math-expert +protocols: + - protocol: responses + version: 1.0.0 +resources: + cpu: "0.5" + memory: 1Gi +environment_variables: + - name: AZURE_AI_MODEL_DEPLOYMENT_NAME + value: ${AZURE_AI_MODEL_DEPLOYMENT_NAME} +# Incoming A2A: expose this agent over A2A (in addition to Responses) and +# publish an agent card for client discovery. `azd deploy` sets both at agent +# create time — no out-of-band PATCH required (azd ext >= 0.1.30). +agent_endpoint: + protocols: + - responses + - a2a +agent_card: + description: A math expert that performs arithmetic operations and explains the steps + version: 1.0.0 + skills: + - id: arithmetic-and-math-expert + name: Arithmetic and math expert + description: Performs arithmetic operations (addition, subtraction, multiplication, division, exponentiation) and returns concise numeric answers. + tags: + - math + examples: + - What is 15 multiplied by 23? diff --git a/samples/python/hosted-agents/langgraph/a2a/a2a-executor/main.py b/samples/python/hosted-agents/langgraph/a2a/a2a-executor/main.py new file mode 100644 index 000000000..00128a9a4 --- /dev/null +++ b/samples/python/hosted-agents/langgraph/a2a/a2a-executor/main.py @@ -0,0 +1,78 @@ +# Copyright (c) Microsoft. All rights reserved. + +"""LangGraph math-expert agent with incoming A2A (Responses protocol). + +Hosts a LangGraph agent built with `langchain.agents.create_agent` on Foundry +over the Responses protocol, using +`langchain_azure_ai.agents.hosting.ResponsesHostServer`. + +Incoming A2A is declared in agent.yaml via `agent_endpoint` + `agent_card`, so +`azd deploy` exposes this agent over A2A (in addition to Responses). Other +Foundry agents (e.g. the sibling `a2a-caller`) can then reach it through +Foundry's A2A endpoint. The agent code itself stays a plain Responses agent — +A2A is added declaratively by the platform, not in code. + +Conversation state is managed server-side by the platform via +`previous_response_id` — no application-side session storage is needed. +""" + +from __future__ import annotations + +import os +from typing import Annotated + +from azure.identity import DefaultAzureCredential +from dotenv import load_dotenv +from langchain.agents import create_agent +from langchain_core.tools import tool + +from langchain_azure_ai.agents.hosting import ResponsesHostServer +from langchain_azure_ai.chat_models import AzureAIOpenAIApiChatModel + +load_dotenv() + + +# ── Tools ──────────────────────────────────────────────────────────── +@tool +def calculator( + expression: Annotated[str, "A math expression to evaluate, e.g. '15 * 23'."], +) -> str: + """Evaluate a simple arithmetic expression and return the result.""" + try: + return str(eval(expression, {"__builtins__": {}})) # noqa: S307 + except Exception as exc: # noqa: BLE001 - surface the error to the model + return f"Error: {exc}" + + +# ── Chat model ─────────────────────────────────────────────────────── +def _build_chat_model() -> AzureAIOpenAIApiChatModel: + return AzureAIOpenAIApiChatModel( + project_endpoint=os.environ["FOUNDRY_PROJECT_ENDPOINT"], + credential=DefaultAzureCredential(), + model=os.environ.get("AZURE_AI_MODEL_DEPLOYMENT_NAME", "gpt-4o"), + ) + + +_INSTRUCTIONS = ( + "You are a math expert. When the user asks an arithmetic or algebra question, " + "use the calculator tool to compute the answer carefully, then reply with a " + "concise numeric result followed by a one-sentence explanation of the steps. " + "If the question is not math-related, politely say that you only answer math " + "questions." +) + + +# ── Entrypoint ─────────────────────────────────────────────────────── +def main() -> None: + graph = create_agent( + _build_chat_model(), + tools=[calculator], + system_prompt=_INSTRUCTIONS, + ) + + port = int(os.environ.get("PORT", "8088")) + ResponsesHostServer(graph).run(port=port) + + +if __name__ == "__main__": + main() diff --git a/samples/python/hosted-agents/langgraph/a2a/a2a-executor/requirements.txt b/samples/python/hosted-agents/langgraph/a2a/a2a-executor/requirements.txt new file mode 100644 index 000000000..8220fa0f3 --- /dev/null +++ b/samples/python/hosted-agents/langgraph/a2a/a2a-executor/requirements.txt @@ -0,0 +1,3 @@ +langchain-azure-ai[hosting]>=1.2.4 +langchain +python-dotenv