diff --git a/CHANGELOG.md b/CHANGELOG.md index e7601a673..4fd2bd605 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ * _No changes yet_ ENHANCEMENTS: -* _No changes yet_ +* Add Azure AI Foundry workspace service with support for AI Hub, AI Search, Cosmos DB, and VNet injection for Standard Agents ([#4509](https://github.com/microsoft/AzureTRE/issues/4509)) BUG FIXES: * _No changes yet_ diff --git a/core/terraform/dns_zones_non_core.tf b/core/terraform/dns_zones_non_core.tf index 0f6ee7338..2e090733f 100644 --- a/core/terraform/dns_zones_non_core.tf +++ b/core/terraform/dns_zones_non_core.tf @@ -52,6 +52,44 @@ resource "azurerm_private_dns_zone_virtual_network_link" "cognitivesearch" { lifecycle { ignore_changes = [tags] } } +# AI Foundry service DNS zones - these are not yet in the environment configuration module, +# so we define them directly rather than via the for_each non_core pattern. +resource "azurerm_private_dns_zone" "ai_services" { + name = "privatelink.services.ai.azure.com" + resource_group_name = azurerm_resource_group.core.name + tags = local.tre_core_tags + + lifecycle { ignore_changes = [tags] } +} + +resource "azurerm_private_dns_zone_virtual_network_link" "ai_services" { + resource_group_name = azurerm_resource_group.core.name + virtual_network_id = module.network.core_vnet_id + private_dns_zone_name = azurerm_private_dns_zone.ai_services.name + name = azurerm_private_dns_zone.ai_services.name + registration_enabled = false + tags = local.tre_core_tags + lifecycle { ignore_changes = [tags] } +} + +resource "azurerm_private_dns_zone" "ai_search" { + name = "privatelink.search.windows.net" + resource_group_name = azurerm_resource_group.core.name + tags = local.tre_core_tags + + lifecycle { ignore_changes = [tags] } +} + +resource "azurerm_private_dns_zone_virtual_network_link" "ai_search" { + resource_group_name = azurerm_resource_group.core.name + virtual_network_id = module.network.core_vnet_id + private_dns_zone_name = azurerm_private_dns_zone.ai_search.name + name = azurerm_private_dns_zone.ai_search.name + registration_enabled = false + tags = local.tre_core_tags + lifecycle { ignore_changes = [tags] } +} + # Once the deployment of the app gateway is complete, we can proceed to include the required DNS zone for Nexus, which is dependent on the FQDN of the app gateway. resource "azurerm_private_dns_zone" "nexus" { name = "nexus-${module.appgateway.app_gateway_fqdn}" diff --git a/templates/workspace_services/ai-foundry/Dockerfile.tmpl b/templates/workspace_services/ai-foundry/Dockerfile.tmpl new file mode 100644 index 000000000..369223b31 --- /dev/null +++ b/templates/workspace_services/ai-foundry/Dockerfile.tmpl @@ -0,0 +1,15 @@ +# syntax=docker/dockerfile-upstream:1.4.0 +FROM --platform=linux/amd64 debian:bookworm-slim + +# PORTER_INIT + +RUN rm -f /etc/apt/apt.conf.d/docker-clean; echo 'Binary::apt::APT::Keep-Downloaded-Packages "true";' > /etc/apt/apt.conf.d/keep-cache + +# Git is required for terraform init +RUN --mount=type=cache,target=/var/cache/apt --mount=type=cache,target=/var/lib/apt \ + apt-get update && apt-get install -y git jq --no-install-recommends + +# PORTER_MIXINS + +# Use the BUNDLE_DIR build argument to copy files into the bundle +COPY --link . ${BUNDLE_DIR}/ diff --git a/templates/workspace_services/ai-foundry/porter.yaml b/templates/workspace_services/ai-foundry/porter.yaml new file mode 100644 index 000000000..9096c80da --- /dev/null +++ b/templates/workspace_services/ai-foundry/porter.yaml @@ -0,0 +1,203 @@ +--- +schemaVersion: 1.0.0 +name: tre-workspace-service-ai-foundry +version: 0.2.28 +description: "Azure AI Foundry workspace service - provides AI/ML capabilities with private endpoint access" +registry: azuretre +dockerfile: Dockerfile.tmpl + +credentials: + - name: azure_tenant_id + env: ARM_TENANT_ID + - name: azure_subscription_id + env: ARM_SUBSCRIPTION_ID + - name: azure_client_id + env: ARM_CLIENT_ID + - name: azure_client_secret + env: ARM_CLIENT_SECRET + +parameters: + - name: workspace_id + type: string + - name: tre_id + type: string + + # the following are added automatically by the resource processor + - name: id + type: string + description: "Resource ID" + env: id + - name: tfstate_resource_group_name + type: string + description: "Resource group containing the Terraform state storage account" + - name: tfstate_storage_account_name + type: string + description: "The name of the Terraform state storage account" + - name: tfstate_container_name + env: tfstate_container_name + type: string + default: "tfstate" + description: "The name of the Terraform state storage container" + - name: arm_use_msi + env: ARM_USE_MSI + type: boolean + default: false + - name: arm_environment + env: ARM_ENVIRONMENT + type: string + default: "public" + + # AI Foundry specific parameters + - name: display_name + type: string + default: "Azure AI Foundry" + description: "Display name for the AI Foundry service" + - name: openai_model + type: string + default: "gpt-4o | 2024-05-13" + description: "OpenAI model to deploy" + - name: openai_model_capacity + type: integer + default: 10 + description: "Capacity for the OpenAI model deployment" + - name: is_exposed_externally + type: boolean + default: false + description: "Determines if the AI Foundry resources are accessible from outside the workspace network" + env: IS_EXPOSED_EXTERNALLY + - name: enable_ai_search + type: boolean + default: false + description: "Enable Azure AI Search for RAG and knowledge retrieval scenarios" + - name: enable_cosmos_db + type: boolean + default: false + description: "Enable Azure Cosmos DB for agent state persistence and conversation history" + - name: enable_agent_networking + type: boolean + default: false + description: "Enable VNet injection for Standard Agents (can take 30-60+ min). Deploy without this first, then upgrade to enable." + - name: address_space + type: string + description: "Address space for the AI Foundry agent subnet" + - name: workspace_owners_group_id + type: string + description: "Object ID of the workspace owners AAD group" + - name: workspace_researchers_group_id + type: string + description: "Object ID of the workspace researchers AAD group" + +mixins: + - exec + - terraform: + clientVersion: 1.14.3 + +outputs: + - name: ai_foundry_id + type: string + applyTo: + - install + - upgrade + - name: ai_foundry_name + type: string + applyTo: + - install + - upgrade + - name: connection_uri + type: string + applyTo: + - install + - upgrade + - name: workspace_address_spaces + type: string + applyTo: + - install + - upgrade + +install: + - terraform: + description: "Deploy AI Foundry workspace service" + vars: + workspace_id: ${ bundle.parameters.workspace_id } + tre_id: ${ bundle.parameters.tre_id } + tre_resource_id: ${ bundle.parameters.id } + arm_environment: ${ bundle.parameters.arm_environment } + display_name: ${ bundle.parameters.display_name } + openai_model: ${ bundle.parameters.openai_model } + openai_model_capacity: ${ bundle.parameters.openai_model_capacity } + is_exposed_externally: ${ bundle.parameters.is_exposed_externally } + enable_ai_search: ${ bundle.parameters.enable_ai_search } + enable_cosmos_db: ${ bundle.parameters.enable_cosmos_db } + enable_agent_networking: ${ bundle.parameters.enable_agent_networking } + address_space: ${ bundle.parameters.address_space } + workspace_owners_group_id: ${ bundle.parameters.workspace_owners_group_id } + workspace_researchers_group_id: ${ bundle.parameters.workspace_researchers_group_id } + backendConfig: + use_azuread_auth: "true" + use_oidc: "true" + resource_group_name: ${ bundle.parameters.tfstate_resource_group_name } + storage_account_name: ${ bundle.parameters.tfstate_storage_account_name } + container_name: ${ bundle.parameters.tfstate_container_name } + key: tre-workspace-service-ai-foundry-${ bundle.parameters.id } + outputs: + - name: ai_foundry_id + - name: ai_foundry_name + - name: connection_uri + - name: workspace_address_spaces + +upgrade: + - terraform: + description: "Upgrade AI Foundry workspace service" + vars: + workspace_id: ${ bundle.parameters.workspace_id } + tre_id: ${ bundle.parameters.tre_id } + tre_resource_id: ${ bundle.parameters.id } + arm_environment: ${ bundle.parameters.arm_environment } + display_name: ${ bundle.parameters.display_name } + openai_model: ${ bundle.parameters.openai_model } + openai_model_capacity: ${ bundle.parameters.openai_model_capacity } + is_exposed_externally: ${ bundle.parameters.is_exposed_externally } + enable_ai_search: ${ bundle.parameters.enable_ai_search } + enable_cosmos_db: ${ bundle.parameters.enable_cosmos_db } + enable_agent_networking: ${ bundle.parameters.enable_agent_networking } + address_space: ${ bundle.parameters.address_space } + workspace_owners_group_id: ${ bundle.parameters.workspace_owners_group_id } + workspace_researchers_group_id: ${ bundle.parameters.workspace_researchers_group_id } + backendConfig: + use_azuread_auth: "true" + use_oidc: "true" + resource_group_name: ${ bundle.parameters.tfstate_resource_group_name } + storage_account_name: ${ bundle.parameters.tfstate_storage_account_name } + container_name: ${ bundle.parameters.tfstate_container_name } + key: tre-workspace-service-ai-foundry-${ bundle.parameters.id } + outputs: + - name: ai_foundry_id + - name: ai_foundry_name + - name: connection_uri + - name: workspace_address_spaces + +uninstall: + - terraform: + description: "Tear down AI Foundry workspace service" + vars: + workspace_id: ${ bundle.parameters.workspace_id } + tre_id: ${ bundle.parameters.tre_id } + tre_resource_id: ${ bundle.parameters.id } + arm_environment: ${ bundle.parameters.arm_environment } + display_name: ${ bundle.parameters.display_name } + openai_model: ${ bundle.parameters.openai_model } + openai_model_capacity: ${ bundle.parameters.openai_model_capacity } + is_exposed_externally: ${ bundle.parameters.is_exposed_externally } + enable_ai_search: ${ bundle.parameters.enable_ai_search } + enable_cosmos_db: ${ bundle.parameters.enable_cosmos_db } + enable_agent_networking: ${ bundle.parameters.enable_agent_networking } + address_space: ${ bundle.parameters.address_space } + workspace_owners_group_id: ${ bundle.parameters.workspace_owners_group_id } + workspace_researchers_group_id: ${ bundle.parameters.workspace_researchers_group_id } + backendConfig: + use_azuread_auth: "true" + use_oidc: "true" + resource_group_name: ${ bundle.parameters.tfstate_resource_group_name } + storage_account_name: ${ bundle.parameters.tfstate_storage_account_name } + container_name: ${ bundle.parameters.tfstate_container_name } + key: tre-workspace-service-ai-foundry-${ bundle.parameters.id } diff --git a/templates/workspace_services/ai-foundry/template_schema.json b/templates/workspace_services/ai-foundry/template_schema.json new file mode 100644 index 000000000..8b95f7194 --- /dev/null +++ b/templates/workspace_services/ai-foundry/template_schema.json @@ -0,0 +1,351 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://github.com/microsoft/AzureTRE/templates/workspace_services/ai-foundry/template_schema.json", + "type": "object", + "title": "AI Foundry Workspace Service", + "description": "Provides Azure AI Foundry capabilities within the workspace with private endpoint access only", + "required": [], + "properties": { + "display_name": { + "type": "string", + "title": "Name for the workspace service", + "description": "The name of the workspace service to be displayed to users", + "default": "Azure AI Foundry", + "updateable": true + }, + "description": { + "type": "string", + "title": "Description of the workspace service", + "description": "Description of the workspace service", + "default": "Azure AI Foundry for building and deploying AI applications", + "updateable": true + }, + "overview": { + "type": "string", + "title": "Workspace Service Overview", + "description": "Long form description of the workspace service, in markdown syntax", + "default": "Azure AI Foundry is a comprehensive platform for developing, deploying, and managing AI applications. It provides access to a variety of AI models, including GPT-4, GPT-4o, and more. This service is deployed with private endpoints only, preventing any data exfiltration. For more information, see the [Azure AI Foundry documentation](https://learn.microsoft.com/en-us/azure/ai-studio/).", + "updateable": true + }, + "openai_model": { + "$id": "#/properties/openai_model", + "type": "string", + "title": "OpenAI Model", + "description": "Which OpenAI Model should be deployed? (be mindful of subscription limits)", + "enum": [ + "gpt-4o | 2024-05-13", + "gpt-4o | 2024-08-06", + "gpt-4o-mini | 2024-07-18", + "gpt-4 | turbo-2024-04-09", + "gpt-4 | 0613", + "gpt-35-turbo | 0125", + "gpt-35-turbo | 1106" + ], + "default": "gpt-4o | 2024-05-13", + "updateable": true + }, + "openai_model_capacity": { + "$id": "#/properties/openai_model_capacity", + "type": "integer", + "title": "Model Capacity", + "description": "The capacity (in thousands of tokens per minute) for the model deployment", + "default": 10, + "minimum": 1, + "maximum": 100, + "updateable": true + }, + "is_exposed_externally": { + "$id": "#/properties/is_exposed_externally", + "type": "boolean", + "title": "Expose externally", + "description": "Is the Azure AI Foundry accessible from outside of the workspace network. When enabled, resources are accessible via public endpoints.", + "default": false + }, + "enable_ai_search": { + "$id": "#/properties/enable_ai_search", + "type": "boolean", + "title": "Enable Azure AI Search", + "description": "Deploy Azure AI Search for RAG (Retrieval-Augmented Generation) and knowledge retrieval scenarios. Enables the AI Foundry project to search and index documents.", + "default": false, + "updateable": true + }, + "enable_cosmos_db": { + "$id": "#/properties/enable_cosmos_db", + "type": "boolean", + "title": "Enable Azure Cosmos DB", + "description": "Deploy Azure Cosmos DB for AI agent state persistence and conversation history. Required for multi-turn agent conversations and long-running agent tasks.", + "default": false, + "updateable": true + }, + "enable_agent_networking": { + "$id": "#/properties/enable_agent_networking", + "type": "boolean", + "title": "Enable Agent VNet Injection", + "description": "Enable VNet injection for Standard Agents to prevent data exfiltration. WARNING: This can take 30-60+ minutes to provision. Deploy without this first, then upgrade to enable.", + "default": false, + "updateable": true + }, + "address_space": { + "$id": "#/properties/address_space", + "type": "string", + "title": "Address space", + "description": "The address space for use by AI Foundry agent subnet" + }, + "workspace_owners_group_id": { + "$id": "#/properties/workspace_owners_group_id", + "type": "string", + "title": "Workspace Owners Group ID", + "description": "Object ID of the workspace owners AAD group" + }, + "workspace_researchers_group_id": { + "$id": "#/properties/workspace_researchers_group_id", + "type": "string", + "title": "Workspace Researchers Group ID", + "description": "Object ID of the workspace researchers AAD group" + } + }, + "uiSchema": { + "address_space": { + "classNames": "tre-hidden" + }, + "workspace_owners_group_id": { + "classNames": "tre-hidden" + }, + "workspace_researchers_group_id": { + "classNames": "tre-hidden" + }, + "openai_model": { + "ui:widget": "radio" + }, + "openai_model_capacity": { + "ui:widget": "updown" + }, + "is_exposed_externally": { + "ui:widget": "checkbox" + }, + "enable_ai_search": { + "ui:widget": "checkbox" + }, + "enable_cosmos_db": { + "ui:widget": "checkbox" + }, + "enable_agent_networking": { + "ui:widget": "checkbox" + } + }, + "pipeline": { + "install": [ + { + "stepId": "aif-ws-upgrade", + "stepTitle": "Upgrade workspace to track address space allocation", + "resourceType": "workspace", + "resourceAction": "upgrade", + "properties": [] + }, + { + "stepId": "main", + "properties": [ + { + "name": "workspace_owners_group_id", + "type": "string", + "value": "{{ resource.parent.properties.workspace_owners_group_id }}" + }, + { + "name": "workspace_researchers_group_id", + "type": "string", + "value": "{{ resource.parent.properties.workspace_researchers_group_id }}" + } + ] + }, + { + "stepId": "aif-firewall-rules", + "stepTitle": "Add network firewall rules for AI Foundry", + "resourceTemplateName": "tre-shared-service-firewall", + "resourceType": "shared-service", + "resourceAction": "upgrade", + "properties": [ + { + "name": "network_rule_collections", + "type": "array", + "arraySubstitutionAction": "replace", + "arrayMatchField": "name", + "value": { + "name": "nrc_svc_{{ resource.id }}_aifoundry", + "action": "Allow", + "rules": [ + { + "name": "AIFoundry_Authentication", + "description": "AI Foundry Authentication and Portal Access", + "source_addresses": "{{ resource.properties.workspace_address_spaces }}", + "destination_addresses": [ + "AzureActiveDirectory", + "AzureResourceManager" + ], + "destination_ports": [ + "443" + ], + "protocols": [ + "TCP" + ] + } + ] + } + }, + { + "name": "rule_collections", + "type": "array", + "arraySubstitutionAction": "replace", + "arrayMatchField": "name", + "value": { + "name": "arc_svc_{{ resource.id }}_aifoundry", + "action": "Allow", + "rules": [ + { + "name": "AIFoundry_Portal", + "description": "AI Foundry Portal Access", + "source_addresses": "{{ resource.properties.workspace_address_spaces }}", + "target_fqdns": [ + "ai.azure.com", + "*.aadcdn.msftauth.net", + "aadcdn.msftauth.net", + "*.aadcdn.msauth.net", + "aadcdn.msauth.net", + "*.aadcdn.msauthimages.net", + "aadcdn.msauthimages.net", + "*.msftauth.net", + "*.msauth.net", + "login.microsoft.com", + "*.login.microsoft.com", + "login.microsoftonline.com", + "*.login.microsoftonline.com", + "login.live.com", + "*.login.live.com" + ], + "protocols": [ + { + "port": "443", + "type": "Https" + } + ] + } + ] + } + } + ] + } + ], + "upgrade": [ + { + "stepId": "main" + }, + { + "stepId": "aif-firewall-rules-upgrade", + "stepTitle": "Update network firewall rules for AI Foundry", + "resourceTemplateName": "tre-shared-service-firewall", + "resourceType": "shared-service", + "resourceAction": "upgrade", + "properties": [ + { + "name": "network_rule_collections", + "type": "array", + "arraySubstitutionAction": "replace", + "arrayMatchField": "name", + "value": { + "name": "nrc_svc_{{ resource.id }}_aifoundry", + "action": "Allow", + "rules": [ + { + "name": "AIFoundry_Authentication", + "description": "AI Foundry Authentication and Portal Access", + "source_addresses": "{{ resource.properties.workspace_address_spaces }}", + "destination_addresses": [ + "AzureActiveDirectory", + "AzureResourceManager" + ], + "destination_ports": [ + "443" + ], + "protocols": [ + "TCP" + ] + } + ] + } + }, + { + "name": "rule_collections", + "type": "array", + "arraySubstitutionAction": "replace", + "arrayMatchField": "name", + "value": { + "name": "arc_svc_{{ resource.id }}_aifoundry", + "action": "Allow", + "rules": [ + { + "name": "AIFoundry_Portal", + "description": "AI Foundry Portal Access", + "source_addresses": "{{ resource.properties.workspace_address_spaces }}", + "target_fqdns": [ + "ai.azure.com", + "*.aadcdn.msftauth.net", + "aadcdn.msftauth.net", + "*.aadcdn.msauth.net", + "aadcdn.msauth.net", + "*.aadcdn.msauthimages.net", + "aadcdn.msauthimages.net", + "*.msftauth.net", + "*.msauth.net", + "login.microsoft.com", + "*.login.microsoft.com", + "login.microsoftonline.com", + "*.login.microsoftonline.com", + "login.live.com", + "*.login.live.com" + ], + "protocols": [ + { + "port": "443", + "type": "Https" + } + ] + } + ] + } + } + ] + } + ], + "uninstall": [ + { + "stepId": "aif-firewall-rules-remove", + "stepTitle": "Remove network firewall rules for AI Foundry", + "resourceTemplateName": "tre-shared-service-firewall", + "resourceType": "shared-service", + "resourceAction": "upgrade", + "properties": [ + { + "name": "network_rule_collections", + "type": "array", + "arraySubstitutionAction": "remove", + "arrayMatchField": "name", + "value": { + "name": "nrc_svc_{{ resource.id }}_aifoundry" + } + }, + { + "name": "rule_collections", + "type": "array", + "arraySubstitutionAction": "remove", + "arrayMatchField": "name", + "value": { + "name": "arc_svc_{{ resource.id }}_aifoundry" + } + } + ] + }, + { + "stepId": "main" + } + ] + } +} diff --git a/templates/workspace_services/ai-foundry/terraform/.terraform.lock.hcl b/templates/workspace_services/ai-foundry/terraform/.terraform.lock.hcl new file mode 100644 index 000000000..bb26e09aa --- /dev/null +++ b/templates/workspace_services/ai-foundry/terraform/.terraform.lock.hcl @@ -0,0 +1,102 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/azure/azapi" { + version = "2.8.0" + constraints = "~> 2.8" + hashes = [ + "h1:T1HpMzwCr3VVt5rCJ2ntcZ75TXDB9boby48c4DPWi70=", + "zh:048fa67ba123c6da65a7af12453328e36e1783cac1dbecc905d44ee7a1daa41c", + "zh:08dfb8c493a99aa54ea0c00f5d2e2389aac55d70b31bfc50a38e4ab61800aca8", + "zh:0d5bf53f356864567bf0855eb90b0b9aa4619b60fd1469210461ad88c0508a6f", + "zh:221cc52181d81bd741e8624ba9619ae20438f7a13828b72aa138a51b57bc1483", + "zh:51e7485e4f502cbbefe9b4ea991961eb9b19f41862593150905197bbb37cc6fb", + "zh:6e2d0986176bbeabdfa7dc3d1bf37d0a24549ebff29a3c9e8c5082e03cc38247", + "zh:87e46ceddcd3a4b7ed16f6b853c286840753d8af8ae8df0618ab5f29e950976b", + "zh:894998419943fadb3b85d1469665e9b7cdf492e6dc30907a77e32043e1d52b6a", + "zh:9f1efae3ad37510d947e7a27118a84bae55e35681b047d939781da96dd6ab6c7", + "zh:a201371f6c4c65b6976a8a360223c188ea91b7a33078fdd3a5f5f0ac7b438d35", + "zh:af3cc16bdfc545e61ce66449b9daaebfaa0c5e495777241c9414671a31e37ffa", + "zh:dbbb263a5f4c40624823fd3e68dc046b1f00325548393557384f0914a4694278", + ] +} + +provider "registry.terraform.io/hashicorp/azurerm" { + version = "4.58.0" + constraints = "~> 4.58" + hashes = [ + "h1:MESGDsXHZy6+1pCObte7/WSXaIzH9FUFy/Vp8MPVxiI=", + "zh:041c2a778ab4dd5a9af174b1d6f75409e5aabfc359cb386dfea3fb09e3f32709", + "zh:0a302531a61e7383acf99a6202d7984b2ea559306f45021381665c827a830d46", + "zh:0c69f132c7609683d907e87b89210a298d84c5b0121b62278949931bc54ca952", + "zh:0cadf48e9d2d9daed43212a3c9d886d7faaf68787b6e955456cbe4f43e4a17ec", + "zh:35ef4293d7731f6ff1f8bcba2c4529f987b7fac243c1ac1c154bbc02c9703c25", + "zh:3cb2679e1d56865e0ee0cf4c5d1404dbad0db42d11425e7bf0580a026cc64287", + "zh:4e56411f5119042d4962acff5c6d64224a49a69154ba80e6df63fa57b1e6d284", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:ca4626411a111720c220f9849c7d2e1fcd5d380f56459e096d835a9dbf9e6e13", + "zh:d31c4e65dcb096974479b2d548fffb86fc9a5262aff1b01fe62ef442ce536c6b", + "zh:d9631602999c1853e53ee2c5aef7476e23c7787beddc3599c10dbaa4891ba166", + "zh:f31ba7c9341037ceb7d49467946c01b2b0930404ed1d5643c1451f734a613a03", + ] +} + +provider "registry.terraform.io/hashicorp/null" { + version = "3.2.4" + constraints = "~> 3.2" + hashes = [ + "h1:hkf5w5B6q8e2A42ND2CjAvgvSN3puAosDmOJb3zCVQM=", + "zh:59f6b52ab4ff35739647f9509ee6d93d7c032985d9f8c6237d1f8a59471bbbe2", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:795c897119ff082133150121d39ff26cb5f89a730a2c8c26f3a9c1abf81a9c43", + "zh:7b9c7b16f118fbc2b05a983817b8ce2f86df125857966ad356353baf4bff5c0a", + "zh:85e33ab43e0e1726e5f97a874b8e24820b6565ff8076523cc2922ba671492991", + "zh:9d32ac3619cfc93eb3c4f423492a8e0f79db05fec58e449dee9b2d5873d5f69f", + "zh:9e15c3c9dd8e0d1e3731841d44c34571b6c97f5b95e8296a45318b94e5287a6e", + "zh:b4c2ab35d1b7696c30b64bf2c0f3a62329107bd1a9121ce70683dec58af19615", + "zh:c43723e8cc65bcdf5e0c92581dcbbdcbdcf18b8d2037406a5f2033b1e22de442", + "zh:ceb5495d9c31bfb299d246ab333f08c7fb0d67a4f82681fbf47f2a21c3e11ab5", + "zh:e171026b3659305c558d9804062762d168f50ba02b88b231d20ec99578a6233f", + "zh:ed0fe2acdb61330b01841fa790be00ec6beaac91d41f311fb8254f74eb6a711f", + ] +} + +provider "registry.terraform.io/hashicorp/random" { + version = "3.8.1" + constraints = "~> 3.8" + hashes = [ + "h1:Eexl06+6J+s75uD46+WnZtpJZYRVUMB0AiuPBifK6Jc=", + "zh:08dd03b918c7b55713026037c5400c48af5b9f468f483463321bd18e17b907b4", + "zh:0eee654a5542dc1d41920bbf2419032d6f0d5625b03bd81339e5b33394a3e0ae", + "zh:229665ddf060aa0ed315597908483eee5b818a17d09b6417a0f52fd9405c4f57", + "zh:2469d2e48f28076254a2a3fc327f184914566d9e40c5780b8d96ebf7205f8bc0", + "zh:37d7eb334d9561f335e748280f5535a384a88675af9a9eac439d4cfd663bcb66", + "zh:741101426a2f2c52dee37122f0f4a2f2d6af6d852cb1db634480a86398fa3511", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:a902473f08ef8df62cfe6116bd6c157070a93f66622384300de235a533e9d4a9", + "zh:b85c511a23e57a2147355932b3b6dce2a11e856b941165793a0c3d7578d94d05", + "zh:c5172226d18eaac95b1daac80172287b69d4ce32750c82ad77fa0768be4ea4b8", + "zh:dab4434dba34aad569b0bc243c2d3f3ff86dd7740def373f2a49816bd2ff819b", + "zh:f49fd62aa8c5525a5c17abd51e27ca5e213881d58882fd42fec4a545b53c9699", + ] +} + +provider "registry.terraform.io/hashicorp/time" { + version = "0.13.1" + constraints = "~> 0.13" + hashes = [ + "h1:+W+DMrVoVnoXo3f3M4W+OpZbkCrUn6PnqDF33D2Cuf0=", + "zh:02cb9aab1002f0f2a94a4f85acec8893297dc75915f7404c165983f720a54b74", + "zh:04429b2b31a492d19e5ecf999b116d396dac0b24bba0d0fb19ecaefe193fdb8f", + "zh:26f8e51bb7c275c404ba6028c1b530312066009194db721a8427a7bc5cdbc83a", + "zh:772ff8dbdbef968651ab3ae76d04afd355c32f8a868d03244db3f8496e462690", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:898db5d2b6bd6ca5457dccb52eedbc7c5b1a71e4a4658381bcbb38cedbbda328", + "zh:8de913bf09a3fa7bedc29fec18c47c571d0c7a3d0644322c46f3aa648cf30cd8", + "zh:9402102c86a87bdfe7e501ffbb9c685c32bbcefcfcf897fd7d53df414c36877b", + "zh:b18b9bb1726bb8cfbefc0a29cf3657c82578001f514bcf4c079839b6776c47f0", + "zh:b9d31fdc4faecb909d7c5ce41d2479dd0536862a963df434be4b16e8e4edc94d", + "zh:c951e9f39cca3446c060bd63933ebb89cedde9523904813973fbc3d11863ba75", + "zh:e5b773c0d07e962291be0e9b413c7a22c044b8c7b58c76e8aa91d1659990dfb5", + ] +} diff --git a/templates/workspace_services/ai-foundry/terraform/ai_foundry.tf b/templates/workspace_services/ai-foundry/terraform/ai_foundry.tf new file mode 100644 index 000000000..91e1e7a70 --- /dev/null +++ b/templates/workspace_services/ai-foundry/terraform/ai_foundry.tf @@ -0,0 +1,199 @@ +# AI Foundry (AIServices) Account +# Using azurerm_cognitive_account which properly handles LRO and waits for provisioningState=Succeeded + +resource "random_string" "suffix" { + length = 5 + lower = true + upper = false + numeric = true + special = false +} + +# Create AI Foundry account using azurerm provider +# azurerm properly waits for account provisioning to complete before returning +resource "azurerm_cognitive_account" "ai_foundry" { + name = "aif-${local.service_resource_name_suffix}" + location = data.azurerm_resource_group.ws.location + resource_group_name = data.azurerm_resource_group.ws.name + kind = "AIServices" + sku_name = "S0" + custom_subdomain_name = "aif-${local.service_resource_name_suffix}" + public_network_access_enabled = var.is_exposed_externally + local_auth_enabled = true + project_management_enabled = true + + identity { + type = "SystemAssigned" + } + + network_acls { + default_action = "Allow" + } + + # Network injection for agent networking (optional) + dynamic "network_injection" { + for_each = var.enable_agent_networking ? [1] : [] + content { + scenario = "agent" + subnet_id = azurerm_subnet.agents.id + } + } + + tags = local.workspace_service_tags + + timeouts { + create = "60m" + update = "60m" + delete = "30m" + read = "5m" + } + + lifecycle { + ignore_changes = [tags] + } +} + +# Wait for AIServices to fully provision internally +# The azurerm provider returns before Azure has fully completed internal provisioning +# for AIServices with project_management_enabled=true +resource "time_sleep" "wait_for_ai_foundry" { + depends_on = [azurerm_cognitive_account.ai_foundry] + create_duration = "600s" # 10 minutes + + triggers = { + account_id = azurerm_cognitive_account.ai_foundry.id + } +} + +# Private endpoint for AI Foundry +resource "azurerm_private_endpoint" "ai_foundry" { + count = var.is_exposed_externally ? 0 : 1 + + name = "pe-aif-${local.service_resource_name_suffix}" + location = data.azurerm_resource_group.ws.location + resource_group_name = data.azurerm_resource_group.ws.name + subnet_id = data.azurerm_subnet.services.id + tags = local.workspace_service_tags + + private_service_connection { + name = "psc-aif-${local.service_resource_name_suffix}" + private_connection_resource_id = azurerm_cognitive_account.ai_foundry.id + is_manual_connection = false + subresource_names = ["account"] + } + + depends_on = [time_sleep.wait_for_ai_foundry] + + private_dns_zone_group { + name = "dns-aif-${local.service_resource_name_suffix}" + private_dns_zone_ids = [ + data.azurerm_private_dns_zone.cognitive_services.id, + data.azurerm_private_dns_zone.openai.id, + data.azurerm_private_dns_zone.ai_services.id + ] + } + + timeouts { + create = "30m" + update = "30m" + delete = "30m" + read = "5m" + } + + lifecycle { + ignore_changes = [tags] + } +} + +# Default AI Foundry Project - required for connections and portal access +resource "azurerm_cognitive_account_project" "default" { + name = "default" + cognitive_account_id = azurerm_cognitive_account.ai_foundry.id + location = data.azurerm_resource_group.ws.location + display_name = "Default Project" + + identity { + type = "SystemAssigned" + } + + depends_on = [azurerm_private_endpoint.ai_foundry] + + timeouts { + create = "30m" + update = "30m" + delete = "30m" + read = "5m" + } +} + +# OpenAI Model Deployment +resource "azurerm_cognitive_deployment" "openai" { + name = local.openai_model.name + cognitive_account_id = azurerm_cognitive_account.ai_foundry.id + + model { + format = "OpenAI" + name = local.openai_model.name + version = local.openai_model.version + } + + sku { + name = "Standard" + capacity = var.openai_model_capacity + } + + timeouts { + create = "30m" + update = "30m" + delete = "30m" + read = "5m" + } +} + +# Storage container for AI Foundry artifacts (unique per service instance) +resource "azurerm_storage_container" "ai_foundry" { + name = "aif-${random_string.suffix.result}" + storage_account_id = data.azurerm_storage_account.workspace.id + container_access_type = "private" +} + +# Connection from AI Foundry Project to Workspace Storage +# This makes the storage account visible in the AI Foundry portal +resource "azapi_resource" "storage_connection" { + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview" + name = "workspace-storage" + parent_id = azurerm_cognitive_account_project.default.id + + schema_validation_enabled = false + + body = { + properties = { + category = "AzureBlob" + target = data.azurerm_storage_account.workspace.primary_blob_endpoint + authType = "AAD" + metadata = { + ResourceId = data.azurerm_storage_account.workspace.id + AccountName = data.azurerm_storage_account.workspace.name + ContainerName = azurerm_storage_container.ai_foundry.name + } + } + } + + depends_on = [ + azurerm_cognitive_account_project.default, + azurerm_storage_container.ai_foundry + ] +} + +# Role assignment for AI Foundry to access workspace storage +resource "azurerm_role_assignment" "ai_foundry_storage_blob_contributor" { + scope = data.azurerm_storage_account.workspace.id + role_definition_name = "Storage Blob Data Contributor" + principal_id = azurerm_cognitive_account.ai_foundry.identity[0].principal_id +} + +resource "azurerm_role_assignment" "ai_foundry_storage_file_contributor" { + scope = data.azurerm_storage_account.workspace.id + role_definition_name = "Storage File Data Privileged Contributor" + principal_id = azurerm_cognitive_account.ai_foundry.identity[0].principal_id +} diff --git a/templates/workspace_services/ai-foundry/terraform/ai_search.tf b/templates/workspace_services/ai-foundry/terraform/ai_search.tf new file mode 100644 index 000000000..134beaf08 --- /dev/null +++ b/templates/workspace_services/ai-foundry/terraform/ai_search.tf @@ -0,0 +1,128 @@ +# Azure AI Search (optional) +# Creates Azure Cognitive Search service for RAG scenarios + +resource "azapi_resource" "ai_search" { + count = var.enable_ai_search ? 1 : 0 + + type = "Microsoft.Search/searchServices@2024-06-01-preview" + name = "srch-${local.service_resource_name_suffix}-${random_string.suffix.result}" + location = data.azurerm_resource_group.ws.location + parent_id = data.azurerm_resource_group.ws.id + + identity { + type = "SystemAssigned" + } + + body = { + sku = { + name = "standard" + } + properties = { + replicaCount = 1 + partitionCount = 1 + hostingMode = "default" + semanticSearch = "standard" + disableLocalAuth = false + publicNetworkAccess = var.is_exposed_externally ? "Enabled" : "Disabled" + networkRuleSet = { + bypass = "None" + } + authOptions = { + aadOrApiKey = { + aadAuthFailureMode = "http401WithBearerChallenge" + } + } + } + } + + schema_validation_enabled = true + tags = local.workspace_service_tags + + timeouts { + create = "60m" + update = "60m" + delete = "30m" + } + + lifecycle { + ignore_changes = [tags] + } +} + +# Private endpoint for AI Search +resource "azurerm_private_endpoint" "ai_search" { + count = var.enable_ai_search && !var.is_exposed_externally ? 1 : 0 + + name = "pe-${azapi_resource.ai_search[0].name}" + location = data.azurerm_resource_group.ws.location + resource_group_name = data.azurerm_resource_group.ws.name + subnet_id = data.azurerm_subnet.services.id + tags = local.workspace_service_tags + + private_service_connection { + name = "psc-${azapi_resource.ai_search[0].name}" + private_connection_resource_id = azapi_resource.ai_search[0].id + is_manual_connection = false + subresource_names = ["searchService"] + } + + private_dns_zone_group { + name = "dns-${azapi_resource.ai_search[0].name}" + private_dns_zone_ids = [data.azurerm_private_dns_zone.ai_search.id] + } + + depends_on = [azapi_resource.ai_search] + + lifecycle { + ignore_changes = [tags] + } +} + +# Role assignment for AI Foundry to access AI Search +resource "azurerm_role_assignment" "ai_foundry_search_contributor" { + count = var.enable_ai_search ? 1 : 0 + + scope = azapi_resource.ai_search[0].id + role_definition_name = "Search Service Contributor" + principal_id = azurerm_cognitive_account.ai_foundry.identity[0].principal_id +} + +resource "azurerm_role_assignment" "ai_foundry_search_index_contributor" { + count = var.enable_ai_search ? 1 : 0 + + scope = azapi_resource.ai_search[0].id + role_definition_name = "Search Index Data Contributor" + principal_id = azurerm_cognitive_account.ai_foundry.identity[0].principal_id +} + +# Connection from AI Foundry Project to AI Search +# This makes AI Search visible as a knowledge store in the AI Foundry portal +resource "azapi_resource" "ai_search_connection" { + count = var.enable_ai_search ? 1 : 0 + + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview" + name = "ai-search" + parent_id = azurerm_cognitive_account_project.default.id + + schema_validation_enabled = false + + body = { + properties = { + category = "CognitiveSearch" + target = "https://${azapi_resource.ai_search[0].name}.search.windows.net" + authType = "AAD" + metadata = { + ApiVersion = "2024-06-01-preview" + ResourceId = azapi_resource.ai_search[0].id + } + } + } + + depends_on = [ + azapi_resource.ai_search, + azurerm_private_endpoint.ai_search, + azurerm_role_assignment.ai_foundry_search_contributor, + azurerm_role_assignment.ai_foundry_search_index_contributor, + azurerm_cognitive_account_project.default + ] +} diff --git a/templates/workspace_services/ai-foundry/terraform/cosmos_db.tf b/templates/workspace_services/ai-foundry/terraform/cosmos_db.tf new file mode 100644 index 000000000..c0e6326c0 --- /dev/null +++ b/templates/workspace_services/ai-foundry/terraform/cosmos_db.tf @@ -0,0 +1,117 @@ +# Azure Cosmos DB (optional) +# Creates Cosmos DB for agent state persistence and conversation history + +resource "azurerm_cosmosdb_account" "agents" { + count = var.enable_cosmos_db ? 1 : 0 + + name = "cosmos-${local.service_resource_name_suffix}-${random_string.suffix.result}" + location = data.azurerm_resource_group.ws.location + resource_group_name = data.azurerm_resource_group.ws.name + offer_type = "Standard" + kind = "GlobalDocumentDB" + automatic_failover_enabled = false + public_network_access_enabled = var.is_exposed_externally + local_authentication_disabled = false + access_key_metadata_writes_enabled = true + + consistency_policy { + consistency_level = "Session" + } + + geo_location { + location = data.azurerm_resource_group.ws.location + failover_priority = 0 + } + + capabilities { + name = "EnableServerless" + } + + tags = local.workspace_service_tags + + timeouts { + create = "60m" + update = "60m" + delete = "30m" + } + + lifecycle { + ignore_changes = [tags] + } +} + +# Private endpoint for Cosmos DB +resource "azurerm_private_endpoint" "cosmos_db" { + count = var.enable_cosmos_db && !var.is_exposed_externally ? 1 : 0 + + name = "pe-${azurerm_cosmosdb_account.agents[0].name}" + location = data.azurerm_resource_group.ws.location + resource_group_name = data.azurerm_resource_group.ws.name + subnet_id = data.azurerm_subnet.services.id + tags = local.workspace_service_tags + + private_service_connection { + name = "psc-${azurerm_cosmosdb_account.agents[0].name}" + private_connection_resource_id = azurerm_cosmosdb_account.agents[0].id + is_manual_connection = false + subresource_names = ["Sql"] + } + + private_dns_zone_group { + name = "dns-${azurerm_cosmosdb_account.agents[0].name}" + private_dns_zone_ids = [data.azurerm_private_dns_zone.cosmos_db[0].id] + } + + lifecycle { + ignore_changes = [tags] + } +} + +# Role assignment for AI Foundry to access Cosmos DB +resource "azurerm_role_assignment" "ai_foundry_cosmos_operator" { + count = var.enable_cosmos_db ? 1 : 0 + + scope = azurerm_cosmosdb_account.agents[0].id + role_definition_name = "Cosmos DB Operator" + principal_id = azurerm_cognitive_account.ai_foundry.identity[0].principal_id +} + +resource "azurerm_role_assignment" "ai_foundry_cosmos_contributor" { + count = var.enable_cosmos_db ? 1 : 0 + + scope = azurerm_cosmosdb_account.agents[0].id + role_definition_name = "Cosmos DB Account Reader Role" + principal_id = azurerm_cognitive_account.ai_foundry.identity[0].principal_id +} + +# Connection from AI Foundry Project to Cosmos DB +# This makes Cosmos DB visible for agent state storage in the AI Foundry portal +resource "azapi_resource" "cosmos_db_connection" { + count = var.enable_cosmos_db ? 1 : 0 + + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview" + name = "cosmos-db" + parent_id = azurerm_cognitive_account_project.default.id + + schema_validation_enabled = false + + body = { + properties = { + category = "CosmosDB" + target = azurerm_cosmosdb_account.agents[0].endpoint + authType = "AAD" + metadata = { + ResourceId = azurerm_cosmosdb_account.agents[0].id + database = "AgentsDB" + } + } + } + + depends_on = [ + azurerm_cosmosdb_account.agents, + azurerm_private_endpoint.cosmos_db, + azurerm_role_assignment.ai_foundry_cosmos_operator, + azurerm_role_assignment.ai_foundry_cosmos_contributor, + azurerm_cognitive_account_project.default + ] +} diff --git a/templates/workspace_services/ai-foundry/terraform/keyvault.tf b/templates/workspace_services/ai-foundry/terraform/keyvault.tf new file mode 100644 index 000000000..254cbc7ce --- /dev/null +++ b/templates/workspace_services/ai-foundry/terraform/keyvault.tf @@ -0,0 +1,62 @@ +# Key Vault for AI Foundry +# Dedicated Key Vault for AI Foundry secrets (separate from workspace KV which has sensitive credentials) + +resource "azurerm_key_vault" "ai_foundry" { + name = local.key_vault_name + location = data.azurerm_resource_group.ws.location + resource_group_name = data.azurerm_resource_group.ws.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + purge_protection_enabled = false + soft_delete_retention_days = 7 + public_network_access_enabled = var.is_exposed_externally + rbac_authorization_enabled = true + + network_acls { + bypass = "AzureServices" + default_action = var.is_exposed_externally ? "Allow" : "Deny" + } + + tags = local.workspace_service_tags + + lifecycle { + ignore_changes = [tags] + } +} + +# Private endpoint for Key Vault +resource "azurerm_private_endpoint" "keyvault" { + count = var.is_exposed_externally ? 0 : 1 + + name = "pe-${azurerm_key_vault.ai_foundry.name}" + location = data.azurerm_resource_group.ws.location + resource_group_name = data.azurerm_resource_group.ws.name + subnet_id = data.azurerm_subnet.services.id + tags = local.workspace_service_tags + + private_service_connection { + name = "psc-${azurerm_key_vault.ai_foundry.name}" + private_connection_resource_id = azurerm_key_vault.ai_foundry.id + is_manual_connection = false + subresource_names = ["vault"] + } + + private_dns_zone_group { + name = "dns-${azurerm_key_vault.ai_foundry.name}" + private_dns_zone_ids = [data.azurerm_private_dns_zone.keyvault.id] + } + + lifecycle { + ignore_changes = [tags] + } +} + +# Role assignment for AI Foundry to access Key Vault +resource "azurerm_role_assignment" "ai_foundry_kv_secrets_officer" { + scope = azurerm_key_vault.ai_foundry.id + role_definition_name = "Key Vault Secrets Officer" + principal_id = azurerm_cognitive_account.ai_foundry.identity[0].principal_id +} + +# Data source for current client config +data "azurerm_client_config" "current" {} diff --git a/templates/workspace_services/ai-foundry/terraform/locals.tf b/templates/workspace_services/ai-foundry/terraform/locals.tf new file mode 100644 index 000000000..b05d5579e --- /dev/null +++ b/templates/workspace_services/ai-foundry/terraform/locals.tf @@ -0,0 +1,27 @@ +locals { + short_service_id = substr(var.tre_resource_id, -4, -1) + short_workspace_id = substr(var.workspace_id, -4, -1) + workspace_resource_name_suffix = "${var.tre_id}-ws-${local.short_workspace_id}" + service_resource_name_suffix = "${var.tre_id}-ws-${local.short_workspace_id}-svc-${local.short_service_id}" + core_resource_group_name = "rg-${var.tre_id}" + + # Base name for AI Foundry resources (must be 3-7 characters per AVM module requirement) + # Using only 3 chars to leave room for module's naming suffixes (storage account names max 24 chars) + ai_foundry_base_name = lower(substr("ai${local.short_service_id}", 0, 3)) + + # Explicit names for BYOR resources to avoid length issues + storage_account_name = lower(substr("staif${local.short_workspace_id}${local.short_service_id}", 0, 24)) + key_vault_name = "kv-aif-${local.short_service_id}" + + workspace_service_tags = { + tre_id = var.tre_id + tre_workspace_id = var.workspace_id + tre_workspace_service_id = var.tre_resource_id + } + + # Parse OpenAI model from "model_name | version" format + openai_model = { + name = trimspace(split("|", var.openai_model)[0]) + version = trimspace(split("|", var.openai_model)[1]) + } +} diff --git a/templates/workspace_services/ai-foundry/terraform/main.tf b/templates/workspace_services/ai-foundry/terraform/main.tf new file mode 100644 index 000000000..8a9f41284 --- /dev/null +++ b/templates/workspace_services/ai-foundry/terraform/main.tf @@ -0,0 +1,106 @@ +terraform { + required_version = ">= 1.9.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.58" + } + azapi = { + source = "Azure/azapi" + version = "~> 2.8" + } + random = { + source = "hashicorp/random" + version = "~> 3.8" + } + time = { + source = "hashicorp/time" + version = "~> 0.13" + } + null = { + source = "hashicorp/null" + version = "~> 3.2" + } + } + + backend "azurerm" {} +} + +provider "azurerm" { + features {} + # Use Azure AD auth for storage operations (required when shared key access is disabled by policy) + storage_use_azuread = true +} + +provider "azapi" {} + +# Data sources for workspace resources +data "azurerm_resource_group" "ws" { + name = "rg-${var.tre_id}-ws-${local.short_workspace_id}" +} + +data "azurerm_virtual_network" "ws" { + name = "vnet-${var.tre_id}-ws-${local.short_workspace_id}" + resource_group_name = data.azurerm_resource_group.ws.name +} + +data "azurerm_subnet" "services" { + name = "ServicesSubnet" + virtual_network_name = data.azurerm_virtual_network.ws.name + resource_group_name = data.azurerm_resource_group.ws.name +} + +# Use the core route table for routing through the firewall +data "azurerm_route_table" "rt" { + name = "rt-${var.tre_id}" + resource_group_name = local.core_resource_group_name +} + +# Private DNS zones from core resource group +data "azurerm_private_dns_zone" "cognitive_services" { + name = "privatelink.cognitiveservices.azure.com" + resource_group_name = local.core_resource_group_name +} + +data "azurerm_private_dns_zone" "openai" { + name = "privatelink.openai.azure.com" + resource_group_name = local.core_resource_group_name +} + +data "azurerm_private_dns_zone" "ai_services" { + name = "privatelink.services.ai.azure.com" + resource_group_name = local.core_resource_group_name +} + +data "azurerm_private_dns_zone" "blob" { + name = "privatelink.blob.core.windows.net" + resource_group_name = local.core_resource_group_name +} + +data "azurerm_private_dns_zone" "keyvault" { + name = "privatelink.vaultcore.azure.net" + resource_group_name = local.core_resource_group_name +} + +data "azurerm_private_dns_zone" "ai_search" { + name = "privatelink.search.windows.net" + resource_group_name = local.core_resource_group_name +} + +data "azurerm_private_dns_zone" "file" { + name = "privatelink.file.core.windows.net" + resource_group_name = local.core_resource_group_name +} + +# Workspace storage account - reuse to avoid quota limits +data "azurerm_storage_account" "workspace" { + name = "stgws${local.short_workspace_id}" + resource_group_name = data.azurerm_resource_group.ws.name +} + +data "azurerm_private_dns_zone" "cosmos_db" { + count = var.enable_cosmos_db ? 1 : 0 + name = "privatelink.documents.azure.com" + resource_group_name = local.core_resource_group_name +} diff --git a/templates/workspace_services/ai-foundry/terraform/network.tf b/templates/workspace_services/ai-foundry/terraform/network.tf new file mode 100644 index 000000000..e51b6a464 --- /dev/null +++ b/templates/workspace_services/ai-foundry/terraform/network.tf @@ -0,0 +1,132 @@ +# Network Security Group for AI Foundry Agents +resource "azurerm_network_security_group" "agents" { + location = data.azurerm_virtual_network.ws.location + name = "nsg-aif-agents-${local.short_service_id}" + resource_group_name = data.azurerm_virtual_network.ws.resource_group_name + tags = local.workspace_service_tags + + lifecycle { ignore_changes = [tags] } +} + +# Agent subnet with Microsoft.App/environments delegation for AI Foundry agents +resource "azurerm_subnet" "agents" { + name = "AIFAgentSubnet${local.short_service_id}" + virtual_network_name = data.azurerm_virtual_network.ws.name + resource_group_name = data.azurerm_virtual_network.ws.resource_group_name + address_prefixes = [var.address_space] + + private_endpoint_network_policies = "Disabled" + + # Required delegation for AI Foundry Standard Agents with VNet injection + delegation { + name = "Microsoft.App.environments" + + service_delegation { + name = "Microsoft.App/environments" + actions = [ + "Microsoft.Network/virtualNetworks/subnets/join/action" + ] + } + } +} + +resource "azurerm_subnet_network_security_group_association" "agents" { + network_security_group_id = azurerm_network_security_group.agents.id + subnet_id = azurerm_subnet.agents.id +} + +# NSG Rules for AI Foundry Agents + +resource "azurerm_network_security_rule" "allow_inbound_within_workspace_vnet" { + access = "Allow" + destination_port_range = "*" + destination_address_prefixes = data.azurerm_virtual_network.ws.address_space + source_address_prefixes = data.azurerm_virtual_network.ws.address_space + direction = "Inbound" + name = "inbound-within-workspace-vnet" + network_security_group_name = azurerm_network_security_group.agents.name + priority = 100 + protocol = "*" + resource_group_name = data.azurerm_resource_group.ws.name + source_port_range = "*" +} + +resource "azurerm_network_security_rule" "allow_outbound_within_workspace_vnet" { + access = "Allow" + destination_port_range = "*" + destination_address_prefixes = data.azurerm_virtual_network.ws.address_space + source_address_prefixes = data.azurerm_virtual_network.ws.address_space + direction = "Outbound" + name = "outbound-within-workspace-vnet" + network_security_group_name = azurerm_network_security_group.agents.name + priority = 100 + protocol = "*" + resource_group_name = data.azurerm_resource_group.ws.name + source_port_range = "*" +} + +# Allow outbound to services subnet (for accessing TRE resources and private endpoints) +resource "azurerm_network_security_rule" "allow_outbound_to_services" { + access = "Allow" + destination_address_prefixes = data.azurerm_subnet.services.address_prefixes + destination_port_range = "*" + direction = "Outbound" + name = "to-services-subnet" + network_security_group_name = azurerm_network_security_group.agents.name + priority = 101 + protocol = "*" + resource_group_name = data.azurerm_resource_group.ws.name + source_address_prefix = "*" + source_port_range = "*" +} + +# Allow outbound HTTPS for AI Foundry agents to access Azure services +resource "azurerm_network_security_rule" "allow_outbound_https" { + access = "Allow" + destination_address_prefix = "INTERNET" + destination_port_range = "443" + direction = "Outbound" + name = "to-internet-https" + network_security_group_name = azurerm_network_security_group.agents.name + priority = 102 + protocol = "Tcp" + resource_group_name = data.azurerm_resource_group.ws.name + source_address_prefix = "*" + source_port_range = "*" +} + +# Deny all other outbound traffic (data exfiltration prevention) +resource "azurerm_network_security_rule" "deny_outbound_override" { + access = "Deny" + destination_address_prefix = "*" + destination_port_range = "*" + direction = "Outbound" + name = "deny-outbound-override" + network_security_group_name = azurerm_network_security_group.agents.name + priority = 4096 + protocol = "*" + resource_group_name = data.azurerm_resource_group.ws.name + source_address_prefix = "*" + source_port_range = "*" +} + +# Deny all inbound from outside the VNet +resource "azurerm_network_security_rule" "deny_all_inbound_override" { + access = "Deny" + destination_address_prefix = "*" + destination_port_range = "*" + direction = "Inbound" + name = "deny-inbound-override" + network_security_group_name = azurerm_network_security_group.agents.name + priority = 4096 + protocol = "*" + resource_group_name = data.azurerm_resource_group.ws.name + source_address_prefix = "*" + source_port_range = "*" +} + +# Associate the agent subnet with the workspace route table (routes through firewall) +resource "azurerm_subnet_route_table_association" "agents" { + route_table_id = data.azurerm_route_table.rt.id + subnet_id = azurerm_subnet.agents.id +} diff --git a/templates/workspace_services/ai-foundry/terraform/outputs.tf b/templates/workspace_services/ai-foundry/terraform/outputs.tf new file mode 100644 index 000000000..9b3feae0e --- /dev/null +++ b/templates/workspace_services/ai-foundry/terraform/outputs.tf @@ -0,0 +1,24 @@ +output "ai_foundry_id" { + value = azurerm_cognitive_account.ai_foundry.id + description = "The resource ID of the AI Foundry account" +} + +output "ai_foundry_name" { + value = azurerm_cognitive_account.ai_foundry.name + description = "The name of the AI Foundry account" +} + +output "connection_uri" { + value = "https://ai.azure.com/resource/overview?tid=${data.azurerm_client_config.current.tenant_id}&wsid=${azurerm_cognitive_account.ai_foundry.id}" + description = "The connection URI for accessing the AI Foundry in Azure AI Foundry portal" +} + +output "is_exposed_externally" { + value = var.is_exposed_externally + description = "Whether the service is accessible from outside the workspace network" +} + +output "workspace_address_spaces" { + value = data.azurerm_virtual_network.ws.address_space + description = "The address spaces of the workspace virtual network" +} diff --git a/templates/workspace_services/ai-foundry/terraform/roles.tf b/templates/workspace_services/ai-foundry/terraform/roles.tf new file mode 100644 index 000000000..b62dc1200 --- /dev/null +++ b/templates/workspace_services/ai-foundry/terraform/roles.tf @@ -0,0 +1,89 @@ +# Role definitions +data "azurerm_role_definition" "azure_ai_developer" { + name = "Azure AI Developer" +} + +data "azurerm_role_definition" "cognitive_services_user" { + name = "Cognitive Services User" +} + +data "azurerm_role_definition" "cognitive_services_contributor" { + name = "Cognitive Services Contributor" +} + +data "azurerm_role_definition" "reader" { + name = "Reader" +} + +data "azurerm_role_definition" "key_vault_secrets_user" { + name = "Key Vault Secrets User" +} + +# Note: Storage roles are managed by the base workspace template. +# AI Foundry creates its own Key Vault (separate from workspace key vault +# which contains sensitive infrastructure secrets like client_secret). +# Workspace group IDs are passed from the parent workspace properties. + +# Role assignments for workspace owners group +resource "azurerm_role_assignment" "owners_ai_developer" { + scope = azurerm_cognitive_account.ai_foundry.id + role_definition_id = data.azurerm_role_definition.azure_ai_developer.id + principal_id = var.workspace_owners_group_id +} + +resource "azurerm_role_assignment" "owners_cognitive_services_user" { + scope = azurerm_cognitive_account.ai_foundry.id + role_definition_id = data.azurerm_role_definition.cognitive_services_user.id + principal_id = var.workspace_owners_group_id +} + +resource "azurerm_role_assignment" "owners_cognitive_services_contributor" { + scope = azurerm_cognitive_account.ai_foundry.id + role_definition_id = data.azurerm_role_definition.cognitive_services_contributor.id + principal_id = var.workspace_owners_group_id +} + +resource "azurerm_role_assignment" "owners_reader" { + scope = azurerm_cognitive_account.ai_foundry.id + role_definition_id = data.azurerm_role_definition.reader.id + principal_id = var.workspace_owners_group_id +} + +# Role assignments for workspace researchers group +resource "azurerm_role_assignment" "researchers_ai_developer" { + scope = azurerm_cognitive_account.ai_foundry.id + role_definition_id = data.azurerm_role_definition.azure_ai_developer.id + principal_id = var.workspace_researchers_group_id +} + +resource "azurerm_role_assignment" "researchers_cognitive_services_user" { + scope = azurerm_cognitive_account.ai_foundry.id + role_definition_id = data.azurerm_role_definition.cognitive_services_user.id + principal_id = var.workspace_researchers_group_id +} + +resource "azurerm_role_assignment" "researchers_cognitive_services_contributor" { + scope = azurerm_cognitive_account.ai_foundry.id + role_definition_id = data.azurerm_role_definition.cognitive_services_contributor.id + principal_id = var.workspace_researchers_group_id +} + +resource "azurerm_role_assignment" "researchers_reader" { + scope = azurerm_cognitive_account.ai_foundry.id + role_definition_id = data.azurerm_role_definition.reader.id + principal_id = var.workspace_researchers_group_id +} + +# AI Foundry Key Vault role assignments +# Required for accessing secrets used by AI Foundry (separate from workspace key vault) +resource "azurerm_role_assignment" "owners_aif_keyvault" { + scope = azurerm_key_vault.ai_foundry.id + role_definition_id = data.azurerm_role_definition.key_vault_secrets_user.id + principal_id = var.workspace_owners_group_id +} + +resource "azurerm_role_assignment" "researchers_aif_keyvault" { + scope = azurerm_key_vault.ai_foundry.id + role_definition_id = data.azurerm_role_definition.key_vault_secrets_user.id + principal_id = var.workspace_researchers_group_id +} diff --git a/templates/workspace_services/ai-foundry/terraform/variables.tf b/templates/workspace_services/ai-foundry/terraform/variables.tf new file mode 100644 index 000000000..46f8c7c84 --- /dev/null +++ b/templates/workspace_services/ai-foundry/terraform/variables.tf @@ -0,0 +1,77 @@ +variable "workspace_id" { + type = string + description = "The workspace ID" +} + +variable "tre_id" { + type = string + description = "The TRE ID" +} + +variable "tre_resource_id" { + type = string + description = "The TRE resource ID for this workspace service" +} + +variable "arm_environment" { + type = string + description = "The ARM environment" + default = "public" +} + +variable "display_name" { + type = string + description = "Display name for the AI Foundry service" + default = "Azure AI Foundry" +} + +variable "openai_model" { + type = string + description = "OpenAI model to deploy in format 'model_name | version'" + default = "gpt-4o | 2024-05-13" +} + +variable "openai_model_capacity" { + type = number + description = "Capacity for the OpenAI model deployment (in thousands of tokens per minute)" + default = 10 +} + +variable "is_exposed_externally" { + type = bool + description = "Determines if the AI Foundry resources are accessible from outside the workspace network" + default = false +} + +variable "enable_ai_search" { + type = bool + description = "Enable Azure AI Search for RAG and knowledge retrieval scenarios" + default = false +} + +variable "enable_cosmos_db" { + type = bool + description = "Enable Azure Cosmos DB for agent state persistence and conversation history" + default = false +} + +variable "enable_agent_networking" { + type = bool + description = "Enable VNet injection for Standard Agents. Adds network injections which can take 30-60+ minutes to provision. Deploy without this first, then upgrade to enable." + default = false +} + +variable "address_space" { + type = string + description = "Address space for the AI Foundry agent subnet" +} + +variable "workspace_owners_group_id" { + type = string + description = "Object ID of the workspace owners AAD group" +} + +variable "workspace_researchers_group_id" { + type = string + description = "Object ID of the workspace researchers AAD group" +}