From e6f613160abfaa5bbd96defb70c6be03018b92a1 Mon Sep 17 00:00:00 2001 From: mattfeltonma Date: Thu, 28 Aug 2025 21:54:25 -0400 Subject: [PATCH] modified - added templates for AI Foundry Accounts * Added template for AI Foundry Account * Added template for AI Foundry with Basic Agent * Added template for AI Foundry with Standard Agent * Added template for AI Foundry with Standard Agent and Bring-Your-Own-Virtual Network --- .../101-azure-ai-foundry-account/README.md | 5 + .../101-azure-ai-foundry-account/main.tf | 100 +++ .../101-azure-ai-foundry-account/outputs.tf | 7 + .../101-azure-ai-foundry-account/providers.tf | 34 + .../101-azure-ai-foundry-account/variables.tf | 10 + .../README.md | 5 + .../201-azure-ai-foundry-basic-agent/main.tf | 127 +++ .../outputs.tf | 11 + .../providers.tf | 35 + .../variables.tf | 10 + .../README.md | 5 + .../main.tf | 794 ++++++++++++++++++ .../outputs.tf | 7 + .../providers.tf | 30 + .../variables.tf | 23 + .../README.md | 5 + .../main.tf | 642 ++++++++++++++ .../outputs.tf | 7 + .../providers.tf | 54 ++ .../variables.tf | 34 + 20 files changed, 1945 insertions(+) create mode 100644 quickstart/101-azure-ai-foundry-account/README.md create mode 100644 quickstart/101-azure-ai-foundry-account/main.tf create mode 100644 quickstart/101-azure-ai-foundry-account/outputs.tf create mode 100644 quickstart/101-azure-ai-foundry-account/providers.tf create mode 100644 quickstart/101-azure-ai-foundry-account/variables.tf create mode 100644 quickstart/201-azure-ai-foundry-basic-agent/README.md create mode 100644 quickstart/201-azure-ai-foundry-basic-agent/main.tf create mode 100644 quickstart/201-azure-ai-foundry-basic-agent/outputs.tf create mode 100644 quickstart/201-azure-ai-foundry-basic-agent/providers.tf create mode 100644 quickstart/201-azure-ai-foundry-basic-agent/variables.tf create mode 100644 quickstart/301-azure-ai-foundry-account-standard-agent/README.md create mode 100644 quickstart/301-azure-ai-foundry-account-standard-agent/main.tf create mode 100644 quickstart/301-azure-ai-foundry-account-standard-agent/outputs.tf create mode 100644 quickstart/301-azure-ai-foundry-account-standard-agent/providers.tf create mode 100644 quickstart/301-azure-ai-foundry-account-standard-agent/variables.tf create mode 100644 quickstart/302-azure-ai-foundry-account-standard-agent-byovnet/README.md create mode 100644 quickstart/302-azure-ai-foundry-account-standard-agent-byovnet/main.tf create mode 100644 quickstart/302-azure-ai-foundry-account-standard-agent-byovnet/outputs.tf create mode 100644 quickstart/302-azure-ai-foundry-account-standard-agent-byovnet/providers.tf create mode 100644 quickstart/302-azure-ai-foundry-account-standard-agent-byovnet/variables.tf diff --git a/quickstart/101-azure-ai-foundry-account/README.md b/quickstart/101-azure-ai-foundry-account/README.md new file mode 100644 index 000000000..b71df747c --- /dev/null +++ b/quickstart/101-azure-ai-foundry-account/README.md @@ -0,0 +1,5 @@ +# Azure AI Foundry Account + +This deployment configuration specifies an [Azure AI Foundry Account](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-azure-ai-foundry) and a deployment of the OpenAI 4.1 model. + +This configuration describes the minimal set of resources you require to get started with Azure AI Foundry Accounts. \ No newline at end of file diff --git a/quickstart/101-azure-ai-foundry-account/main.tf b/quickstart/101-azure-ai-foundry-account/main.tf new file mode 100644 index 000000000..74567897d --- /dev/null +++ b/quickstart/101-azure-ai-foundry-account/main.tf @@ -0,0 +1,100 @@ +########## Create infrastructure resources +########## + +## Get subscription data +## + +data "azurerm_client_config" "current" {} + +## Create a random string +## +resource "random_string" "unique" { + length = 4 + min_numeric = 4 + numeric = true + special = false + lower = true + upper = false +} + +## Create a resource group for the resources to be stored in +## +resource "azurerm_resource_group" "rg" { + name = "${var.resource_group_name_prefix}-aifoundry${random_string.unique.result}" + location = var.region +} + +########## Create AI Foundry resource +########## + +## Create the AI Foundry resource +## +resource "azapi_resource" "ai_foundry" { + depends_on = [ + azapi_resource_action.purge_ai_foundry + ] + + type = "Microsoft.CognitiveServices/accounts@2025-06-01" + name = "aifoundry${random_string.unique.result}" + parent_id = azurerm_resource_group.rg.id + location = var.region + schema_validation_enabled = false + + body = { + kind = "AIServices", + sku = { + name = "S0" + } + identity = { + type = "SystemAssigned" + } + + properties = { + # Support both Entra ID and API Key authentication for underlining 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 = "aifoundry${random_string.unique.result}" + + # Network-related controls + # Disable public access but allow Trusted Azure Services exception + publicNetworkAccess = "Enabled" + networkAcls = { + defaultAction = "Allow" + } + } + } +} + +## Create a deployment for OpenAI's GPT-4.1 in the AI Foundry resource +## +resource "azurerm_cognitive_deployment" "aifoundry_deployment_gpt_41" { + depends_on = [ + azapi_resource.ai_foundry + ] + + name = "gpt-4.1" + cognitive_account_id = azapi_resource.ai_foundry.id + + sku { + name = "GlobalStandard" + capacity = 1 + } + + model { + format = "OpenAI" + name = "gpt-4.1" + version = "2025-04-14" + } +} + +## Added AI Foundry account purger to purge AI Foundry Account after deletion +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" + when = "destroy" +} diff --git a/quickstart/101-azure-ai-foundry-account/outputs.tf b/quickstart/101-azure-ai-foundry-account/outputs.tf new file mode 100644 index 000000000..538d6fe67 --- /dev/null +++ b/quickstart/101-azure-ai-foundry-account/outputs.tf @@ -0,0 +1,7 @@ +output "resource_group_name" { + value = azurerm_resource_group.rg.name +} + +output "ai_foundry_name" { + value = azapi_resource.ai_foundry.name +} \ No newline at end of file diff --git a/quickstart/101-azure-ai-foundry-account/providers.tf b/quickstart/101-azure-ai-foundry-account/providers.tf new file mode 100644 index 000000000..5dceb612d --- /dev/null +++ b/quickstart/101-azure-ai-foundry-account/providers.tf @@ -0,0 +1,34 @@ +terraform { + required_version = ">= 1.10.0, < 2.0.0" + required_providers { + azapi = { + source = "azure/azapi" + version = "~> 2.5" + } + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.37" + } + random = { + source = "hashicorp/random" + version = "~> 3.7" + } + time = { + source = "hashicorp/time" + version = "~> 0.13" + } + } +} + +# Setup providers +provider "azapi" { +} + +provider "azurerm" { + features { + resource_group { + prevent_deletion_if_contains_resources = false + } + } + storage_use_azuread = true +} diff --git a/quickstart/101-azure-ai-foundry-account/variables.tf b/quickstart/101-azure-ai-foundry-account/variables.tf new file mode 100644 index 000000000..51d079d28 --- /dev/null +++ b/quickstart/101-azure-ai-foundry-account/variables.tf @@ -0,0 +1,10 @@ +variable "region" { + description = "The Azure region to deploy the resources to" + type = string +} + +variable "resource_group_name_prefix" { + type = string + default = "rg" + description = "Prefix of the resource group name that's combined with a random ID so name is unique in your Azure subscription." +} \ No newline at end of file diff --git a/quickstart/201-azure-ai-foundry-basic-agent/README.md b/quickstart/201-azure-ai-foundry-basic-agent/README.md new file mode 100644 index 000000000..3099b9f28 --- /dev/null +++ b/quickstart/201-azure-ai-foundry-basic-agent/README.md @@ -0,0 +1,5 @@ +# Azure AI Foundry Account with Basic Agent + +This deployment configuration specifies an [Azure AI Foundry Account](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-azure-ai-foundry) and a deployment of the OpenAI 4.1 model. An AI Foundry Project is deployed within the AI Foundry Account to be used to [build basic agents](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/overview) with the Agent Service. + +This configuration describes the minimal set of resources you require to get started with AI Foundry Accounts, Projects, and the Agent Service. \ No newline at end of file diff --git a/quickstart/201-azure-ai-foundry-basic-agent/main.tf b/quickstart/201-azure-ai-foundry-basic-agent/main.tf new file mode 100644 index 000000000..ba01f4388 --- /dev/null +++ b/quickstart/201-azure-ai-foundry-basic-agent/main.tf @@ -0,0 +1,127 @@ +########## Create infrastructure resources +########## + +## Get subscription data +## + +data "azurerm_client_config" "current" {} + +## Create a random string +## +resource "random_string" "unique" { + length = 4 + min_numeric = 4 + numeric = true + special = false + lower = true + upper = false +} + +## Create a resource group for the resources to be stored in +## +resource "azurerm_resource_group" "rg" { + name = "${var.resource_group_name_prefix}-aifoundry${random_string.unique.result}" + location = var.region +} + +########## Create AI Foundry resource +########## + +## Create the AI Foundry resource +## +resource "azapi_resource" "ai_foundry" { + depends_on = [ + azapi_resource_action.purge_ai_foundry + ] + + type = "Microsoft.CognitiveServices/accounts@2025-06-01" + name = "aifoundry${random_string.unique.result}" + parent_id = azurerm_resource_group.rg.id + location = var.region + schema_validation_enabled = false + + body = { + kind = "AIServices", + sku = { + name = "S0" + } + identity = { + type = "SystemAssigned" + } + + properties = { + # Support both Entra ID and API Key authentication for underlining 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 = "aifoundry${random_string.unique.result}" + + # Network-related controls + # Disable public access but allow Trusted Azure Services exception + publicNetworkAccess = "Enabled" + networkAcls = { + defaultAction = "Allow" + } + } + } +} + +## Create a deployment for OpenAI's GPT-4.1 in the AI Foundry resource +## +resource "azurerm_cognitive_deployment" "aifoundry_deployment_gpt_41" { + depends_on = [ + azapi_resource.ai_foundry + ] + + name = "gpt-4.1" + cognitive_account_id = azapi_resource.ai_foundry.id + + sku { + name = "GlobalStandard" + capacity = 1 + } + + model { + format = "OpenAI" + name = "gpt-4.1" + version = "2025-04-14" + } +} + +resource "azapi_resource" "ai_foundry_project" { + depends_on = [ + azapi_resource.ai_foundry, + azurerm_cognitive_deployment.aifoundry_deployment_gpt_41 + ] + + type = "Microsoft.CognitiveServices/accounts/projects@2025-06-01" + name = "project${random_string.unique.result}" + parent_id = azapi_resource.ai_foundry.id + location = var.region + schema_validation_enabled = false + + body = { + sku = { + name = "S0" + } + identity = { + type = "SystemAssigned" + } + + properties = { + displayName = "project" + description = "A project for the AI Foundry account with network secured deployed Agent" + } + } +} + +## Added AI Foundry account purger to purge the AI Foundry Account after deletion +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" + when = "destroy" +} diff --git a/quickstart/201-azure-ai-foundry-basic-agent/outputs.tf b/quickstart/201-azure-ai-foundry-basic-agent/outputs.tf new file mode 100644 index 000000000..150294acc --- /dev/null +++ b/quickstart/201-azure-ai-foundry-basic-agent/outputs.tf @@ -0,0 +1,11 @@ +output "resource_group_name" { + value = azurerm_resource_group.rg.name +} + +output "ai_foundry_account_name" { + value = azapi_resource.ai_foundry.name +} + +output "ai_foundry_project_name" { + value = azapi_resource.ai_foundry_project.name +} \ No newline at end of file diff --git a/quickstart/201-azure-ai-foundry-basic-agent/providers.tf b/quickstart/201-azure-ai-foundry-basic-agent/providers.tf new file mode 100644 index 000000000..76cfeed13 --- /dev/null +++ b/quickstart/201-azure-ai-foundry-basic-agent/providers.tf @@ -0,0 +1,35 @@ +terraform { + required_version = ">= 1.10.0, < 2.0.0" + required_providers { + azapi = { + source = "azure/azapi" + version = "~> 2.5" + } + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.37" + } + random = { + source = "hashicorp/random" + version = "~> 3.7" + } + time = { + source = "hashicorp/time" + version = "~> 0.13" + } + } + +} + +# Setup providers +provider "azapi" { +} + +provider "azurerm" { + features { + resource_group { + prevent_deletion_if_contains_resources = false + } + } + storage_use_azuread = true +} diff --git a/quickstart/201-azure-ai-foundry-basic-agent/variables.tf b/quickstart/201-azure-ai-foundry-basic-agent/variables.tf new file mode 100644 index 000000000..51d079d28 --- /dev/null +++ b/quickstart/201-azure-ai-foundry-basic-agent/variables.tf @@ -0,0 +1,10 @@ +variable "region" { + description = "The Azure region to deploy the resources to" + type = string +} + +variable "resource_group_name_prefix" { + type = string + default = "rg" + description = "Prefix of the resource group name that's combined with a random ID so name is unique in your Azure subscription." +} \ No newline at end of file diff --git a/quickstart/301-azure-ai-foundry-account-standard-agent/README.md b/quickstart/301-azure-ai-foundry-account-standard-agent/README.md new file mode 100644 index 000000000..2a72ac3fd --- /dev/null +++ b/quickstart/301-azure-ai-foundry-account-standard-agent/README.md @@ -0,0 +1,5 @@ +# Azure AI Foundry Account with Standard Agent + +This deployment configuration specifies an [Azure AI Foundry Account](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-azure-ai-foundry) and a deployment of the OpenAI 4.1 model. An AI Foundry Project is deployed within the AI Foundry Account. The AI Foundry Account and Project are configured to support the Agent Service with a [standard agent](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/concepts/standard-agent-setup). + +This configuration describes the minimal set of resources you require to get started with Azure AI Foundry Accounts, Projects, and Agent Service configured in a Standard Agent configuration. \ No newline at end of file diff --git a/quickstart/301-azure-ai-foundry-account-standard-agent/main.tf b/quickstart/301-azure-ai-foundry-account-standard-agent/main.tf new file mode 100644 index 000000000..cc571cf97 --- /dev/null +++ b/quickstart/301-azure-ai-foundry-account-standard-agent/main.tf @@ -0,0 +1,794 @@ +########## Create infrastructure resources +########## + +## Get subscription data +## +data "azurerm_client_config" "current" {} + +## Create a random string +## +resource "random_string" "unique" { + length = 4 + min_numeric = 4 + numeric = true + special = false + lower = true + upper = false +} + +## Create a resource group for the resources to be stored in +## +resource "azurerm_resource_group" "rg" { + name = "rg-aifoundry${random_string.unique.result}" + location = var.region +} + +## Create a virtual network for the AI Foundry resource and supporting resources +## +resource "azurerm_virtual_network" "vnet" { + name = "vnet-agents${random_string.unique.result}" + location = var.region + resource_group_name = azurerm_resource_group.rg.name + address_space = [ + var.virtual_network_address_space + ] +} + +## Create two subnets one for the Standard Agent VNet injection and one for the AI Foundry resource +## +resource "azurerm_subnet" "subnet_agent" { + name = "snet-agent" + resource_group_name = azurerm_resource_group.rg.name + virtual_network_name = azurerm_virtual_network.vnet.name + address_prefixes = [ + var.agent_subnet_address_prefix + ] + delegation { + name = "Microsoft.App/environments" + service_delegation { + name = "Microsoft.App/environments" + actions = [ + "Microsoft.Network/virtualNetworks/subnets/join/action" + ] + } + } +} + +resource "azurerm_subnet" "subnet_pe" { + name = "snet-pe" + resource_group_name = azurerm_resource_group.rg.name + virtual_network_name = azurerm_virtual_network.vnet.name + address_prefixes = [ + var.private_endpoint_subnet_address_prefix + ] +} + +########## Create resoures required to store agent data +########## + +## Create a storage account for agent data +## +resource "azurerm_storage_account" "storage_account" { + name = "aifoundry${random_string.unique.result}storage" + resource_group_name = azurerm_resource_group.rg.name + location = var.region + + 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 + network_rules { + default_action = "Deny" + bypass = [ + "AzureServices" + ] + } +} + +## Create the Cosmos DB account to store agent threads +## +resource "azurerm_cosmosdb_account" "cosmosdb" { + name = "aifoundry${random_string.unique.result}cosmosdb" + location = var.region + resource_group_name = azurerm_resource_group.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.region + failover_priority = 0 + zone_redundant = false + } +} + +## Create an AI Search instance that will be used to store vector embeddings +## +resource "azapi_resource" "ai_search" { + type = "Microsoft.Search/searchServices@2025-05-01" + name = "aifoundry${random_string.unique.result}search" + parent_id = azurerm_resource_group.rg.id + location = var.region + schema_validation_enabled = true + + 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 +########## + +## Create the AI Foundry resource +## +resource "azapi_resource" "ai_foundry" { + depends_on = [ + azurerm_subnet.subnet_agent, + azapi_resource_action.purge_ai_foundry + ] + + type = "Microsoft.CognitiveServices/accounts@2025-06-01" + name = "aifoundry${random_string.unique.result}" + parent_id = azurerm_resource_group.rg.id + location = var.region + schema_validation_enabled = false + + body = { + kind = "AIServices", + sku = { + name = "S0" + } + identity = { + type = "SystemAssigned" + } + + properties = { + # Support both Entra ID and API Key authentication for underlining 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 = "aifoundry${random_string.unique.result}" + + # Network-related controls + # Disable public access but allow Trusted Azure Services exception + publicNetworkAccess = "Disabled" + networkAcls = { + defaultAction = "Allow" + } + + # Enable VNet injection for Standard Agents + networkInjections = [ + { + scenario = "agent" + subnetArmId = azurerm_subnet.subnet_agent.id + useMicrosoftManagedNetwork = false + } + ] + } + } +} + +## Create a deployment for OpenAI's GPT-4o in the AI Foundry resource +## +resource "azurerm_cognitive_deployment" "aifoundry_deployment_gpt_4o" { + depends_on = [ + azapi_resource.ai_foundry + ] + + name = "gpt-4o" + cognitive_account_id = azapi_resource.ai_foundry.id + + sku { + name = "GlobalStandard" + capacity = 1 + } + + model { + format = "OpenAI" + name = "gpt-4o" + version = "2024-11-20" + } +} + +########## Create Private DNS Zones, Links, and Private Endpoints +########## + +## Create required Private DNS Zones +## +resource "azurerm_private_dns_zone" "plz_cosmos_db" { + name = "privatelink.documents.azure.com" + resource_group_name = azurerm_resource_group.rg.name +} + +resource "azurerm_private_dns_zone" "plz_ai_search" { + name = "privatelink.search.windows.net" + resource_group_name = azurerm_resource_group.rg.name +} + +resource "azurerm_private_dns_zone" "plz_storage_blob" { + name = "privatelink.blob.core.windows.net" + resource_group_name = azurerm_resource_group.rg.name +} + +resource "azurerm_private_dns_zone" "plz_cognitive_services" { + name = "privatelink.cognitiveservices.azure.com" + resource_group_name = azurerm_resource_group.rg.name +} + +resource "azurerm_private_dns_zone" "plz_ai_services" { + name = "privatelink.services.ai.azure.com" + resource_group_name = azurerm_resource_group.rg.name +} + +resource "azurerm_private_dns_zone" "plz_openai" { + name = "privatelink.openai.azure.com" + resource_group_name = azurerm_resource_group.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" { + depends_on = [ + azurerm_private_dns_zone.plz_cosmos_db, + azurerm_virtual_network.vnet + ] + name = "cosmosdb-${random_string.unique.result}-link" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.plz_cosmos_db.name + virtual_network_id = azurerm_virtual_network.vnet.id + registration_enabled = false +} + +resource "azurerm_private_dns_zone_virtual_network_link" "plz_ai_search_link" { + depends_on = [ + azurerm_private_dns_zone_virtual_network_link.plz_cosmos_db_link, + azurerm_private_dns_zone.plz_ai_search, + azurerm_virtual_network.vnet + ] + + name = "aisearch-${random_string.unique.result}-link" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.plz_ai_search.name + virtual_network_id = azurerm_virtual_network.vnet.id + registration_enabled = false +} + +resource "azurerm_private_dns_zone_virtual_network_link" "plz_storage_blob_link" { + depends_on = [ + azurerm_private_dns_zone_virtual_network_link.plz_ai_search_link, + azurerm_private_dns_zone.plz_storage_blob, + azurerm_virtual_network.vnet + ] + name = "storage-${random_string.unique.result}-link" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.plz_storage_blob.name + virtual_network_id = azurerm_virtual_network.vnet.id + registration_enabled = false +} + +resource "azurerm_private_dns_zone_virtual_network_link" "plz_cognitive_services_link" { + depends_on = [ + azurerm_private_dns_zone_virtual_network_link.plz_storage_blob_link, + azurerm_private_dns_zone.plz_cognitive_services, + azurerm_virtual_network.vnet + ] + name = "cogsvc-${random_string.unique.result}-link" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.plz_cognitive_services.name + virtual_network_id = azurerm_virtual_network.vnet.id + registration_enabled = false +} + +resource "azurerm_private_dns_zone_virtual_network_link" "plz_ai_services_link" { + depends_on = [ + azurerm_private_dns_zone_virtual_network_link.plz_cognitive_services_link, + azurerm_private_dns_zone.plz_ai_services, + azurerm_virtual_network.vnet + ] + name = "aiservices-${random_string.unique.result}-link" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.plz_ai_services.name + virtual_network_id = azurerm_virtual_network.vnet.id + registration_enabled = false +} + +resource "azurerm_private_dns_zone_virtual_network_link" "plz_openai_link" { + depends_on = [ + azurerm_private_dns_zone_virtual_network_link.plz_ai_services_link, + azurerm_private_dns_zone.plz_openai, + azurerm_virtual_network.vnet + ] + name = "openai-${random_string.unique.result}-link" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.plz_openai.name + virtual_network_id = azurerm_virtual_network.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_ai_search_link, + azurerm_private_dns_zone_virtual_network_link.plz_storage_blob_link, + azurerm_private_dns_zone_virtual_network_link.plz_cognitive_services_link, + azurerm_private_dns_zone_virtual_network_link.plz_ai_services_link, + azurerm_private_dns_zone_virtual_network_link.plz_openai_link, + azurerm_private_dns_zone_virtual_network_link.plz_cosmos_db_link, + azurerm_storage_account.storage_account, + azurerm_virtual_network.vnet + ] + + name = "${azurerm_storage_account.storage_account.name}-private-endpoint" + location = var.region + resource_group_name = azurerm_resource_group.rg.name + subnet_id = azurerm_subnet.subnet_pe.id + + private_service_connection { + name = "${azurerm_storage_account.storage_account.name}-private-link-service-connection" + private_connection_resource_id = azurerm_storage_account.storage_account.id + subresource_names = [ + "blob" + ] + is_manual_connection = false + } + + private_dns_zone_group { + name = "${azurerm_storage_account.storage_account.name}-dns-config" + private_dns_zone_ids = [ + azurerm_private_dns_zone.plz_storage_blob.id + ] + } +} + +resource "azurerm_private_endpoint" "pe_cosmosdb" { + depends_on = [ + azurerm_private_endpoint.pe_storage, + azurerm_cosmosdb_account.cosmosdb, + azurerm_virtual_network.vnet + ] + + name = "${azurerm_cosmosdb_account.cosmosdb.name}-private-endpoint" + location = var.region + resource_group_name = azurerm_resource_group.rg.name + subnet_id = azurerm_subnet.subnet_pe.id + + private_service_connection { + name = "${azurerm_cosmosdb_account.cosmosdb.name}-private-link-service-connection" + private_connection_resource_id = azurerm_cosmosdb_account.cosmosdb.id + subresource_names = [ + "Sql" + ] + is_manual_connection = false + } + + private_dns_zone_group { + name = "${azurerm_cosmosdb_account.cosmosdb.name}-dns-config" + private_dns_zone_ids = [ + azurerm_private_dns_zone.plz_cosmos_db.id + ] + } +} + +resource "azurerm_private_endpoint" "pe_aisearch" { + depends_on = [ + azurerm_private_endpoint.pe_cosmosdb, + azapi_resource.ai_search, + azurerm_virtual_network.vnet + ] + + name = "${azapi_resource.ai_search.name}-private-endpoint" + location = var.region + resource_group_name = azurerm_resource_group.rg.name + subnet_id = azurerm_subnet.subnet_pe.id + + private_service_connection { + name = "${azapi_resource.ai_search.name}-private-link-service-connection" + private_connection_resource_id = azapi_resource.ai_search.id + subresource_names = [ + "searchService" + ] + is_manual_connection = false + } + + private_dns_zone_group { + name = "${azapi_resource.ai_search.name}-dns-config" + private_dns_zone_ids = [ + azurerm_private_dns_zone.plz_ai_search.id + ] + } +} + +resource "azurerm_private_endpoint" "pe_aifoundry" { + depends_on = [ + azurerm_private_endpoint.pe_aisearch, + azapi_resource.ai_foundry, + azurerm_virtual_network.vnet + ] + + name = "${azapi_resource.ai_foundry.name}-private-endpoint" + location = var.region + resource_group_name = azurerm_resource_group.rg.name + subnet_id = azurerm_subnet.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-config" + private_dns_zone_ids = [ + azurerm_private_dns_zone.plz_cognitive_services.id, + azurerm_private_dns_zone.plz_ai_services.id, + azurerm_private_dns_zone.plz_openai.id + ] + } +} + +########## Create the AI Foundry project, project connections, role assignments, and project-level capability host +########## + +## Create AI Foundry project +## +resource "azapi_resource" "ai_foundry_project" { + depends_on = [ + azapi_resource.ai_foundry, + azurerm_private_endpoint.pe_storage, + azurerm_private_endpoint.pe_cosmosdb, + azurerm_private_endpoint.pe_aisearch, + azurerm_private_endpoint.pe_aifoundry + ] + + type = "Microsoft.CognitiveServices/accounts/projects@2025-06-01" + name = "project${random_string.unique.result}" + parent_id = azapi_resource.ai_foundry.id + location = var.region + schema_validation_enabled = false + + body = { + sku = { + name = "S0" + } + identity = { + type = "SystemAssigned" + } + + properties = { + displayName = "project" + description = "A project for the AI Foundry account with network secured deployed Agent" + } + } + + response_export_values = [ + "identity.principalId", + "properties.internalId" + ] +} + +## Create project ID GUID from internal ID +## +locals { + project_id_guid = "${substr(azapi_resource.ai_foundry_project.output.properties.internalId, 0, 8)}-${substr(azapi_resource.ai_foundry_project.output.properties.internalId, 8, 4)}-${substr(azapi_resource.ai_foundry_project.output.properties.internalId, 12, 4)}-${substr(azapi_resource.ai_foundry_project.output.properties.internalId, 16, 4)}-${substr(azapi_resource.ai_foundry_project.output.properties.internalId, 20, 12)}" +} + +## Wait 10 seconds for the AI Foundry project system-assigned managed identity to be created and to replicate +## through Entra ID +resource "time_sleep" "wait_project_identities" { + depends_on = [ + azapi_resource.ai_foundry_project + ] + create_duration = "10s" +} + +## Create AI Foundry project connections +## +resource "azapi_resource" "conn_cosmosdb" { + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-06-01" + name = azurerm_cosmosdb_account.cosmosdb.name + parent_id = azapi_resource.ai_foundry_project.id + schema_validation_enabled = false + + depends_on = [ + azapi_resource.ai_foundry_project + ] + + body = { + name = azurerm_cosmosdb_account.cosmosdb.name + properties = { + category = "CosmosDb" + target = azurerm_cosmosdb_account.cosmosdb.endpoint + authType = "AAD" + metadata = { + ApiType = "Azure" + ResourceId = azurerm_cosmosdb_account.cosmosdb.id + location = var.region + } + } + } +} + +## Create the AI Foundry project connection to Azure Storage Account +## +resource "azapi_resource" "conn_storage" { + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-06-01" + name = azurerm_storage_account.storage_account.name + parent_id = azapi_resource.ai_foundry_project.id + schema_validation_enabled = false + + depends_on = [ + azapi_resource.ai_foundry_project + ] + + body = { + name = azurerm_storage_account.storage_account.name + properties = { + category = "AzureStorageAccount" + target = azurerm_storage_account.storage_account.primary_blob_endpoint + authType = "AAD" + metadata = { + ApiType = "Azure" + ResourceId = azurerm_storage_account.storage_account.id + location = var.region + } + } + } + + response_export_values = [ + "identity.principalId" + ] +} + +## Create the AI Foundry project connection to AI Search +## +resource "azapi_resource" "conn_aisearch" { + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-06-01" + name = azapi_resource.ai_search.name + parent_id = azapi_resource.ai_foundry_project.id + schema_validation_enabled = false + + depends_on = [ + azapi_resource.ai_foundry_project + ] + + body = { + name = azapi_resource.ai_search.name + properties = { + category = "CognitiveSearch" + target = "https://${azapi_resource.ai_search.name}.search.windows.net" + authType = "AAD" + metadata = { + ApiType = "Azure" + ApiVersion = "2025-05-01-preview" + ResourceId = azapi_resource.ai_search.id + location = var.region + } + } + } + + response_export_values = [ + "identity.principalId" + ] +} + +## 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 = [ + resource.time_sleep.wait_project_identities + ] + name = uuidv5("dns", "${azapi_resource.ai_foundry_project.name}${azapi_resource.ai_foundry_project.output.identity.principalId}${azurerm_resource_group.rg.name}cosmosdboperator") + scope = azurerm_cosmosdb_account.cosmosdb.id + role_definition_name = "Cosmos DB Operator" + principal_id = azapi_resource.ai_foundry_project.output.identity.principalId +} + +resource "azurerm_role_assignment" "storage_blob_data_contributor_ai_foundry_project" { + depends_on = [ + resource.time_sleep.wait_project_identities + ] + name = uuidv5("dns", "${azapi_resource.ai_foundry_project.name}${azapi_resource.ai_foundry_project.output.identity.principalId}${azurerm_storage_account.storage_account.name}storageblobdatacontributor") + scope = azurerm_storage_account.storage_account.id + role_definition_name = "Storage Blob Data Contributor" + principal_id = azapi_resource.ai_foundry_project.output.identity.principalId +} + +resource "azurerm_role_assignment" "search_index_data_contributor_ai_foundry_project" { + depends_on = [ + resource.time_sleep.wait_project_identities + ] + name = uuidv5("dns", "${azapi_resource.ai_foundry_project.name}${azapi_resource.ai_foundry_project.output.identity.principalId}${azapi_resource.ai_search.name}searchindexdatacontributor") + scope = azapi_resource.ai_search.id + role_definition_name = "Search Index Data Contributor" + principal_id = azapi_resource.ai_foundry_project.output.identity.principalId +} + +resource "azurerm_role_assignment" "search_service_contributor_ai_foundry_project" { + depends_on = [ + resource.time_sleep.wait_project_identities + ] + name = uuidv5("dns", "${azapi_resource.ai_foundry_project.name}${azapi_resource.ai_foundry_project.output.identity.principalId}${azapi_resource.ai_search.name}searchservicecontributor") + scope = azapi_resource.ai_search.id + role_definition_name = "Search Service Contributor" + principal_id = azapi_resource.ai_foundry_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_aisearch, + azapi_resource.conn_cosmosdb, + azapi_resource.conn_storage, + time_sleep.wait_rbac + ] + type = "Microsoft.CognitiveServices/accounts/projects/capabilityHosts@2025-04-01-preview" + name = "caphostproj" + parent_id = azapi_resource.ai_foundry_project.id + schema_validation_enabled = false + + body = { + properties = { + capabilityHostKind = "Agents" + vectorStoreConnections = [ + azapi_resource.ai_search.name + ] + storageConnections = [ + azurerm_storage_account.storage_account.name + ] + threadStorageConnections = [ + azurerm_cosmosdb_account.cosmosdb.name + ] + } + } +} + +## Create the necessary data plane role assignments to the CosmosDb databases created by the AI Foundry Project +## +resource "azurerm_cosmosdb_sql_role_assignment" "cosmosdb_db_sql_role_aifp_user_thread_message_store" { + depends_on = [ + azapi_resource.ai_foundry_project_capability_host + ] + name = uuidv5("dns", "${azapi_resource.ai_foundry_project.name}${azapi_resource.ai_foundry_project.output.identity.principalId}userthreadmessage_dbsqlrole") + resource_group_name = azurerm_resource_group.rg.name + account_name = azurerm_cosmosdb_account.cosmosdb.name + scope = "${azurerm_cosmosdb_account.cosmosdb.id}/dbs/enterprise_memory/colls/${local.project_id_guid}-thread-message-store" + role_definition_id = "${azurerm_cosmosdb_account.cosmosdb.id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002" + principal_id = azapi_resource.ai_foundry_project.output.identity.principalId +} + +resource "azurerm_cosmosdb_sql_role_assignment" "cosmosdb_db_sql_role_aifp_system_thread_name" { + depends_on = [ + azurerm_cosmosdb_sql_role_assignment.cosmosdb_db_sql_role_aifp_user_thread_message_store + ] + name = uuidv5("dns", "${azapi_resource.ai_foundry_project.name}${azapi_resource.ai_foundry_project.output.identity.principalId}systemthread_dbsqlrole") + resource_group_name = azurerm_resource_group.rg.name + account_name = azurerm_cosmosdb_account.cosmosdb.name + scope = "${azurerm_cosmosdb_account.cosmosdb.id}/dbs/enterprise_memory/colls/${local.project_id_guid}-system-thread-message-store" + role_definition_id = "${azurerm_cosmosdb_account.cosmosdb.id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002" + principal_id = azapi_resource.ai_foundry_project.output.identity.principalId +} + +resource "azurerm_cosmosdb_sql_role_assignment" "cosmosdb_db_sql_role_aifp_entity_store_name" { + depends_on = [ + azurerm_cosmosdb_sql_role_assignment.cosmosdb_db_sql_role_aifp_system_thread_name + ] + name = uuidv5("dns", "${azapi_resource.ai_foundry_project.name}${azapi_resource.ai_foundry_project.output.identity.principalId}entitystore_dbsqlrole") + resource_group_name = azurerm_resource_group.rg.name + account_name = azurerm_cosmosdb_account.cosmosdb.name + scope = "${azurerm_cosmosdb_account.cosmosdb.id}/dbs/enterprise_memory/colls/${local.project_id_guid}-agent-entity-store" + role_definition_id = "${azurerm_cosmosdb_account.cosmosdb.id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002" + principal_id = azapi_resource.ai_foundry_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_foundry_project.name}${azapi_resource.ai_foundry_project.output.identity.principalId}${azurerm_storage_account.storage_account.name}storageblobdataowner") + scope = azurerm_storage_account.storage_account.id + role_definition_name = "Storage Blob Data Owner" + principal_id = azapi_resource.ai_foundry_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 +} + +## 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/${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" + 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] +} diff --git a/quickstart/301-azure-ai-foundry-account-standard-agent/outputs.tf b/quickstart/301-azure-ai-foundry-account-standard-agent/outputs.tf new file mode 100644 index 000000000..0508e3d15 --- /dev/null +++ b/quickstart/301-azure-ai-foundry-account-standard-agent/outputs.tf @@ -0,0 +1,7 @@ +output "ai_foundry_account_name" { + value = azapi_resource.ai_foundry.name +} + +output "ai_foundry_project_name" { + value = azapi_resource.ai_foundry_project.name +} \ No newline at end of file diff --git a/quickstart/301-azure-ai-foundry-account-standard-agent/providers.tf b/quickstart/301-azure-ai-foundry-account-standard-agent/providers.tf new file mode 100644 index 000000000..bc7b2c4c9 --- /dev/null +++ b/quickstart/301-azure-ai-foundry-account-standard-agent/providers.tf @@ -0,0 +1,30 @@ +terraform { + required_version = ">= 1.10.0, < 2.0.0" + required_providers { + azapi = { + source = "azure/azapi" + version = "~> 2.5" + } + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.37" + } + random = { + source = "hashicorp/random" + version = "~> 3.7" + } + time = { + source = "hashicorp/time" + version = "~> 0.13" + } + } +} + +# Setup providers +provider "azapi" { +} + +provider "azurerm" { + features {} + storage_use_azuread = true +} \ No newline at end of file diff --git a/quickstart/301-azure-ai-foundry-account-standard-agent/variables.tf b/quickstart/301-azure-ai-foundry-account-standard-agent/variables.tf new file mode 100644 index 000000000..5928dfae8 --- /dev/null +++ b/quickstart/301-azure-ai-foundry-account-standard-agent/variables.tf @@ -0,0 +1,23 @@ +## As of 6/2025 this is limited to RFC1918 Class B and Class C address space +variable "virtual_network_address_space" { + description = "The address space for the virtual network" + type = string + default = "192.168.0.0/16" +} + +variable "agent_subnet_address_prefix" { + description = "The address prefix for the subnet that will be delegated to the Standard Agent" + type = string + default = "192.168.0.0/24" +} + +variable "private_endpoint_subnet_address_prefix" { + description = "The address prefix for the subnet that contains the private endpoints" + type = string + default = "192.168.1.0/24" +} + +variable "region" { + description = "The name of the Azure region to provision the resources to" + type = string +} diff --git a/quickstart/302-azure-ai-foundry-account-standard-agent-byovnet/README.md b/quickstart/302-azure-ai-foundry-account-standard-agent-byovnet/README.md new file mode 100644 index 000000000..fdad5becb --- /dev/null +++ b/quickstart/302-azure-ai-foundry-account-standard-agent-byovnet/README.md @@ -0,0 +1,5 @@ +# Azure AI Foundry Account with Standard Agent and Bring-Your-Own-Virtual Network + +This deployment configuration specifies an [Azure AI Foundry Account](https://learn.microsoft.com/en-us/azure/ai-foundry/what-is-azure-ai-foundry) and a deployment of the OpenAI 4.1 model. An AI Foundry Project is deployed within the AI Foundry Account. The AI Foundry Account and Project are configured to support the Agent Service with a [standard agent](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/concepts/standard-agent-setup) in a [bring-your-own-vnet setup](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/how-to/virtual-networks). + +This configuration describes the minimal set of resources you require to get started with Azure AI Foundry Accounts, Projects, and Agent Service configured in a Standard Agent configuration. \ No newline at end of file diff --git a/quickstart/302-azure-ai-foundry-account-standard-agent-byovnet/main.tf b/quickstart/302-azure-ai-foundry-account-standard-agent-byovnet/main.tf new file mode 100644 index 000000000..9f5e97832 --- /dev/null +++ b/quickstart/302-azure-ai-foundry-account-standard-agent-byovnet/main.tf @@ -0,0 +1,642 @@ +########## Create infrastructure resources +########## + +## Create a random string +## +resource "random_string" "unique" { + length = 4 + min_numeric = 4 + numeric = true + special = false + lower = true + upper = false +} + +########## Create resoures required to for agent data storage +########## + +## Create a storage account for agent data +## +resource "azurerm_storage_account" "storage_account" { + provider = azurerm.workload_subscription + + name = "aifoundry${random_string.unique.result}storage" + resource_group_name = var.resource_group_name_resources + location = var.region + + 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 + network_rules { + default_action = "Deny" + bypass = [ + "AzureServices" + ] + } +} + +## Create the Cosmos DB account to store agent threads +## +resource "azurerm_cosmosdb_account" "cosmosdb" { + provider = azurerm.workload_subscription + + name = "aifoundry${random_string.unique.result}cosmosdb" + location = var.region + resource_group_name = var.resource_group_name_resources + + # 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.region + failover_priority = 0 + zone_redundant = false + } +} + +## Create an AI Search instance that will be used to store vector embeddings +## +resource "azapi_resource" "ai_search" { + provider = azapi.workload_subscription + + type = "Microsoft.Search/searchServices@2025-05-01" + name = "aifoundry${random_string.unique.result}search" + parent_id = "/subscriptions/${var.subscription_id_resources}/resourceGroups/${var.resource_group_name_resources}" + location = var.region + schema_validation_enabled = true + + 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 +########## + +## Create the AI Foundry resource +## +resource "azapi_resource" "ai_foundry" { + provider = azapi.workload_subscription + + type = "Microsoft.CognitiveServices/accounts@2025-06-01" + name = "aifoundry${random_string.unique.result}" + parent_id = "/subscriptions/${var.subscription_id_resources}/resourceGroups/${var.resource_group_name_resources}" + location = var.region + schema_validation_enabled = false + + body = { + kind = "AIServices", + sku = { + name = "S0" + } + identity = { + type = "SystemAssigned" + } + + properties = { + # Support both Entra ID and API Key authentication for underlining 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 = "aifoundry${random_string.unique.result}" + + # Network-related controls + # Disable public access but allow Trusted Azure Services exception + publicNetworkAccess = "Disabled" + networkAcls = { + defaultAction = "Allow" + } + + # Enable VNet injection for Standard Agents + networkInjections = [ + { + scenario = "agent" + subnetArmId = var.subnet_id_agent + useMicrosoftManagedNetwork = false + } + ] + } + } +} + +## Create a deployment for OpenAI's GPT-4o in the AI Foundry resource +## +resource "azurerm_cognitive_deployment" "aifoundry_deployment_gpt_4o" { + provider = azurerm.workload_subscription + + depends_on = [ + azapi_resource.ai_foundry + ] + + name = "gpt-4o" + cognitive_account_id = azapi_resource.ai_foundry.id + + sku { + name = "GlobalStandard" + capacity = 1 + } + + model { + format = "OpenAI" + name = "gpt-4o" + version = "2024-11-20" + } +} + +########## Create Private DNS Zones, Links, and Private Endpoints +########## + +## Create Private Endpoints for resources +## +resource "azurerm_private_endpoint" "pe_storage" { + provider = azurerm.workload_subscription + + depends_on = [ + azurerm_storage_account.storage_account + ] + + name = "${azurerm_storage_account.storage_account.name}-private-endpoint" + location = var.region + resource_group_name = var.resource_group_name_resources + subnet_id = var.subnet_id_private_endpoint + private_service_connection { + name = "${azurerm_storage_account.storage_account.name}-private-link-service-connection" + private_connection_resource_id = azurerm_storage_account.storage_account.id + subresource_names = [ + "blob" + ] + is_manual_connection = false + } + + private_dns_zone_group { + name = "${azurerm_storage_account.storage_account.name}-dns-config" + private_dns_zone_ids = [ + "/subscriptions/${var.subscription_id_infra}/resourceGroups/${var.resource_group_name_dns}/providers/Microsoft.Network/privateDnsZones/privatelink.blob.core.windows.net" + ] + } +} + +resource "azurerm_private_endpoint" "pe_cosmosdb" { + provider = azurerm.workload_subscription + + depends_on = [ + azurerm_private_endpoint.pe_storage, + azurerm_cosmosdb_account.cosmosdb + ] + + name = "${azurerm_cosmosdb_account.cosmosdb.name}-private-endpoint" + location = var.region + resource_group_name = var.resource_group_name_resources + subnet_id = var.subnet_id_private_endpoint + + private_service_connection { + name = "${azurerm_cosmosdb_account.cosmosdb.name}-private-link-service-connection" + private_connection_resource_id = azurerm_cosmosdb_account.cosmosdb.id + subresource_names = [ + "Sql" + ] + is_manual_connection = false + } + + private_dns_zone_group { + name = "${azurerm_cosmosdb_account.cosmosdb.name}-dns-config" + private_dns_zone_ids = [ + "/subscriptions/${var.subscription_id_infra}/resourceGroups/${var.resource_group_name_dns}/providers/Microsoft.Network/privateDnsZones/privatelink.documents.azure.com" + ] + } +} + +resource "azurerm_private_endpoint" "pe_aisearch" { + provider = azurerm.workload_subscription + + depends_on = [ + azurerm_private_endpoint.pe_cosmosdb, + azapi_resource.ai_search + ] + + name = "${azapi_resource.ai_search.name}-private-endpoint" + location = var.region + resource_group_name = var.resource_group_name_resources + subnet_id = var.subnet_id_private_endpoint + + private_service_connection { + name = "${azapi_resource.ai_search.name}-private-link-service-connection" + private_connection_resource_id = azapi_resource.ai_search.id + subresource_names = [ + "searchService" + ] + is_manual_connection = false + } + + private_dns_zone_group { + name = "${azapi_resource.ai_search.name}-dns-config" + private_dns_zone_ids = [ + "/subscriptions/${var.subscription_id_infra}/resourceGroups/${var.resource_group_name_dns}/providers/Microsoft.Network/privateDnsZones/privatelink.search.windows.net" + ] + } +} + +resource "azurerm_private_endpoint" "pe_aifoundry" { + provider = azurerm.workload_subscription + + depends_on = [ + azurerm_private_endpoint.pe_aisearch, + azapi_resource.ai_foundry + ] + + name = "${azapi_resource.ai_foundry.name}-private-endpoint" + location = var.region + resource_group_name = var.resource_group_name_resources + subnet_id = var.subnet_id_private_endpoint + + 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-config" + private_dns_zone_ids = [ + "/subscriptions/${var.subscription_id_infra}/resourceGroups/${var.resource_group_name_dns}/providers/Microsoft.Network/privateDnsZones/privatelink.cognitiveservices.azure.com", + "/subscriptions/${var.subscription_id_infra}/resourceGroups/${var.resource_group_name_dns}/providers/Microsoft.Network/privateDnsZones/privatelink.services.ai.azure.com", + "/subscriptions/${var.subscription_id_infra}/resourceGroups/${var.resource_group_name_dns}/providers/Microsoft.Network/privateDnsZones/privatelink.openai.azure.com" + ] + } +} + +########## Create the AI Foundry project, project connections, role assignments, and project-level capability host +########## + +## Create AI Foundry project +## +resource "azapi_resource" "ai_foundry_project" { + provider = azapi.workload_subscription + + depends_on = [ + azapi_resource.ai_foundry, + azurerm_private_endpoint.pe_storage, + azurerm_private_endpoint.pe_cosmosdb, + azurerm_private_endpoint.pe_aisearch, + azurerm_private_endpoint.pe_aifoundry + ] + + type = "Microsoft.CognitiveServices/accounts/projects@2025-06-01" + name = "project${random_string.unique.result}" + parent_id = azapi_resource.ai_foundry.id + location = var.region + schema_validation_enabled = false + + body = { + sku = { + name = "S0" + } + identity = { + type = "SystemAssigned" + } + + properties = { + displayName = "project" + description = "A project for the AI Foundry account with network secured deployed Agent" + } + } + + response_export_values = [ + "identity.principalId", + "properties.internalId" + ] +} + +## Create project ID GUID from internal ID +## +locals { + project_id_guid = "${substr(azapi_resource.ai_foundry_project.output.properties.internalId, 0, 8)}-${substr(azapi_resource.ai_foundry_project.output.properties.internalId, 8, 4)}-${substr(azapi_resource.ai_foundry_project.output.properties.internalId, 12, 4)}-${substr(azapi_resource.ai_foundry_project.output.properties.internalId, 16, 4)}-${substr(azapi_resource.ai_foundry_project.output.properties.internalId, 20, 12)}" +} + + +## Wait 10 seconds for the AI Foundry project system-assigned managed identity to be created and to replicate +## through Entra ID +resource "time_sleep" "wait_project_identities" { + depends_on = [ + azapi_resource.ai_foundry_project + ] + create_duration = "10s" +} + +## Create AI Foundry project connections +## +resource "azapi_resource" "conn_cosmosdb" { + provider = azapi.workload_subscription + + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-06-01" + name = azurerm_cosmosdb_account.cosmosdb.name + parent_id = azapi_resource.ai_foundry_project.id + schema_validation_enabled = false + + depends_on = [ + azapi_resource.ai_foundry_project + ] + + body = { + name = azurerm_cosmosdb_account.cosmosdb.name + properties = { + category = "CosmosDb" + target = azurerm_cosmosdb_account.cosmosdb.endpoint + authType = "AAD" + metadata = { + ApiType = "Azure" + ResourceId = azurerm_cosmosdb_account.cosmosdb.id + location = var.region + } + } + } +} + +## Create the AI Foundry project connection to Azure Storage Account +## +resource "azapi_resource" "conn_storage" { + provider = azapi.workload_subscription + + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-06-01" + name = azurerm_storage_account.storage_account.name + parent_id = azapi_resource.ai_foundry_project.id + schema_validation_enabled = false + + depends_on = [ + azapi_resource.ai_foundry_project + ] + + body = { + name = azurerm_storage_account.storage_account.name + properties = { + category = "AzureStorageAccount" + target = azurerm_storage_account.storage_account.primary_blob_endpoint + authType = "AAD" + metadata = { + ApiType = "Azure" + ResourceId = azurerm_storage_account.storage_account.id + location = var.region + } + } + } + + response_export_values = [ + "identity.principalId" + ] +} + +## Create the AI Foundry project connection to AI Search +## +resource "azapi_resource" "conn_aisearch" { + provider = azapi.workload_subscription + + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-06-01" + name = azapi_resource.ai_search.name + parent_id = azapi_resource.ai_foundry_project.id + schema_validation_enabled = false + + depends_on = [ + azapi_resource.ai_foundry_project + ] + + body = { + name = azapi_resource.ai_search.name + properties = { + category = "CognitiveSearch" + target = "https://${azapi_resource.ai_search.name}.search.windows.net" + authType = "AAD" + metadata = { + ApiType = "Azure" + ApiVersion = "2025-05-01-preview" + ResourceId = azapi_resource.ai_search.id + location = var.region + } + } + } + + response_export_values = [ + "identity.principalId" + ] +} + +## 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" { + provider = azurerm.workload_subscription + + depends_on = [ + resource.time_sleep.wait_project_identities + ] + name = uuidv5("dns", "${azapi_resource.ai_foundry_project.name}${azapi_resource.ai_foundry_project.output.identity.principalId}${var.resource_group_name_resources}cosmosdboperator") + scope = azurerm_cosmosdb_account.cosmosdb.id + role_definition_name = "Cosmos DB Operator" + principal_id = azapi_resource.ai_foundry_project.output.identity.principalId +} + +resource "azurerm_role_assignment" "storage_blob_data_contributor_ai_foundry_project" { + provider = azurerm.workload_subscription + + depends_on = [ + resource.time_sleep.wait_project_identities + ] + name = uuidv5("dns", "${azapi_resource.ai_foundry_project.name}${azapi_resource.ai_foundry_project.output.identity.principalId}${azurerm_storage_account.storage_account.name}storageblobdatacontributor") + scope = azurerm_storage_account.storage_account.id + role_definition_name = "Storage Blob Data Contributor" + principal_id = azapi_resource.ai_foundry_project.output.identity.principalId +} + +resource "azurerm_role_assignment" "search_index_data_contributor_ai_foundry_project" { + provider = azurerm.workload_subscription + + depends_on = [ + resource.time_sleep.wait_project_identities + ] + name = uuidv5("dns", "${azapi_resource.ai_foundry_project.name}${azapi_resource.ai_foundry_project.output.identity.principalId}${azapi_resource.ai_search.name}searchindexdatacontributor") + scope = azapi_resource.ai_search.id + role_definition_name = "Search Index Data Contributor" + principal_id = azapi_resource.ai_foundry_project.output.identity.principalId +} + +resource "azurerm_role_assignment" "search_service_contributor_ai_foundry_project" { + provider = azurerm.workload_subscription + + depends_on = [ + resource.time_sleep.wait_project_identities + ] + name = uuidv5("dns", "${azapi_resource.ai_foundry_project.name}${azapi_resource.ai_foundry_project.output.identity.principalId}${azapi_resource.ai_search.name}searchservicecontributor") + scope = azapi_resource.ai_search.id + role_definition_name = "Search Service Contributor" + principal_id = azapi_resource.ai_foundry_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" { + provider = azapi.workload_subscription + + depends_on = [ + azapi_resource.conn_aisearch, + azapi_resource.conn_cosmosdb, + azapi_resource.conn_storage, + time_sleep.wait_rbac + ] + type = "Microsoft.CognitiveServices/accounts/projects/capabilityHosts@2025-04-01-preview" + name = "caphostproj" + parent_id = azapi_resource.ai_foundry_project.id + schema_validation_enabled = false + + body = { + properties = { + capabilityHostKind = "Agents" + vectorStoreConnections = [ + azapi_resource.ai_search.name + ] + storageConnections = [ + azurerm_storage_account.storage_account.name + ] + threadStorageConnections = [ + azurerm_cosmosdb_account.cosmosdb.name + ] + } + } +} + +## Create the necessary data plane role assignments to the CosmosDb databases created by the AI Foundry Project +## +resource "azurerm_cosmosdb_sql_role_assignment" "cosmosdb_db_sql_role_aifp_user_thread_message_store" { + provider = azurerm.workload_subscription + + depends_on = [ + azapi_resource.ai_foundry_project_capability_host + ] + name = uuidv5("dns", "${azapi_resource.ai_foundry_project.name}${azapi_resource.ai_foundry_project.output.identity.principalId}userthreadmessage_dbsqlrole") + resource_group_name = var.resource_group_name_resources + account_name = azurerm_cosmosdb_account.cosmosdb.name + scope = "${azurerm_cosmosdb_account.cosmosdb.id}/dbs/enterprise_memory/colls/${local.project_id_guid}-thread-message-store" + role_definition_id = "${azurerm_cosmosdb_account.cosmosdb.id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002" + principal_id = azapi_resource.ai_foundry_project.output.identity.principalId +} + +resource "azurerm_cosmosdb_sql_role_assignment" "cosmosdb_db_sql_role_aifp_system_thread_name" { + provider = azurerm.workload_subscription + + depends_on = [ + azurerm_cosmosdb_sql_role_assignment.cosmosdb_db_sql_role_aifp_user_thread_message_store + ] + name = uuidv5("dns", "${azapi_resource.ai_foundry_project.name}${azapi_resource.ai_foundry_project.output.identity.principalId}systemthread_dbsqlrole") + resource_group_name = var.resource_group_name_resources + account_name = azurerm_cosmosdb_account.cosmosdb.name + scope = "${azurerm_cosmosdb_account.cosmosdb.id}/dbs/enterprise_memory/colls/${local.project_id_guid}-system-thread-message-store" + role_definition_id = "${azurerm_cosmosdb_account.cosmosdb.id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002" + principal_id = azapi_resource.ai_foundry_project.output.identity.principalId +} + +resource "azurerm_cosmosdb_sql_role_assignment" "cosmosdb_db_sql_role_aifp_entity_store_name" { + provider = azurerm.workload_subscription + + depends_on = [ + azurerm_cosmosdb_sql_role_assignment.cosmosdb_db_sql_role_aifp_system_thread_name + ] + name = uuidv5("dns", "${azapi_resource.ai_foundry_project.name}${azapi_resource.ai_foundry_project.output.identity.principalId}entitystore_dbsqlrole") + resource_group_name = var.resource_group_name_resources + account_name = azurerm_cosmosdb_account.cosmosdb.name + scope = "${azurerm_cosmosdb_account.cosmosdb.id}/dbs/enterprise_memory/colls/${local.project_id_guid}-agent-entity-store" + role_definition_id = "${azurerm_cosmosdb_account.cosmosdb.id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002" + principal_id = azapi_resource.ai_foundry_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" { + provider = azurerm.workload_subscription + + depends_on = [ + azapi_resource.ai_foundry_project_capability_host + ] + name = uuidv5("dns", "${azapi_resource.ai_foundry_project.name}${azapi_resource.ai_foundry_project.output.identity.principalId}${azurerm_storage_account.storage_account.name}storageblobdataowner") + scope = azurerm_storage_account.storage_account.id + role_definition_name = "Storage Blob Data Owner" + principal_id = azapi_resource.ai_foundry_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 +} diff --git a/quickstart/302-azure-ai-foundry-account-standard-agent-byovnet/outputs.tf b/quickstart/302-azure-ai-foundry-account-standard-agent-byovnet/outputs.tf new file mode 100644 index 000000000..0508e3d15 --- /dev/null +++ b/quickstart/302-azure-ai-foundry-account-standard-agent-byovnet/outputs.tf @@ -0,0 +1,7 @@ +output "ai_foundry_account_name" { + value = azapi_resource.ai_foundry.name +} + +output "ai_foundry_project_name" { + value = azapi_resource.ai_foundry_project.name +} \ No newline at end of file diff --git a/quickstart/302-azure-ai-foundry-account-standard-agent-byovnet/providers.tf b/quickstart/302-azure-ai-foundry-account-standard-agent-byovnet/providers.tf new file mode 100644 index 000000000..2abbc96e6 --- /dev/null +++ b/quickstart/302-azure-ai-foundry-account-standard-agent-byovnet/providers.tf @@ -0,0 +1,54 @@ +terraform { + required_version = ">= 1.10.0, < 2.0.0" + required_providers { + azapi = { + source = "azure/azapi" + version = "~> 2.5" + } + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.37" + } + random = { + source = "hashicorp/random" + version = "~> 3.7" + } + time = { + source = "hashicorp/time" + version = "~> 0.13" + } + } +} + +# Setup providers +provider "azapi" { +} + +provider "azapi" { + alias = "workload_subscription" + subscription_id = var.subscription_id_resources +} + +provider "azapi" { + alias = "infra_subscription" + subscription_id = var.subscription_id_infra +} + +provider "azurerm" { + features {} + storage_use_azuread = true +} + +provider "azurerm" { + alias = "workload_subscription" + subscription_id = var.subscription_id_resources + features {} + storage_use_azuread = true +} + +provider "azurerm" { + alias = "infra_subscription" + subscription_id = var.subscription_id_infra + features {} + storage_use_azuread = true +} \ No newline at end of file diff --git a/quickstart/302-azure-ai-foundry-account-standard-agent-byovnet/variables.tf b/quickstart/302-azure-ai-foundry-account-standard-agent-byovnet/variables.tf new file mode 100644 index 000000000..100b3165c --- /dev/null +++ b/quickstart/302-azure-ai-foundry-account-standard-agent-byovnet/variables.tf @@ -0,0 +1,34 @@ +variable "resource_group_name_resources" { + description = "The name of the existing resource group to deploy the resources into" + type = string +} + +variable "resource_group_name_dns" { + description = "The name of the existing resource group where the Private DNS Zones have been created" + type = string +} + +variable "subnet_id_agent" { + description = "The resource id of the subnet that has been delegated to Microsoft.Apps/environments" + type = string +} + +variable "subnet_id_private_endpoint" { + description = "The resource id of the subnet that will be used to deploy Private Endpoints to" + type = string +} + +variable "subscription_id_infra" { + description = "The subscription id where the Private DNS Zones are located" + type = string +} + +variable "subscription_id_resources" { + description = "The subscription id where the resources will be deployed" + type = string +} + +variable "region" { + description = "The name of the Azure region to provision the resources to" + type = string +}