From c6b8dabb45068955d22b4c482bf60b4669a4cee0 Mon Sep 17 00:00:00 2001 From: Dennis Eikelenboom Date: Mon, 23 Mar 2026 11:41:32 -0700 Subject: [PATCH] terraform samples to match bicep --- .../00-basic-azurerm/.skip-tf-validation | 3 + .../00-basic-azurerm/README.md | 10 +- .../00-basic-azurerm/code/example.tfvars | 2 +- .../00-basic/README.md | 102 ---- .../00-basic/code/example.tfvars | 2 +- .../00-basic/code/main.tf | 4 +- .../01-connections/.skip-tf-validation | 4 + .../01-connections/README.md | 37 ++ .../01-connections/connection-ai-search.tf | 108 ++++ .../01-connections/connection-key-vault.tf | 88 +++ .../connection-storage-account.tf | 76 +++ .../02-storage-speech-language/README.md | 36 ++ .../code/example.tfvars | 18 + .../02-storage-speech-language/code/main.tf | 105 ++++ .../code/outputs.tf | 19 + .../code/providers.tf | 8 + .../code/variables.tf | 29 + .../code/versions.tf | 22 + .../03-custom-dns/README.md | 55 ++ .../03-custom-dns/code/example.tfvars | 8 + .../03-custom-dns/code/main.tf | 83 +++ .../03-custom-dns/code/outputs.tf | 19 + .../03-custom-dns/code/providers.tf | 8 + .../03-custom-dns/code/variables.tf | 29 + .../03-custom-dns/code/versions.tf | 18 + .../04-disable-local-auth/README.md | 54 ++ .../04-disable-local-auth/code/example.tfvars | 9 + .../04-disable-local-auth/code/main.tf | 83 +++ .../04-disable-local-auth/code/outputs.tf | 24 + .../04-disable-local-auth/code/providers.tf | 8 + .../04-disable-local-auth/code/variables.tf | 41 ++ .../04-disable-local-auth/code/versions.tf | 18 + .../05-custom-policy-definitions/README.md | 42 ++ .../code/example.tfvars | 14 + .../05-custom-policy-definitions/code/main.tf | 223 ++++++++ .../code/outputs.tf | 24 + .../code/providers.tf | 4 + .../code/variables.tf | 35 ++ .../code/versions.tf | 10 + .../10-private-network-basic/README.md | 60 ++ .../code/example.tfvars | 11 + .../10-private-network-basic/code/main.tf | 174 ++++++ .../10-private-network-basic/code/outputs.tf | 24 + .../code/providers.tf | 8 + .../code/variables.tf | 53 ++ .../10-private-network-basic/code/versions.tf | 18 + .../README.md | 16 +- .../code/main.tf | 2 +- .../README.md | 16 +- .../code/main.tf | 2 +- .../README.md | 60 ++ .../code/example.tfvars | 29 + .../code/main.tf | 529 ++++++++++++++++++ .../code/outputs.tf | 54 ++ .../code/providers.tf | 8 + .../code/variables.tf | 75 +++ .../code/versions.tf | 22 + .../README.md | 58 ++ .../code/example.tfvars | 22 + .../code/main.tf | 397 +++++++++++++ .../code/outputs.tf | 49 ++ .../code/providers.tf | 8 + .../code/variables.tf | 65 +++ .../code/versions.tf | 22 + .../ai-foundry.tf | 20 +- .../aisearch.tf | 2 +- .../cosmos.tf | 4 +- .../network.tf | 36 +- .../outputs.tf | 26 +- .../providers.tf | 6 +- .../storage.tf | 16 +- .../README.md | 55 ++ .../code/example.tfvars | 30 + .../code/main.tf | 387 +++++++++++++ .../code/outputs.tf | 44 ++ .../code/providers.tf | 8 + .../code/variables.tf | 75 +++ .../code/versions.tf | 22 + .../20-user-assigned-identity/README.md | 57 ++ .../code/example.tfvars | 11 + .../20-user-assigned-identity/code/main.tf | 104 ++++ .../20-user-assigned-identity/code/outputs.tf | 19 + .../code/providers.tf | 8 + .../code/variables.tf | 53 ++ .../code/versions.tf | 18 + .../25-entraid-passthrough/README.md | 58 ++ .../code/example.tfvars | 5 + .../25-entraid-passthrough/code/main.tf | 114 ++++ .../25-entraid-passthrough/code/outputs.tf | 11 + .../25-entraid-passthrough/code/providers.tf | 8 + .../25-entraid-passthrough/code/variables.tf | 35 ++ .../25-entraid-passthrough/code/versions.tf | 18 + .../30-customer-managed-keys/README.md | 65 +++ .../code/example.tfvars | 8 + .../30-customer-managed-keys/code/main.tf | 154 +++++ .../30-customer-managed-keys/code/outputs.tf | 24 + .../code/providers.tf | 8 + .../code/variables.tf | 29 + .../30-customer-managed-keys/code/versions.tf | 22 + .../README.md | 59 ++ .../code/example.tfvars | 13 + .../code/main.tf | 347 ++++++++++++ .../code/outputs.tf | 49 ++ .../code/providers.tf | 12 + .../code/variables.tf | 35 ++ .../code/versions.tf | 22 + .../README.md | 57 ++ .../code/example.tfvars | 19 + .../code/main.tf | 159 ++++++ .../code/outputs.tf | 35 ++ .../code/providers.tf | 12 + .../code/variables.tf | 37 ++ .../code/versions.tf | 22 + .../41-standard-agent-setup/README.md | 69 +++ .../code/example.tfvars | 9 + .../41-standard-agent-setup/code/main.tf | 305 ++++++++++ .../41-standard-agent-setup/code/outputs.tf | 29 + .../41-standard-agent-setup/code/providers.tf | 8 + .../41-standard-agent-setup/code/variables.tf | 35 ++ .../41-standard-agent-setup/code/versions.tf | 22 + .../45-basic-agent-bing/README.md | 62 ++ .../45-basic-agent-bing/code/example.tfvars | 11 + .../45-basic-agent-bing/code/main.tf | 142 +++++ .../45-basic-agent-bing/code/outputs.tf | 24 + .../45-basic-agent-bing/code/providers.tf | 8 + .../45-basic-agent-bing/code/variables.tf | 59 ++ .../45-basic-agent-bing/code/versions.tf | 18 + 127 files changed, 6319 insertions(+), 184 deletions(-) create mode 100644 infrastructure/infrastructure-setup-terraform/00-basic-azurerm/.skip-tf-validation delete mode 100644 infrastructure/infrastructure-setup-terraform/00-basic/README.md create mode 100644 infrastructure/infrastructure-setup-terraform/01-connections/.skip-tf-validation create mode 100644 infrastructure/infrastructure-setup-terraform/01-connections/README.md create mode 100644 infrastructure/infrastructure-setup-terraform/01-connections/connection-ai-search.tf create mode 100644 infrastructure/infrastructure-setup-terraform/01-connections/connection-key-vault.tf create mode 100644 infrastructure/infrastructure-setup-terraform/01-connections/connection-storage-account.tf create mode 100644 infrastructure/infrastructure-setup-terraform/02-storage-speech-language/README.md create mode 100644 infrastructure/infrastructure-setup-terraform/02-storage-speech-language/code/example.tfvars create mode 100644 infrastructure/infrastructure-setup-terraform/02-storage-speech-language/code/main.tf create mode 100644 infrastructure/infrastructure-setup-terraform/02-storage-speech-language/code/outputs.tf create mode 100644 infrastructure/infrastructure-setup-terraform/02-storage-speech-language/code/providers.tf create mode 100644 infrastructure/infrastructure-setup-terraform/02-storage-speech-language/code/variables.tf create mode 100644 infrastructure/infrastructure-setup-terraform/02-storage-speech-language/code/versions.tf create mode 100644 infrastructure/infrastructure-setup-terraform/03-custom-dns/README.md create mode 100644 infrastructure/infrastructure-setup-terraform/03-custom-dns/code/example.tfvars create mode 100644 infrastructure/infrastructure-setup-terraform/03-custom-dns/code/main.tf create mode 100644 infrastructure/infrastructure-setup-terraform/03-custom-dns/code/outputs.tf create mode 100644 infrastructure/infrastructure-setup-terraform/03-custom-dns/code/providers.tf create mode 100644 infrastructure/infrastructure-setup-terraform/03-custom-dns/code/variables.tf create mode 100644 infrastructure/infrastructure-setup-terraform/03-custom-dns/code/versions.tf create mode 100644 infrastructure/infrastructure-setup-terraform/04-disable-local-auth/README.md create mode 100644 infrastructure/infrastructure-setup-terraform/04-disable-local-auth/code/example.tfvars create mode 100644 infrastructure/infrastructure-setup-terraform/04-disable-local-auth/code/main.tf create mode 100644 infrastructure/infrastructure-setup-terraform/04-disable-local-auth/code/outputs.tf create mode 100644 infrastructure/infrastructure-setup-terraform/04-disable-local-auth/code/providers.tf create mode 100644 infrastructure/infrastructure-setup-terraform/04-disable-local-auth/code/variables.tf create mode 100644 infrastructure/infrastructure-setup-terraform/04-disable-local-auth/code/versions.tf create mode 100644 infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/README.md create mode 100644 infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/code/example.tfvars create mode 100644 infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/code/main.tf create mode 100644 infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/code/outputs.tf create mode 100644 infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/code/providers.tf create mode 100644 infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/code/variables.tf create mode 100644 infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/code/versions.tf create mode 100644 infrastructure/infrastructure-setup-terraform/10-private-network-basic/README.md create mode 100644 infrastructure/infrastructure-setup-terraform/10-private-network-basic/code/example.tfvars create mode 100644 infrastructure/infrastructure-setup-terraform/10-private-network-basic/code/main.tf create mode 100644 infrastructure/infrastructure-setup-terraform/10-private-network-basic/code/outputs.tf create mode 100644 infrastructure/infrastructure-setup-terraform/10-private-network-basic/code/providers.tf create mode 100644 infrastructure/infrastructure-setup-terraform/10-private-network-basic/code/variables.tf create mode 100644 infrastructure/infrastructure-setup-terraform/10-private-network-basic/code/versions.tf create mode 100644 infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/README.md create mode 100644 infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/code/example.tfvars create mode 100644 infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/code/main.tf create mode 100644 infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/code/outputs.tf create mode 100644 infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/code/providers.tf create mode 100644 infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/code/variables.tf create mode 100644 infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/code/versions.tf create mode 100644 infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/README.md create mode 100644 infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/code/example.tfvars create mode 100644 infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/code/main.tf create mode 100644 infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/code/outputs.tf create mode 100644 infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/code/providers.tf create mode 100644 infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/code/variables.tf create mode 100644 infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/code/versions.tf create mode 100644 infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/README.md create mode 100644 infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/example.tfvars create mode 100644 infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/main.tf create mode 100644 infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/outputs.tf create mode 100644 infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/providers.tf create mode 100644 infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/variables.tf create mode 100644 infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/versions.tf create mode 100644 infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/README.md create mode 100644 infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/code/example.tfvars create mode 100644 infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/code/main.tf create mode 100644 infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/code/outputs.tf create mode 100644 infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/code/providers.tf create mode 100644 infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/code/variables.tf create mode 100644 infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/code/versions.tf create mode 100644 infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/README.md create mode 100644 infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/code/example.tfvars create mode 100644 infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/code/main.tf create mode 100644 infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/code/outputs.tf create mode 100644 infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/code/providers.tf create mode 100644 infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/code/variables.tf create mode 100644 infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/code/versions.tf create mode 100644 infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/README.md create mode 100644 infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/code/example.tfvars create mode 100644 infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/code/main.tf create mode 100644 infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/code/outputs.tf create mode 100644 infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/code/providers.tf create mode 100644 infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/code/variables.tf create mode 100644 infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/code/versions.tf create mode 100644 infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/README.md create mode 100644 infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/code/example.tfvars create mode 100644 infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/code/main.tf create mode 100644 infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/code/outputs.tf create mode 100644 infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/code/providers.tf create mode 100644 infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/code/variables.tf create mode 100644 infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/code/versions.tf create mode 100644 infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/README.md create mode 100644 infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/code/example.tfvars create mode 100644 infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/code/main.tf create mode 100644 infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/code/outputs.tf create mode 100644 infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/code/providers.tf create mode 100644 infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/code/variables.tf create mode 100644 infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/code/versions.tf create mode 100644 infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/README.md create mode 100644 infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/code/example.tfvars create mode 100644 infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/code/main.tf create mode 100644 infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/code/outputs.tf create mode 100644 infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/code/providers.tf create mode 100644 infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/code/variables.tf create mode 100644 infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/code/versions.tf create mode 100644 infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/README.md create mode 100644 infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/code/example.tfvars create mode 100644 infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/code/main.tf create mode 100644 infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/code/outputs.tf create mode 100644 infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/code/providers.tf create mode 100644 infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/code/variables.tf create mode 100644 infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/code/versions.tf diff --git a/infrastructure/infrastructure-setup-terraform/00-basic-azurerm/.skip-tf-validation b/infrastructure/infrastructure-setup-terraform/00-basic-azurerm/.skip-tf-validation new file mode 100644 index 000000000..0e1e6095d --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/00-basic-azurerm/.skip-tf-validation @@ -0,0 +1,3 @@ +# This directory is owned by @azure-ai-foundry/ai-platform-docs (see CODEOWNERS). +# Its .tf files use intentional non-standard HCL formatting that must not be changed. +# Skipped by infrastructure/infrastructure-setup-terraform/scripts/validate.sh. diff --git a/infrastructure/infrastructure-setup-terraform/00-basic-azurerm/README.md b/infrastructure/infrastructure-setup-terraform/00-basic-azurerm/README.md index 9a6f3d65a..c79b01842 100644 --- a/infrastructure/infrastructure-setup-terraform/00-basic-azurerm/README.md +++ b/infrastructure/infrastructure-setup-terraform/00-basic-azurerm/README.md @@ -1,5 +1,5 @@ --- -description: This set of templates demonstrates how to set up Azure AI Foundry in the basic configuration with public network setup and Microsoft-managed storage resources using AzureRM provider. +description: This set of templates demonstrates how to set up Microsoft Foundry in the basic configuration with public network setup and Microsoft-managed storage resources using AzureRM provider. page_type: sample products: - azure @@ -9,11 +9,11 @@ languages: - hcl --- -# Azure AI Foundry: Basic setup with public networking (AzureRM) +# Microsoft Foundry: Basic setup with public networking (AzureRM) ## Key Information -This infrastructure-as-code (IaC) solution deploys Azure AI Foundry with public networking and uses Microsoft-managed storage for file upload experience. It supports getting started scenarios, for typically non-enterprise scenarios. This variant shows AzureRM Terraform provider. +This infrastructure-as-code (IaC) solution deploys Microsoft Foundry with public networking and uses Microsoft-managed storage for file upload experience. It supports getting started scenarios, for typically non-enterprise scenarios. This variant shows AzureRM Terraform provider. ## Prerequisites @@ -99,5 +99,5 @@ code/ ## References -- [Learn more about Azure AI Foundry architecture](https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/architecture) -- [Azure AI Foundry reference docs for Terraform](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/create-resource-terraform) \ No newline at end of file +- [Learn more about Microsoft Foundry architecture](https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/architecture) +- [Microsoft Foundry reference docs for Terraform](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/create-resource-terraform) \ No newline at end of file diff --git a/infrastructure/infrastructure-setup-terraform/00-basic-azurerm/code/example.tfvars b/infrastructure/infrastructure-setup-terraform/00-basic-azurerm/code/example.tfvars index cdfa09c19..ca03533e2 100644 --- a/infrastructure/infrastructure-setup-terraform/00-basic-azurerm/code/example.tfvars +++ b/infrastructure/infrastructure-setup-terraform/00-basic-azurerm/code/example.tfvars @@ -1 +1 @@ -location = "eastus" +location = "eastus2" diff --git a/infrastructure/infrastructure-setup-terraform/00-basic/README.md b/infrastructure/infrastructure-setup-terraform/00-basic/README.md deleted file mode 100644 index b5a846650..000000000 --- a/infrastructure/infrastructure-setup-terraform/00-basic/README.md +++ /dev/null @@ -1,102 +0,0 @@ ---- -description: This set of templates demonstrates how to set up Azure AI Foundry in the basic configuration with public network setup and Microsoft-managed storage resources. -page_type: sample -products: -- azure -- azure-resource-manager -urlFragment: foundry-basic -languages: -- hcl ---- - -# Azure AI Foundry: Basic setup with public networking - -## Key Information - -This infrastructure-as-code (IaC) solution deploys Azure AI Foundry with public networking and uses Microsoft-managed storage for file upload experience. It supports getting started scenarios, for typically non-enterprise scenarios. - -## Prerequisites - -1. **Active Azure subscription(s) with appropriate permissions** - It's recommended to deploy these templates through a deployment pipeline associated to a service principal or managed identity with sufficient permissions over the the workload subscription (such as Owner or Role Based Access Control Administrator and Contributor). If deployed manually, the permissions below should be sufficient. - - - **Workload Subscription** - - **Role Based Access Control Administrator**: Needed over the resource group to create the relevant role assignments - - **Network Contributor**: Needed over the resource group to create virtual network and Private Endpoint resources - - **Azure AI Account Owner**: Needed to create a cognitive services account and project - - **Owner or Role Based Access Administrator**: Needed to assign RBAC to the required resources (Cosmos DB, Azure AI Search, Storage) - - **Azure AI User**: Needed to create and edit agents - -2. **Register Resource Providers** - - ```bash - az provider register --namespace 'Microsoft.CognitiveServices' - ``` - -3. Sufficient quota for all resources in your target Azure region - -4. Azure CLI installed and configured on your local workstation or deployment pipeline server - -5. Terraform CLI version v1.11.4 or later on your local workstation or depoyment pipeline server. This template requires the usage of both the AzureRm and AzApi Terraform providers. - -### Variables - -The variables listed below [must be provided](https://developer.hashicorp.com/terraform/language/values/variables#variable-definition-precedence) when performing deploying the templates. The file example.tfvars provides a sample Terraform variables file that can be used. - -- **location** - The Azure region the resources will be deployed to. This must be the same region where the pre-existing virtual network has been deployed to. - -The variables listed below are optional and if not specified will use the defaults as included in the variables.tf file. - -## Deploy the Terraform template - -1. Fill in the required information for the variables listed in the example.tfvars file and rename the file to terraform.tfvars. - -2. If performing the deployment interactively, log in to Az CLI with a user that has sufficient permissions to deploy the resources. - -```bash -az login -``` - -3. Ensure the proper environmental variables are set for [AzApi](https://registry.terraform.io/providers/Azure/azapi/latest/docs) and [AzureRm](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs) providers. At a minimum, you must set the ARM_SUBSCRIPTION_ID environment variable to the subscription the resoruces will be deployed to. You can do this with the commands below: - -Linux/MacOS -```bash -export ARM_SUBSCRIPTION_ID="YOUR_SUBSCRIPTION_ID" -terraform apply -``` - -Windows -```cmd -set ARM_SUBSCRIPTION_ID="YOUR_SUBSCRIPTION_ID" -terraform apply -``` - -4. Initialize Terraform - -```bash -terraform init -``` - -5. Deploy the resources -```bash -terraform apply -``` - -## Module Structure - -```text -code/ -├── data.tf # Creates data objects for active subscription being deployed to and deployment security context -├── locals.tf # Creates local variables for project GUID -├── main.tf # Main deployment file -├── outputs.tf # Placeholder file for future outputs -├── providers.tf # Terraform provider configuration -├── example.tfvars # Sample tfvars file -├── variables.tf # Terraform variables -├── versions.tf # Configures minimum Terraform version and versions for providers -``` - - -## References - -- [Azure AI Foundry rollout across my organization](https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/planning) \ No newline at end of file diff --git a/infrastructure/infrastructure-setup-terraform/00-basic/code/example.tfvars b/infrastructure/infrastructure-setup-terraform/00-basic/code/example.tfvars index cdfa09c19..ca03533e2 100644 --- a/infrastructure/infrastructure-setup-terraform/00-basic/code/example.tfvars +++ b/infrastructure/infrastructure-setup-terraform/00-basic/code/example.tfvars @@ -1 +1 @@ -location = "eastus" +location = "eastus2" diff --git a/infrastructure/infrastructure-setup-terraform/00-basic/code/main.tf b/infrastructure/infrastructure-setup-terraform/00-basic/code/main.tf index 6bd6175ad..506919916 100644 --- a/infrastructure/infrastructure-setup-terraform/00-basic/code/main.tf +++ b/infrastructure/infrastructure-setup-terraform/00-basic/code/main.tf @@ -13,7 +13,7 @@ resource "random_string" "unique" { ## resource "azapi_resource" "rg" { type = "Microsoft.Resources/resourceGroups@2021-04-01" - name = "rg-aifoundry-${random_string.unique.result}" + name = "tf-319-basic" location = var.location } @@ -63,7 +63,7 @@ resource "azapi_resource" "aifoundry_deployment_gpt_4o" { body = { sku = { - name = "GlobalStandard" + name = "Standard" capacity = 1 } properties = { diff --git a/infrastructure/infrastructure-setup-terraform/01-connections/.skip-tf-validation b/infrastructure/infrastructure-setup-terraform/01-connections/.skip-tf-validation new file mode 100644 index 000000000..b3d12811c --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/01-connections/.skip-tf-validation @@ -0,0 +1,4 @@ +# This directory contains standalone Terraform snippet files, not a complete module. +# Each .tf file declares its own terraform{} and provider blocks and is meant to be +# copied into an existing Terraform configuration, not run directly. +# Skipped by infrastructure/infrastructure-setup-terraform/scripts/validate.sh. diff --git a/infrastructure/infrastructure-setup-terraform/01-connections/README.md b/infrastructure/infrastructure-setup-terraform/01-connections/README.md new file mode 100644 index 000000000..e57074d0a --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/01-connections/README.md @@ -0,0 +1,37 @@ +# Connections Terraform Examples + +Connections enable your AI applications to access tools and objects managed elsewhere in or outside of Azure. + +This folder provides Terraform examples for the most common connection categories: + +- **AI Search**: Connect to Azure AI Search (Cognitive Search) services +- **Key Vault**: Securely access secrets and keys +- **Application Insights**: Enable monitoring and telemetry +- **Azure OpenAI**: Connect to Azure OpenAI endpoints +- **Storage Account**: Access Azure Storage for data +- **Cosmos DB**: Connect to Cosmos DB databases +- **Bing Grounding**: Enable Bing Search for grounding + +## Usage + +Each file demonstrates how to create a specific connection type using Terraform with the AzAPI provider. These are example snippets that you can integrate into your own Terraform configurations. + +## Prerequisites + +- An existing Microsoft Foundry account +- The target resource you want to connect (e.g., AI Search service, Key Vault, etc.) +- Appropriate permissions to create connections + +## Important Notes + +- Uses AzAPI provider for connection resources (not yet in Az +ureRM) +- Some connections require API keys or managed identity configuration +- Role assignments may be needed for managed identity auth + +## Documentation + +- [Microsoft Foundry connections](https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/connections) +- [AzAPI Provider](https://registry.terraform.io/providers/azure/azapi/latest/docs) + +`Tags: Microsoft.CognitiveServices/accounts/connections, Integration` diff --git a/infrastructure/infrastructure-setup-terraform/01-connections/connection-ai-search.tf b/infrastructure/infrastructure-setup-terraform/01-connections/connection-ai-search.tf new file mode 100644 index 000000000..ace040e53 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/01-connections/connection-ai-search.tf @@ -0,0 +1,108 @@ +# Example: Azure AI Search Connection +# This example shows how to create a connection from AI Foundry to Azure AI Search + +terraform { + required_providers { + azapi = { + source = "azure/azapi" + version = "~> 2.5" + } + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.37" + } + } +} + +provider "azapi" {} +provider "azurerm" { + features {} +} + +variable "ai_foundry_name" { + description = "Name of your existing AI Foundry account" + type = string +} + +variable "resource_group_name" { + description = "Resource group containing AI Foundry account" + type = string +} + +variable "location" { + description = "Azure region" + type = string + default = "westus" +} + +variable "create_new_search" { + description = "Create new AI Search service or use existing" + type = bool + default = true +} + +variable "search_service_name" { + description = "Name of the AI Search service" + type = string +} + +# Reference existing AI Foundry account +data "azurerm_resource_group" "rg" { + name = var.resource_group_name +} + +data "azapi_resource" "ai_foundry" { + type = "Microsoft.CognitiveServices/accounts@2025-04-01-preview" + name = var.ai_foundry_name + parent_id = data.azurerm_resource_group.rg.id +} + +# Conditionally create new AI Search service +resource "azurerm_search_service" "search" { + count = var.create_new_search ? 1 : 0 + name = var.search_service_name + resource_group_name = var.resource_group_name + location = var.location + sku = "basic" +} + +# Reference existing AI Search service +data "azurerm_search_service" "existing" { + count = var.create_new_search ? 0 : 1 + name = var.search_service_name + resource_group_name = var.resource_group_name +} + +locals { + search_endpoint = var.create_new_search ? "https://${azurerm_search_service.search[0].name}.search.windows.net" : "https://${data.azurerm_search_service.existing[0].name}.search.windows.net" + search_id = var.create_new_search ? azurerm_search_service.search[0].id : data.azurerm_search_service.existing[0].id + search_primary_key = var.create_new_search ? azurerm_search_service.search[0].primary_key : data.azurerm_search_service.existing[0].primary_key +} + +# Create AI Search connection +resource "azapi_resource" "ai_search_connection" { + type = "Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview" + name = "${var.ai_foundry_name}-aisearch" + parent_id = data.azapi_resource.ai_foundry.id + + body = { + properties = { + category = "CognitiveSearch" + target = local.search_endpoint + authType = "ApiKey" + isSharedToAll = true + credentials = { + key = local.search_primary_key + } + metadata = { + ApiType = "Azure" + ResourceId = local.search_id + location = var.location + } + } + } +} + +output "connection_id" { + value = azapi_resource.ai_search_connection.id +} diff --git a/infrastructure/infrastructure-setup-terraform/01-connections/connection-key-vault.tf b/infrastructure/infrastructure-setup-terraform/01-connections/connection-key-vault.tf new file mode 100644 index 000000000..ac90695a1 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/01-connections/connection-key-vault.tf @@ -0,0 +1,88 @@ +# Example: Key Vault Connection +# This example shows how to create a connection from AI Foundry to Azure Key Vault +# using Managed Identity authentication + +terraform { + required_providers { + azapi = { + source = "azure/azapi" + version = "~> 2.5" + } + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.37" + } + } +} + +provider "azapi" {} +provider "azurerm" { + features {} +} + +variable "ai_foundry_name" { + description = "Name of your existing AI Foundry account" + type = string +} + +variable "resource_group_name" { + description = "Resource group containing AI Foundry and Key Vault" + type = string +} + +variable "key_vault_name" { + description = "Name of the Key Vault" + type = string +} + +# Reference existing resources +data "azurerm_resource_group" "rg" { + name = var.resource_group_name +} + +data "azapi_resource" "ai_foundry" { + type = "Microsoft.CognitiveServices/accounts@2025-04-01-preview" + name = var.ai_foundry_name + parent_id = data.azurerm_resource_group.rg.id +} + +data "azurerm_key_vault" "kv" { + name = var.key_vault_name + resource_group_name = var.resource_group_name +} + +# Create Key Vault connection +resource "azapi_resource" "key_vault_connection" { + type = "Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview" + name = "${var.ai_foundry_name}-keyvault" + parent_id = data.azapi_resource.ai_foundry.id + + body = { + properties = { + category = "AzureKeyVault" + target = data.azurerm_key_vault.kv.id + authType = "AccountManagedIdentity" + isSharedToAll = true + metadata = { + ApiType = "Azure" + ResourceId = data.azurerm_key_vault.kv.id + location = data.azurerm_key_vault.kv.location + } + } + } +} + +# Grant AI Foundry managed identity access to Key Vault +# The AI Foundry account must have a system-assigned managed identity +resource "azurerm_role_assignment" "kv_secrets_officer" { + scope = data.azurerm_key_vault.kv.id + role_definition_name = "Key Vault Secrets Officer" + principal_id = jsondecode(data.azapi_resource.ai_foundry.output).identity.principalId +} + +output "connection_id" { + value = azapi_resource.key_vault_connection.id +} + +# NOTE: All subsequent connections should depend on both the key vault connection +# and the role assignment for proper sequencing diff --git a/infrastructure/infrastructure-setup-terraform/01-connections/connection-storage-account.tf b/infrastructure/infrastructure-setup-terraform/01-connections/connection-storage-account.tf new file mode 100644 index 000000000..b00f84c3e --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/01-connections/connection-storage-account.tf @@ -0,0 +1,76 @@ +# Example: Storage Account Connection +# This example shows how to create a connection from AI Foundry to Azure Storage + +terraform { + required_providers { + azapi = { + source = "azure/azapi" + version = "~> 2.5" + } + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.37" + } + } +} + +provider "azapi" {} +provider "azurerm" { + features {} +} + +variable "ai_foundry_name" { + description = "Name of your existing AI Foundry account" + type = string +} + +variable "resource_group_name" { + description = "Resource group name" + type = string +} + +variable "storage_account_name" { + description = "Name of the storage account" + type = string +} + +# Reference existing resources +data "azurerm_resource_group" "rg" { + name = var.resource_group_name +} + +data "azapi_resource" "ai_foundry" { + type = "Microsoft.CognitiveServices/accounts@2025-04-01-preview" + name = var.ai_foundry_name + parent_id = data.azurerm_resource_group.rg.id +} + +data "azurerm_storage_account" "storage" { + name = var.storage_account_name + resource_group_name = var.resource_group_name +} + +# Create Storage Account connection +resource "azapi_resource" "storage_connection" { + type = "Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview" + name = "${var.ai_foundry_name}-storage" + parent_id = data.azapi_resource.ai_foundry.id + + body = { + properties = { + category = "AzureBlob" + target = "https://${data.azurerm_storage_account.storage.name}.blob.core.windows.net/" + authType = "AccountManagedIdentity" + isSharedToAll = true + metadata = { + ApiType = "Azure" + ResourceId = data.azurerm_storage_account.storage.id + location = data.azurerm_storage_account.storage.location + } + } + } +} + +output "connection_id" { + value = azapi_resource.storage_connection.id +} diff --git a/infrastructure/infrastructure-setup-terraform/02-storage-speech-language/README.md b/infrastructure/infrastructure-setup-terraform/02-storage-speech-language/README.md new file mode 100644 index 000000000..cdcadd167 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/02-storage-speech-language/README.md @@ -0,0 +1,36 @@ +# Azure Cognitive Services Integration Examples + +This folder contains examples for integrating Microsoft Foundry with other Azure Cognitive Services: + +- **Azure Storage Account**: For storing files and data +- **Azure Speech Services**: For speech-to-text and text-to-speech capabilities +- **Azure Language Services**: For natural language processing + +## Overview + +These are reference examples showing how to create connections between Microsoft Foundry and other Azure Cognitive Services. Each example demonstrates configuring the required resources and establishing connections. + +## Prerequisites + +- An existing Microsoft Foundry account +- Azure Storage, Speech, or Language service resources +- Appropriate RBAC permissions + +## Usage + +These examples can be adapted and integrated into your Terraform configurations. Modify the variables to match your resource names and requirements. + +## Status + +This folder now includes a full Terraform sample with: +- Microsoft Foundry account configured for user-owned storage +- Optional Speech and Language resource deployment switches +- Storage RBAC wiring for the account identity + +## Documentation + +- [Microsoft Foundry overview](https://learn.microsoft.com/en-us/azure/ai-foundry/) +- [Azure Storage Account - Terraform](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account) +- [AzAPI Provider](https://registry.terraform.io/providers/azure/azapi/latest/docs) + +`Tags: Integration, Storage, Speech, Language Services` diff --git a/infrastructure/infrastructure-setup-terraform/02-storage-speech-language/code/example.tfvars b/infrastructure/infrastructure-setup-terraform/02-storage-speech-language/code/example.tfvars new file mode 100644 index 000000000..aa897f13b --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/02-storage-speech-language/code/example.tfvars @@ -0,0 +1,18 @@ +# Example configuration for storage-speech-language + +# Azure region +location = "eastus2" + +# AI Foundry account name +ai_foundry_name = "foundry-storage" + +# Storage account configuration +create_storage_account = true +# storage_account_name = "mystorageaccount" # Uncomment to specify custom name +# storage_account_resource_group = "my-rg" # Required if using existing storage + +# Optional: Create Azure Speech Service +create_speech_service = false + +# Optional: Create Azure Language Service +create_language_service = false diff --git a/infrastructure/infrastructure-setup-terraform/02-storage-speech-language/code/main.tf b/infrastructure/infrastructure-setup-terraform/02-storage-speech-language/code/main.tf new file mode 100644 index 000000000..6148db9f2 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/02-storage-speech-language/code/main.tf @@ -0,0 +1,105 @@ +########## Create infrastructure resources +########## + +## Get subscription data +data "azurerm_client_config" "current" {} + +## Create a random string for unique naming +resource "random_string" "unique" { + length = 4 + min_numeric = 4 + numeric = true + special = false + lower = true + upper = false +} + +locals { + storage_account_name = var.create_storage_account ? ( + var.storage_account_name != "" ? var.storage_account_name : "st${random_string.unique.result}foundry" + ) : var.storage_account_name +} + +## Create a resource group +resource "azurerm_resource_group" "rg" { + name = "rg-aifoundry${random_string.unique.result}" + location = var.location +} + +## Reference existing storage account (if not creating new) +data "azurerm_storage_account" "existing" { + count = var.create_storage_account ? 0 : 1 + name = var.storage_account_name + resource_group_name = var.storage_account_resource_group +} + +## Create storage account (if requested) +resource "azurerm_storage_account" "storage" { + count = var.create_storage_account ? 1 : 0 + name = local.storage_account_name + resource_group_name = azurerm_resource_group.rg.name + location = var.location + account_tier = "Standard" + account_replication_type = "RAGRS" + account_kind = "StorageV2" + + allow_nested_items_to_be_public = false + shared_access_key_enabled = true # Required for AI Foundry BYOS + min_tls_version = "TLS1_2" + https_traffic_only_enabled = true + + blob_properties { + delete_retention_policy { + days = 7 + } + } +} + +locals { + storage_id = var.create_storage_account ? azurerm_storage_account.storage[0].id : data.azurerm_storage_account.existing[0].id +} + +## Create AI Foundry account with user-owned storage +resource "azapi_resource" "ai_foundry" { + type = "Microsoft.CognitiveServices/accounts@2025-06-01" + name = var.ai_foundry_name + location = var.location + parent_id = azurerm_resource_group.rg.id + + identity { + type = "SystemAssigned" + } + + body = { + kind = "AIServices" + sku = { + name = "S0" + } + properties = { + allowProjectManagement = true + customSubDomainName = var.ai_foundry_name + disableLocalAuth = false + publicNetworkAccess = "Enabled" + userOwnedStorage = [ + { + resourceId = local.storage_id + } + ] + } + } +} + +## Wait for AI Foundry creation to ensure identity is available +resource "time_sleep" "wait_for_ai_foundry" { + depends_on = [azapi_resource.ai_foundry] + create_duration = "30s" +} + +## Grant AI Foundry access to storage account +resource "azurerm_role_assignment" "storage_blob_data_contributor" { + scope = local.storage_id + role_definition_name = "Storage Blob Data Contributor" + principal_id = azapi_resource.ai_foundry.identity[0].principal_id + + depends_on = [time_sleep.wait_for_ai_foundry] +} \ No newline at end of file diff --git a/infrastructure/infrastructure-setup-terraform/02-storage-speech-language/code/outputs.tf b/infrastructure/infrastructure-setup-terraform/02-storage-speech-language/code/outputs.tf new file mode 100644 index 000000000..8fd152328 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/02-storage-speech-language/code/outputs.tf @@ -0,0 +1,19 @@ +output "resource_group_name" { + description = "The name of the resource group" + value = azurerm_resource_group.rg.name +} + +output "ai_foundry_id" { + description = "The ID of the AI Foundry account" + value = azapi_resource.ai_foundry.id +} + +output "ai_foundry_name" { + description = "The name of the AI Foundry account" + value = var.ai_foundry_name +} + +output "storage_account_id" { + description = "The ID of the storage account" + value = local.storage_id +} diff --git a/infrastructure/infrastructure-setup-terraform/02-storage-speech-language/code/providers.tf b/infrastructure/infrastructure-setup-terraform/02-storage-speech-language/code/providers.tf new file mode 100644 index 000000000..9a71e9677 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/02-storage-speech-language/code/providers.tf @@ -0,0 +1,8 @@ +# Setup providers +provider "azapi" { +} + +provider "azurerm" { + features {} + storage_use_azuread = true +} diff --git a/infrastructure/infrastructure-setup-terraform/02-storage-speech-language/code/variables.tf b/infrastructure/infrastructure-setup-terraform/02-storage-speech-language/code/variables.tf new file mode 100644 index 000000000..da19b5f10 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/02-storage-speech-language/code/variables.tf @@ -0,0 +1,29 @@ +variable "location" { + description = "The Azure region where resources will be deployed" + type = string + default = "eastus2" +} + +variable "ai_foundry_name" { + description = "The name of the AI Foundry account" + type = string + default = "foundry-storage" +} + +variable "create_storage_account" { + description = "Whether to create a new storage account (true) or use an existing one (false)" + type = bool + default = true +} + +variable "storage_account_name" { + description = "Name of the storage account (for new or existing)" + type = string + default = "" +} + +variable "storage_account_resource_group" { + description = "Resource group of existing storage account (only used if create_storage_account=false)" + type = string + default = "" +} diff --git a/infrastructure/infrastructure-setup-terraform/02-storage-speech-language/code/versions.tf b/infrastructure/infrastructure-setup-terraform/02-storage-speech-language/code/versions.tf new file mode 100644 index 000000000..6eef7e581 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/02-storage-speech-language/code/versions.tf @@ -0,0 +1,22 @@ +# Configure the AzApi and AzureRM providers +terraform { + 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.12" + } + } + required_version = ">= 1.10.0, < 2.0.0" +} diff --git a/infrastructure/infrastructure-setup-terraform/03-custom-dns/README.md b/infrastructure/infrastructure-setup-terraform/03-custom-dns/README.md new file mode 100644 index 000000000..8d24b13bd --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/03-custom-dns/README.md @@ -0,0 +1,55 @@ +# Deploy Microsoft Foundry with Custom DNS + +This Terraform template deploys an Microsoft Foundry resource with a custom DNS subdomain name. + +## Description + +- Creates an Microsoft Foundry account with a custom subdomain for API endpoints +- Creates a project +- Deploys a model + +## Prerequisites + +- Azure CLI or Terraform installed +- The custom subdomain name must be globally unique +- Appropriate Azure permissions + +## Custom Subdomain + +The custom subdomain defines the developer API endpoint subdomain. For example: +- Custom subdomain: `my-foundry-instance` +- API endpoint: `https://my-foundry-instance.cognitiveservices.azure.com/` + +## Deployment + +1. Navigate to the code directory: +```bash +cd code +``` + +2. Initialize Terraform: +```bash +terraform init +``` + +3. Customize the `custom_subdomain_name` variable + +4. Deploy: +```bash +terraform plan +terraform apply +``` + +## Resources Created + +- Microsoft Foundry account (with custom subdomain) +- Microsoft Foundry project +- Model deployment + +## Documentation + +- [Custom subdomain names for Azure AI services](https://learn.microsoft.com/en-us/azure/ai-services/cognitive-services-custom-subdomains) +- [Microsoft Foundry overview](https://learn.microsoft.com/en-us/azure/ai-foundry/) +- [AzAPI Provider](https://registry.terraform.io/providers/azure/azapi/latest/docs) + +`Tags: Microsoft.CognitiveServices/accounts, Custom DNS` diff --git a/infrastructure/infrastructure-setup-terraform/03-custom-dns/code/example.tfvars b/infrastructure/infrastructure-setup-terraform/03-custom-dns/code/example.tfvars new file mode 100644 index 000000000..810dda390 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/03-custom-dns/code/example.tfvars @@ -0,0 +1,8 @@ +# Example terraform.tfvars file +# Copy this to terraform.tfvars and customize for your deployment + +location = "eastus2" +custom_subdomain_name = "my-unique-foundry-dns" # Must be globally unique +ai_project_name = "custom-dns-proj" +model_name = "gpt-4.1-mini" +model_version = "2025-04-14" diff --git a/infrastructure/infrastructure-setup-terraform/03-custom-dns/code/main.tf b/infrastructure/infrastructure-setup-terraform/03-custom-dns/code/main.tf new file mode 100644 index 000000000..49a4e5c42 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/03-custom-dns/code/main.tf @@ -0,0 +1,83 @@ +########## Create infrastructure resources +########## + +## Get subscription data +data "azurerm_client_config" "current" {} + +## Create a random string for unique naming +resource "random_string" "unique" { + length = 4 + min_numeric = 4 + numeric = true + special = false + lower = true + upper = false +} + +## Create a resource group +resource "azurerm_resource_group" "rg" { + name = "rg-aifoundry${random_string.unique.result}" + location = var.location +} + +## Create AI Foundry account with custom subdomain +resource "azapi_resource" "ai_foundry" { + type = "Microsoft.CognitiveServices/accounts@2025-06-01" + name = var.custom_subdomain_name + location = var.location + parent_id = azurerm_resource_group.rg.id + + identity { + type = "SystemAssigned" + } + + body = { + kind = "AIServices" + sku = { + name = "S0" + } + properties = { + allowProjectManagement = true + customSubDomainName = var.custom_subdomain_name + disableLocalAuth = false + publicNetworkAccess = "Enabled" + } + } +} + +## Create AI Foundry project +resource "azapi_resource" "ai_project" { + type = "Microsoft.CognitiveServices/accounts/projects@2025-06-01" + name = var.ai_project_name + location = var.location + parent_id = azapi_resource.ai_foundry.id + + identity { + type = "SystemAssigned" + } + + body = { + properties = {} + } +} + +## Deploy model +resource "azapi_resource" "model_deployment" { + type = "Microsoft.CognitiveServices/accounts/deployments@2025-06-01" + name = var.model_name + parent_id = azapi_resource.ai_foundry.id + + body = { + sku = { + capacity = 1 + name = "GlobalStandard" + } + properties = { + model = { + name = var.model_name + format = "OpenAI" + version = var.model_version + } + } + } +} diff --git a/infrastructure/infrastructure-setup-terraform/03-custom-dns/code/outputs.tf b/infrastructure/infrastructure-setup-terraform/03-custom-dns/code/outputs.tf new file mode 100644 index 000000000..feb51c310 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/03-custom-dns/code/outputs.tf @@ -0,0 +1,19 @@ +output "resource_group_name" { + description = "The name of the resource group" + value = azurerm_resource_group.rg.name +} + +output "ai_foundry_id" { + description = "The ID of the AI Foundry account" + value = azapi_resource.ai_foundry.id +} + +output "custom_endpoint" { + description = "The custom API endpoint" + value = "https://${var.custom_subdomain_name}.cognitiveservices.azure.com/" +} + +output "ai_project_id" { + description = "The ID of the AI Foundry project" + value = azapi_resource.ai_project.id +} diff --git a/infrastructure/infrastructure-setup-terraform/03-custom-dns/code/providers.tf b/infrastructure/infrastructure-setup-terraform/03-custom-dns/code/providers.tf new file mode 100644 index 000000000..9a71e9677 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/03-custom-dns/code/providers.tf @@ -0,0 +1,8 @@ +# Setup providers +provider "azapi" { +} + +provider "azurerm" { + features {} + storage_use_azuread = true +} diff --git a/infrastructure/infrastructure-setup-terraform/03-custom-dns/code/variables.tf b/infrastructure/infrastructure-setup-terraform/03-custom-dns/code/variables.tf new file mode 100644 index 000000000..e26291d06 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/03-custom-dns/code/variables.tf @@ -0,0 +1,29 @@ +variable "location" { + description = "The Azure region where resources will be deployed" + type = string + default = "eastus2" +} + +variable "custom_subdomain_name" { + description = "Custom subdomain name for the AI Foundry API endpoint (must be globally unique)" + type = string + default = "my-unique-foundry-dns" +} + +variable "ai_project_name" { + description = "The name of the AI Foundry project" + type = string + default = "custom-dns-proj" +} + +variable "model_name" { + description = "The model to deploy" + type = string + default = "gpt-4.1-mini" +} + +variable "model_version" { + description = "The version of the model" + type = string + default = "2025-04-14" +} diff --git a/infrastructure/infrastructure-setup-terraform/03-custom-dns/code/versions.tf b/infrastructure/infrastructure-setup-terraform/03-custom-dns/code/versions.tf new file mode 100644 index 000000000..e597a8a69 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/03-custom-dns/code/versions.tf @@ -0,0 +1,18 @@ +# Configure the AzApi and AzureRM providers +terraform { + required_providers { + azapi = { + source = "azure/azapi" + version = "~> 2.5" + } + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.37" + } + random = { + source = "hashicorp/random" + version = "~> 3.7" + } + } + required_version = ">= 1.10.0, < 2.0.0" +} diff --git a/infrastructure/infrastructure-setup-terraform/04-disable-local-auth/README.md b/infrastructure/infrastructure-setup-terraform/04-disable-local-auth/README.md new file mode 100644 index 000000000..27b5ad426 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/04-disable-local-auth/README.md @@ -0,0 +1,54 @@ +# Deploy Microsoft Foundry with Local Authentication Disabled + +This Terraform template deploys an Microsoft Foundry resource and project with local (key-based) authentication disabled. Azure Entra ID authentication must be used instead. + +## Description + +- Creates an Microsoft Foundry account with `disableLocalAuth: true` +- Creates a project +- Deploys a GPT-4.1-mini model + +## Prerequisites + +- Azure CLI or Terraform installed +- Appropriate Azure permissions (Contributor role on subscription/resource group) + +## Deployment + +1. Navigate to the code directory: +```bash +cd code +``` + +2. Initialize Terraform: +```bash +terraform init +``` + +3. Review and customize variables in `terraform.tfvars` or provide them via command line + +4. Deploy: +```bash +terraform plan +terraform apply +``` + +## Important Notes + +- With local auth disabled, you must use Azure Entra ID authentication +- Some Microsoft Foundry features work best with Entra ID authentication +- API keys will not work with this configuration + +## Resources Created + +- Microsoft Foundry account (Cognitive Services) +- Microsoft Foundry project +- Model deployment (GPT-4.1-mini) + +## Documentation + +- [Disable local authentication in Azure AI services](https://learn.microsoft.com/en-us/azure/ai-services/disable-local-auth) +- [Microsoft Foundry RBAC](https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/rbac-azure-ai-foundry) +- [AzAPI Provider](https://registry.terraform.io/providers/azure/azapi/latest/docs) + +`Tags: Microsoft.CognitiveServices/accounts, Disable Local Auth, Entra ID` diff --git a/infrastructure/infrastructure-setup-terraform/04-disable-local-auth/code/example.tfvars b/infrastructure/infrastructure-setup-terraform/04-disable-local-auth/code/example.tfvars new file mode 100644 index 000000000..dddc42eb1 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/04-disable-local-auth/code/example.tfvars @@ -0,0 +1,9 @@ +# Example terraform.tfvars file +# Copy this to terraform.tfvars and customize for your deployment + +location = "eastus2" +ai_foundry_name = "foundry-disable-localauth" +ai_project_name = "foundry-disable-localauth-proj" +model_name = "gpt-4.1-mini" +model_version = "2025-04-14" +model_capacity = 1 diff --git a/infrastructure/infrastructure-setup-terraform/04-disable-local-auth/code/main.tf b/infrastructure/infrastructure-setup-terraform/04-disable-local-auth/code/main.tf new file mode 100644 index 000000000..61d37fa4e --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/04-disable-local-auth/code/main.tf @@ -0,0 +1,83 @@ +########## Create infrastructure resources +########## + +## Get subscription data +data "azurerm_client_config" "current" {} + +## Create a random string for unique naming +resource "random_string" "unique" { + length = 4 + min_numeric = 4 + numeric = true + special = false + lower = true + upper = false +} + +## Create a resource group +resource "azurerm_resource_group" "rg" { + name = "rg-aifoundry${random_string.unique.result}" + location = var.location +} + +## Create AI Foundry account with local auth disabled +resource "azapi_resource" "ai_foundry" { + type = "Microsoft.CognitiveServices/accounts@2025-06-01" + name = var.ai_foundry_name + location = var.location + parent_id = azurerm_resource_group.rg.id + + identity { + type = "SystemAssigned" + } + + body = { + kind = "AIServices" + sku = { + name = "S0" + } + properties = { + allowProjectManagement = true + customSubDomainName = var.ai_foundry_name + disableLocalAuth = true + publicNetworkAccess = "Enabled" + } + } +} + +## Create AI Foundry project +resource "azapi_resource" "ai_project" { + type = "Microsoft.CognitiveServices/accounts/projects@2025-06-01" + name = coalesce(var.ai_project_name, "${var.ai_foundry_name}-proj") + location = var.location + parent_id = azapi_resource.ai_foundry.id + + identity { + type = "SystemAssigned" + } + + body = { + properties = {} + } +} + +## Deploy model +resource "azapi_resource" "model_deployment" { + type = "Microsoft.CognitiveServices/accounts/deployments@2025-06-01" + name = var.model_deployment_name + parent_id = azapi_resource.ai_foundry.id + + body = { + sku = { + capacity = var.model_capacity + name = "GlobalStandard" + } + properties = { + model = { + name = var.model_name + format = "OpenAI" + version = var.model_version + } + } + } +} diff --git a/infrastructure/infrastructure-setup-terraform/04-disable-local-auth/code/outputs.tf b/infrastructure/infrastructure-setup-terraform/04-disable-local-auth/code/outputs.tf new file mode 100644 index 000000000..7b3c6adb3 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/04-disable-local-auth/code/outputs.tf @@ -0,0 +1,24 @@ +output "resource_group_name" { + description = "The name of the resource group" + value = azurerm_resource_group.rg.name +} + +output "ai_foundry_id" { + description = "The ID of the AI Foundry account" + value = azapi_resource.ai_foundry.id +} + +output "ai_foundry_name" { + description = "The name of the AI Foundry account" + value = azapi_resource.ai_foundry.name +} + +output "ai_project_id" { + description = "The ID of the AI Foundry project" + value = azapi_resource.ai_project.id +} + +output "model_deployment_name" { + description = "The name of the model deployment" + value = azapi_resource.model_deployment.name +} diff --git a/infrastructure/infrastructure-setup-terraform/04-disable-local-auth/code/providers.tf b/infrastructure/infrastructure-setup-terraform/04-disable-local-auth/code/providers.tf new file mode 100644 index 000000000..9a71e9677 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/04-disable-local-auth/code/providers.tf @@ -0,0 +1,8 @@ +# Setup providers +provider "azapi" { +} + +provider "azurerm" { + features {} + storage_use_azuread = true +} diff --git a/infrastructure/infrastructure-setup-terraform/04-disable-local-auth/code/variables.tf b/infrastructure/infrastructure-setup-terraform/04-disable-local-auth/code/variables.tf new file mode 100644 index 000000000..4a14bbc37 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/04-disable-local-auth/code/variables.tf @@ -0,0 +1,41 @@ +variable "location" { + description = "The Azure region where resources will be deployed" + type = string + default = "eastus2" +} + +variable "ai_foundry_name" { + description = "The name of the AI Foundry account (must be globally unique)" + type = string + default = "foundry-disable-localauth" +} + +variable "ai_project_name" { + description = "The name of the AI Foundry project" + type = string + default = null # Will default to {ai_foundry_name}-proj +} + +variable "model_deployment_name" { + description = "The name of the model deployment" + type = string + default = "gpt-4.1-mini" +} + +variable "model_name" { + description = "The model to deploy" + type = string + default = "gpt-4.1-mini" +} + +variable "model_version" { + description = "The version of the model" + type = string + default = "2025-04-14" +} + +variable "model_capacity" { + description = "The capacity (quota) for the model deployment" + type = number + default = 1 +} diff --git a/infrastructure/infrastructure-setup-terraform/04-disable-local-auth/code/versions.tf b/infrastructure/infrastructure-setup-terraform/04-disable-local-auth/code/versions.tf new file mode 100644 index 000000000..e597a8a69 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/04-disable-local-auth/code/versions.tf @@ -0,0 +1,18 @@ +# Configure the AzApi and AzureRM providers +terraform { + required_providers { + azapi = { + source = "azure/azapi" + version = "~> 2.5" + } + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.37" + } + random = { + source = "hashicorp/random" + version = "~> 3.7" + } + } + required_version = ">= 1.10.0, < 2.0.0" +} diff --git a/infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/README.md b/infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/README.md new file mode 100644 index 000000000..6687052e2 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/README.md @@ -0,0 +1,42 @@ +# Azure Policy Definitions for Microsoft Foundry + +This folder contains examples for implementing custom Azure Policy definitions to govern Microsoft Foundry deployments. + +## Overview + +Azure Policy helps enforce organizational standards and assess compliance at scale. Custom policy definitions can restrict certain connection types or enforce specific configurations. + +## Example Policies + +- Deny disallowed connection types +- Enforce private endpoints +- Require customer-managed keys +- Mandate specific networking configurations + +## Prerequisites + +- Policy Contributor role or equivalent permissions +- Understanding of Azure Policy definition structure + +## Usage + +Define policy rules using Azure Policy definition syntax. Apply policies at subscription or resource group scope. + +## Status + +This folder now includes complete Terraform policy samples with: +- Custom policy definitions for connection category controls +- Optional subscription policy assignment +- Additional policy examples for auth and account-kind governance + +## Example Reference + +The Bicep folder contains a `deny-disallowed-connections.json` policy that can be adapted to Terraform. + +## Documentation + +- [Azure Policy overview](https://learn.microsoft.com/en-us/azure/governance/policy/overview) +- [Azure Policy definition structure](https://learn.microsoft.com/en-us/azure/governance/policy/concepts/definition-structure) +- [azurerm_policy_definition - Terraform](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/policy_definition) + +`Tags: Azure Policy, Governance, Compliance` diff --git a/infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/code/example.tfvars b/infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/code/example.tfvars new file mode 100644 index 000000000..bb38f4053 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/code/example.tfvars @@ -0,0 +1,14 @@ +# Example configuration for custom policy definitions + +# Policy definition name +policy_name = "deny-disallowed-foundry-connections" + +# Allowed connection categories (customize as needed) +allowed_categories = ["BingLLMSearch", "CognitiveSearch", "AzureOpenAI"] + +# Set to true to assign the policy to the subscription +assign_policy = false + +# Policy assignment configuration (used if assign_policy = true) +assignment_name = "deny-disallowed-connections-assignment" +assignment_display_name = "Deny Disallowed Foundry Connections" diff --git a/infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/code/main.tf b/infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/code/main.tf new file mode 100644 index 000000000..1d2cf49e1 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/code/main.tf @@ -0,0 +1,223 @@ +########## Create Azure Policy Definitions +########## + +## Get subscription data +data "azurerm_client_config" "current" {} +data "azurerm_subscription" "current" {} + +## Policy Definition: Deny Disallowed Connection Categories +resource "azurerm_policy_definition" "deny_disallowed_connections" { + name = var.policy_name + policy_type = "Custom" + mode = "All" + display_name = "Only selected Foundry connection categories are allowed" + description = "Only selected Foundry connection categories are allowed" + + metadata = jsonencode({ + version = "1.0.0" + }) + + parameters = jsonencode({ + allowedCategories = { + type = "Array" + metadata = { + description = "Categories allowed for Microsoft.CognitiveServices/accounts/connections and Microsoft.CognitiveServices/accounts/projects/connections" + displayName = "Allowed connection categories" + } + defaultValue = var.allowed_categories + } + }) + + policy_rule = jsonencode({ + if = { + anyOf = [ + { + allOf = [ + { + field = "type" + equals = "Microsoft.CognitiveServices/accounts/connections" + }, + { + field = "Microsoft.CognitiveServices/accounts/connections/category" + notIn = "[parameters('allowedCategories')]" + } + ] + }, + { + allOf = [ + { + field = "type" + equals = "Microsoft.CognitiveServices/accounts/projects/connections" + }, + { + field = "Microsoft.CognitiveServices/accounts/projects/connections/category" + notIn = "[parameters('allowedCategories')]" + } + ] + } + ] + } + then = { + effect = "deny" + } + }) +} + +## Policy Assignment (optional) +resource "azurerm_subscription_policy_assignment" "deny_disallowed_connections" { + count = var.assign_policy ? 1 : 0 + name = var.assignment_name + display_name = var.assignment_display_name + policy_definition_id = azurerm_policy_definition.deny_disallowed_connections.id + subscription_id = data.azurerm_subscription.current.id + + parameters = jsonencode({ + allowedCategories = { + value = var.allowed_categories + } + }) +} + +## Additional Policy: Deny Key-Based Authentication Connections +resource "azurerm_policy_definition" "deny_key_auth_connections" { + name = "deny-key-auth-connections" + policy_type = "Custom" + mode = "All" + display_name = "Deny AI Foundry connections using API key-based authentication" + description = "This policy denies the creation of connections that use API key authentication for enhanced security" + + metadata = jsonencode({ + version = "1.0.0" + }) + + policy_rule = jsonencode({ + if = { + anyOf = [ + { + allOf = [ + { + field = "type" + equals = "Microsoft.CognitiveServices/accounts/projects/connections" + }, + { + field = "Microsoft.CognitiveServices/accounts/projects/connections/authType" + equals = "ApiKey" + } + ] + }, + { + allOf = [ + { + field = "type" + equals = "Microsoft.CognitiveServices/accounts/connections" + }, + { + field = "Microsoft.CognitiveServices/accounts/connections/authType" + equals = "ApiKey" + } + ] + } + ] + } + then = { + effect = "deny" + } + }) +} + +## Policy: Deny Disallowed MCP Tools +resource "azurerm_policy_definition" "deny_disallowed_mcp_tools" { + name = "deny-disallowed-mcp-tools" + policy_type = "Custom" + mode = "All" + display_name = "Only allow Foundry MCP connections from select sources" + description = "Only selected Foundry MCP connection sources are allowed" + + metadata = jsonencode({ + version = "1.0.0" + }) + + parameters = jsonencode({ + allowedSources = { + type = "Array" + metadata = { + description = "Only select target addresses are allowed for MCP connections." + displayName = "Allowed connection targets" + } + defaultValue = var.allowed_mcp_sources + } + }) + + policy_rule = jsonencode({ + if = { + anyOf = [ + { + allOf = [ + { + field = "type" + equals = "Microsoft.CognitiveServices/accounts/connections" + }, + { + field = "Microsoft.CognitiveServices/accounts/connections/category" + equals = "RemoteTool" + }, + { + field = "Microsoft.CognitiveServices/accounts/connections/target" + notIn = "[parameters('allowedSources')]" + } + ] + }, + { + allOf = [ + { + field = "type" + equals = "Microsoft.CognitiveServices/accounts/projects/connections" + }, + { + field = "Microsoft.CognitiveServices/accounts/connections/category" + equals = "RemoteTool" + }, + { + field = "Microsoft.CognitiveServices/accounts/projects/connections/target" + notIn = "[parameters('allowedSources')]" + } + ] + } + ] + } + then = { + effect = "deny" + } + }) +} + +## Additional Policy: Deny Non-AIServices Resource Kinds +resource "azurerm_policy_definition" "deny_non_foundry_kinds" { + name = "deny-non-foundry-resource-kinds" + policy_type = "Custom" + mode = "All" + display_name = "Deny account kinds that do not support the full AI Foundry capabilities." + description = "This policy denies the creation of account kinds that do not support the full AI Foundry capabilities." + + metadata = jsonencode({ + version = "1.0.0" + }) + + policy_rule = jsonencode({ + if = { + allOf = [ + { + field = "type" + equals = "Microsoft.CognitiveServices/accounts" + }, + { + field = "kind" + notEquals = "AIServices" + } + ] + } + then = { + effect = "deny" + } + }) +} diff --git a/infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/code/outputs.tf b/infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/code/outputs.tf new file mode 100644 index 000000000..5cdee3545 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/code/outputs.tf @@ -0,0 +1,24 @@ +output "policy_definition_id" { + description = "The ID of the deny disallowed connections policy definition" + value = azurerm_policy_definition.deny_disallowed_connections.id +} + +output "policy_definition_name" { + description = "The name of the deny disallowed connections policy definition" + value = azurerm_policy_definition.deny_disallowed_connections.name +} + +output "policy_assignment_id" { + description = "The ID of the policy assignment (if created)" + value = var.assign_policy ? azurerm_subscription_policy_assignment.deny_disallowed_connections[0].id : null +} + +output "deny_key_auth_policy_id" { + description = "The ID of the deny key auth connections policy definition" + value = azurerm_policy_definition.deny_key_auth_connections.id +} + +output "deny_non_foundry_kinds_policy_id" { + description = "The ID of the deny non-Foundry kinds policy definition" + value = azurerm_policy_definition.deny_non_foundry_kinds.id +} diff --git a/infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/code/providers.tf b/infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/code/providers.tf new file mode 100644 index 000000000..41f10fc5c --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/code/providers.tf @@ -0,0 +1,4 @@ +# Setup providers +provider "azurerm" { + features {} +} diff --git a/infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/code/variables.tf b/infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/code/variables.tf new file mode 100644 index 000000000..425b4f08d --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/code/variables.tf @@ -0,0 +1,35 @@ +variable "policy_name" { + description = "Name for the custom policy definition" + type = string + default = "deny-disallowed-foundry-connections" +} + +variable "allowed_categories" { + description = "List of allowed connection categories for AI Foundry" + type = list(string) + default = ["BingLLMSearch"] +} + +variable "assign_policy" { + description = "Whether to assign the policy to the subscription" + type = bool + default = false +} + +variable "assignment_name" { + description = "Name for the policy assignment (if enabled)" + type = string + default = "deny-disallowed-connections-assignment" +} + +variable "assignment_display_name" { + description = "Display name for the policy assignment" + type = string + default = "Deny Disallowed Foundry Connections" +} + +variable "allowed_mcp_sources" { + description = "List of allowed MCP connection target addresses" + type = list(string) + default = ["https://api.githubcopilot.com/mcp"] +} diff --git a/infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/code/versions.tf b/infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/code/versions.tf new file mode 100644 index 000000000..ca6c88657 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/05-custom-policy-definitions/code/versions.tf @@ -0,0 +1,10 @@ +# Configure the AzureRM provider +terraform { + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.37" + } + } + required_version = ">= 1.10.0, < 2.0.0" +} diff --git a/infrastructure/infrastructure-setup-terraform/10-private-network-basic/README.md b/infrastructure/infrastructure-setup-terraform/10-private-network-basic/README.md new file mode 100644 index 000000000..5873a9ea2 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/10-private-network-basic/README.md @@ -0,0 +1,60 @@ +# Deploy Microsoft Foundry with Private Network Configuration + +This Terraform template deploys an Microsoft Foundry resource with public network access disabled and a private endpoint for secure access. + +## Description + +- Creates an Microsoft Foundry account with `publicNetworkAccess: Disabled` +- Creates a virtual network and subnet for private endpoints +- Creates a private endpoint to access the Microsoft Foundry resource +- Creates a project +- Deploys a GPT-4o model + +## Prerequisites + +- Azure CLI or Terraform installed +- Appropriate Azure permissions +- Access to the VNet (VM, VPN, or ExpressRoute) to use the private Foundry resource + +## Deployment + +1. Navigate to the code directory: +```bash +cd code +``` + +2. Initialize Terraform: +```bash +terraform init +``` + +3. Review and customize variables + +4. Deploy: +```bash +terraform plan +terraform apply +``` + +## Important Notes + +- To access your Foundry resource securely, use a VM, VPN, or ExpressRoute connected to the VNet +- Public network access is completely disabled +- Private DNS zone integration may be needed for name resolution + +## Resources Created + +- Virtual Network and Subnet +- Microsoft Foundry account (with public network access disabled) +- Private Endpoint +- Microsoft Foundry project +- Model deployment + +## Documentation + +- [Configure private link for Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-link) +- [azurerm_private_endpoint - Terraform](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/private_endpoint) +- [azurerm_private_dns_zone - Terraform](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/private_dns_zone) +- [AzAPI Provider](https://registry.terraform.io/providers/azure/azapi/latest/docs) + +`Tags: Microsoft.CognitiveServices/accounts, Microsoft.Network/virtualNetworks, Microsoft.Network/privateEndpoints` diff --git a/infrastructure/infrastructure-setup-terraform/10-private-network-basic/code/example.tfvars b/infrastructure/infrastructure-setup-terraform/10-private-network-basic/code/example.tfvars new file mode 100644 index 000000000..d6c2f233a --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/10-private-network-basic/code/example.tfvars @@ -0,0 +1,11 @@ +# Example terraform.tfvars file +# Copy this to terraform.tfvars and customize for your deployment + +location = "eastus2" +ai_foundry_name = "foundrypnadisabled" +virtual_network_name = "private-vnet" +virtual_network_address_space = "192.168.0.0/16" +pe_subnet_name = "pe-subnet" +pe_subnet_address_prefix = "192.168.0.0/24" +model_name = "gpt-4o" +model_version = "2024-08-06" diff --git a/infrastructure/infrastructure-setup-terraform/10-private-network-basic/code/main.tf b/infrastructure/infrastructure-setup-terraform/10-private-network-basic/code/main.tf new file mode 100644 index 000000000..d1fc017ef --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/10-private-network-basic/code/main.tf @@ -0,0 +1,174 @@ +########## Create infrastructure resources +########## + +## Get subscription data +data "azurerm_client_config" "current" {} + +## Create a random string for unique naming +resource "random_string" "unique" { + length = 4 + min_numeric = 4 + numeric = true + special = false + lower = true + upper = false +} + +## Create a resource group +resource "azurerm_resource_group" "rg" { + name = "rg-aifoundry${random_string.unique.result}" + location = var.location +} + +## Create a virtual network +resource "azurerm_virtual_network" "vnet" { + name = var.virtual_network_name + location = var.location + resource_group_name = azurerm_resource_group.rg.name + address_space = [var.virtual_network_address_space] +} + +## Create a subnet for private endpoints +resource "azurerm_subnet" "pe_subnet" { + name = var.pe_subnet_name + resource_group_name = azurerm_resource_group.rg.name + virtual_network_name = azurerm_virtual_network.vnet.name + address_prefixes = [var.pe_subnet_address_prefix] +} + +## Create AI Foundry account with public network access disabled +resource "azapi_resource" "ai_foundry" { + type = "Microsoft.CognitiveServices/accounts@2025-06-01" + name = var.ai_foundry_name + location = var.location + parent_id = azurerm_resource_group.rg.id + + identity { + type = "SystemAssigned" + } + + body = { + kind = "AIServices" + sku = { + name = "S0" + } + properties = { + allowProjectManagement = true + customSubDomainName = format("%s%s", var.ai_foundry_name, random_string.unique.result) + disableLocalAuth = false + publicNetworkAccess = "Disabled" + } + } +} + +## Create private DNS zones +resource "azurerm_private_dns_zone" "ai_services" { + name = "privatelink.services.ai.azure.com" + resource_group_name = azurerm_resource_group.rg.name +} + +resource "azurerm_private_dns_zone" "openai" { + name = "privatelink.openai.azure.com" + resource_group_name = azurerm_resource_group.rg.name +} + +resource "azurerm_private_dns_zone" "cognitive_services" { + name = "privatelink.cognitiveservices.azure.com" + resource_group_name = azurerm_resource_group.rg.name +} + +## Link DNS zones to VNet +resource "azurerm_private_dns_zone_virtual_network_link" "ai_services_link" { + name = "aiServices-link" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.ai_services.name + virtual_network_id = azurerm_virtual_network.vnet.id + registration_enabled = false +} + +resource "azurerm_private_dns_zone_virtual_network_link" "openai_link" { + name = "aiServicesOpenAI-link" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.openai.name + virtual_network_id = azurerm_virtual_network.vnet.id + registration_enabled = false +} + +resource "azurerm_private_dns_zone_virtual_network_link" "cognitive_services_link" { + name = "aiServicesCognitiveServices-link" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.cognitive_services.name + virtual_network_id = azurerm_virtual_network.vnet.id + registration_enabled = false +} + +## Create private endpoint for AI Foundry +resource "azurerm_private_endpoint" "ai_foundry_pe" { + name = "${var.ai_foundry_name}-pe" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + subnet_id = azurerm_subnet.pe_subnet.id + + private_service_connection { + name = "${var.ai_foundry_name}-psc" + private_connection_resource_id = azapi_resource.ai_foundry.id + is_manual_connection = false + subresource_names = ["account"] + } + + private_dns_zone_group { + name = "${var.ai_foundry_name}-dns-group" + private_dns_zone_ids = [ + azurerm_private_dns_zone.ai_services.id, + azurerm_private_dns_zone.openai.id, + azurerm_private_dns_zone.cognitive_services.id + ] + } + + depends_on = [ + azurerm_private_dns_zone_virtual_network_link.ai_services_link, + azurerm_private_dns_zone_virtual_network_link.openai_link, + azurerm_private_dns_zone_virtual_network_link.cognitive_services_link + ] +} + +## Create AI Foundry project +resource "azapi_resource" "ai_project" { + type = "Microsoft.CognitiveServices/accounts/projects@2025-06-01" + name = coalesce(var.ai_project_name, "${var.ai_foundry_name}-proj") + location = var.location + parent_id = azapi_resource.ai_foundry.id + + identity { + type = "SystemAssigned" + } + + body = { + properties = {} + } + + depends_on = [azurerm_private_endpoint.ai_foundry_pe] +} + +## Deploy model +resource "azapi_resource" "model_deployment" { + type = "Microsoft.CognitiveServices/accounts/deployments@2025-06-01" + name = var.model_name + parent_id = azapi_resource.ai_foundry.id + + body = { + sku = { + capacity = 1 + name = "GlobalStandard" + } + properties = { + model = { + name = var.model_name + format = "OpenAI" + version = var.model_version + } + } + } + + depends_on = [azapi_resource.ai_project] +} diff --git a/infrastructure/infrastructure-setup-terraform/10-private-network-basic/code/outputs.tf b/infrastructure/infrastructure-setup-terraform/10-private-network-basic/code/outputs.tf new file mode 100644 index 000000000..85d751a78 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/10-private-network-basic/code/outputs.tf @@ -0,0 +1,24 @@ +output "resource_group_name" { + description = "The name of the resource group" + value = azurerm_resource_group.rg.name +} + +output "virtual_network_id" { + description = "The ID of the virtual network" + value = azurerm_virtual_network.vnet.id +} + +output "ai_foundry_id" { + description = "The ID of the AI Foundry account" + value = azapi_resource.ai_foundry.id +} + +output "private_endpoint_id" { + description = "The ID of the private endpoint" + value = azurerm_private_endpoint.ai_foundry_pe.id +} + +output "ai_project_id" { + description = "The ID of the AI Foundry project" + value = azapi_resource.ai_project.id +} diff --git a/infrastructure/infrastructure-setup-terraform/10-private-network-basic/code/providers.tf b/infrastructure/infrastructure-setup-terraform/10-private-network-basic/code/providers.tf new file mode 100644 index 000000000..9a71e9677 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/10-private-network-basic/code/providers.tf @@ -0,0 +1,8 @@ +# Setup providers +provider "azapi" { +} + +provider "azurerm" { + features {} + storage_use_azuread = true +} diff --git a/infrastructure/infrastructure-setup-terraform/10-private-network-basic/code/variables.tf b/infrastructure/infrastructure-setup-terraform/10-private-network-basic/code/variables.tf new file mode 100644 index 000000000..c8208e58f --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/10-private-network-basic/code/variables.tf @@ -0,0 +1,53 @@ +variable "location" { + description = "The Azure region where resources will be deployed" + type = string + default = "eastus2" +} + +variable "ai_foundry_name" { + description = "The name of the AI Foundry account" + type = string + default = "foundrypnadisabled" +} + +variable "ai_project_name" { + description = "The name of the AI Foundry project" + type = string + default = null # Will default to {ai_foundry_name}-proj +} + +variable "virtual_network_name" { + description = "The name of the virtual network" + type = string + default = "private-vnet" +} + +variable "virtual_network_address_space" { + description = "The address space for the virtual network" + type = string + default = "192.168.0.0/16" +} + +variable "pe_subnet_name" { + description = "The name of the private endpoint subnet" + type = string + default = "pe-subnet" +} + +variable "pe_subnet_address_prefix" { + description = "The address prefix for the private endpoint subnet" + type = string + default = "192.168.0.0/24" +} + +variable "model_name" { + description = "The model to deploy" + type = string + default = "gpt-4o" +} + +variable "model_version" { + description = "The version of the model" + type = string + default = "2024-08-06" +} diff --git a/infrastructure/infrastructure-setup-terraform/10-private-network-basic/code/versions.tf b/infrastructure/infrastructure-setup-terraform/10-private-network-basic/code/versions.tf new file mode 100644 index 000000000..e597a8a69 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/10-private-network-basic/code/versions.tf @@ -0,0 +1,18 @@ +# Configure the AzApi and AzureRM providers +terraform { + required_providers { + azapi = { + source = "azure/azapi" + version = "~> 2.5" + } + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.37" + } + random = { + source = "hashicorp/random" + version = "~> 3.7" + } + } + required_version = ">= 1.10.0, < 2.0.0" +} diff --git a/infrastructure/infrastructure-setup-terraform/15a-private-network-standard-agent-setup/README.md b/infrastructure/infrastructure-setup-terraform/15a-private-network-standard-agent-setup/README.md index cf9346ff5..e221e47e1 100644 --- a/infrastructure/infrastructure-setup-terraform/15a-private-network-standard-agent-setup/README.md +++ b/infrastructure/infrastructure-setup-terraform/15a-private-network-standard-agent-setup/README.md @@ -18,7 +18,7 @@ languages: > > Private Class A subnet support is GA and available in the following regions. **Supported regions: Australia East, Brazil South, Canada East, East US, East US 2, France Central, Germany West Central, Italy North, Japan East, South Africa North, South Central US, South India, Spain Central, Sweden Central, UAE North, UK South, West Europe, West US, West US 3.** > -> Private Class B and C subnet support is already GA and available in all regions supported by Azure AI Foundry Agent Service. Deployment templates and setup steps are identical for Class A, B, and C subnets. For more on the supported regions of the Azure AI Foundry Agent service, see [Models supported by Azure AI Foundry Agent Service](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/concepts/model-region-support?tabs=global-standard) +> Private Class B and C subnet support is already GA and available in all regions supported by Microsoft Foundry Agent Service. Deployment templates and setup steps are identical for Class A, B, and C subnets. For more on the supported regions of the Microsoft Foundry Agent service, see [Models supported by Microsoft Foundry Agent Service](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/concepts/model-region-support?tabs=global-standard) ## Key Information @@ -76,7 +76,7 @@ Note: The following resources will be created automatically for you: - Azure Cosmos DB for NoSQL - Azure AI Search - Azure Storage -- AI Foundry resource +- Microsoft Foundry resource - Virtual network with two subnets - Private Endpoints for resources above @@ -153,12 +153,12 @@ The architecture this deployment supports is pictured below with the resources d - Create new Azure Storage resource - Create new Azure AI Search resource -5. Create Azure AI Foundry Resource (Cognitive Services/accounts, kind=AIServices) +5. Create Microsoft Foundry Resource (Cognitive Services/accounts, kind=AIServices) 6. Create account-level connections: - Deploy GPT-4o or other agent-compatible model -7. Create private endpoints with DNS resolution for the Azure Resources: Azure Cosmos DB Account, Azure Storage Storage, Azure AI Search, and Azure AI Foundry +7. Create private endpoints with DNS resolution for the Azure Resources: Azure Cosmos DB Account, Azure Storage Storage, Azure AI Search, and Microsoft Foundry 8. Create Project (Cognitive Services/accounts/project) @@ -186,7 +186,7 @@ The architecture this deployment supports is pictured below with the resources d The deployment creates an isolated network environment: - **Private Endpoints:** - - AI Foundry + - Microsoft Foundry - AI Search - CosmosDB - Storage @@ -197,7 +197,7 @@ The deployment creates an isolated network environment: ### Core Components -1. **AI Foundry Resource** +1. **Microsoft Foundry Resource** - Central orchestration point - Manages service connections - Network-isolated capability hosts @@ -285,8 +285,8 @@ code/ ## References -- [Azure AI Foundry Networking Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-link?tabs=azure-portal&pivots=fdp-project) -- [Azure AI Foundry RBAC Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/rbac-azure-ai-foundry?pivots=fdp-project) +- [Microsoft Foundry Networking Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-link?tabs=azure-portal&pivots=fdp-project) +- [Microsoft Foundry RBAC Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/rbac-azure-ai-foundry?pivots=fdp-project) - [Private Endpoint Documentation](https://learn.microsoft.com/en-us/azure/private-link/) - [RBAC Documentation](https://learn.microsoft.com/en-us/azure/role-based-access-control/) - [Network Security Best Practices](https://learn.microsoft.com/en-us/azure/security/fundamentals/network-best-practices) diff --git a/infrastructure/infrastructure-setup-terraform/15a-private-network-standard-agent-setup/code/main.tf b/infrastructure/infrastructure-setup-terraform/15a-private-network-standard-agent-setup/code/main.tf index 49a2a8538..14950dd85 100644 --- a/infrastructure/infrastructure-setup-terraform/15a-private-network-standard-agent-setup/code/main.tf +++ b/infrastructure/infrastructure-setup-terraform/15a-private-network-standard-agent-setup/code/main.tf @@ -148,7 +148,7 @@ resource "azapi_resource" "ai_search" { # Search-specific properties replicaCount = 1 partitionCount = 1 - hostingMode = "default" + hostingMode = "Default" semanticSearch = "disabled" # Identity-related controls diff --git a/infrastructure/infrastructure-setup-terraform/15b-private-network-standard-agent-setup-byovnet/README.md b/infrastructure/infrastructure-setup-terraform/15b-private-network-standard-agent-setup-byovnet/README.md index b106eda20..dabbb0b94 100644 --- a/infrastructure/infrastructure-setup-terraform/15b-private-network-standard-agent-setup-byovnet/README.md +++ b/infrastructure/infrastructure-setup-terraform/15b-private-network-standard-agent-setup-byovnet/README.md @@ -17,7 +17,7 @@ languages: > > Private Class A subnet support is GA and available in the following regions. **Supported regions: Australia East, Brazil South, Canada East, East US, East US 2, France Central, Germany West Central, Italy North, Japan East, South Africa North, South Central US, South India, Spain Central, Sweden Central, UAE North, UK South, West Europe, West US, West US 3.** > -> Private Class B and C subnet support is already GA and available in all regions supported by Azure AI Foundry Agent Service. Deployment templates and setup steps are identical for Class A, B, and C subnets. For more on the supported regions of the Azure AI Foundry Agent service, see [Models supported by Azure AI Foundry Agent Service](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/concepts/model-region-support?tabs=global-standard) +> Private Class B and C subnet support is already GA and available in all regions supported by Microsoft Foundry Agent Service. Deployment templates and setup steps are identical for Class A, B, and C subnets. For more on the supported regions of the Microsoft Foundry Agent service, see [Models supported by Microsoft Foundry Agent Service](https://learn.microsoft.com/en-us/azure/ai-foundry/agents/concepts/model-region-support?tabs=global-standard) ## Key Information @@ -88,7 +88,7 @@ Note: The following resources will be created automatically for you: - Azure Cosmos DB for NoSQL - Azure AI Search - Azure Storage -- AI Foundry resource +- Microsoft Foundry resource - Private Endpoints for resources above ### Variables @@ -155,12 +155,12 @@ The architecture this deployment supports is pictured below with the resources d - Create new Azure Storage resource - Create new Azure AI Search resource -2. Create Azure AI Foundry Resource (Cognitive Services/accounts, kind=AIServices) +2. Create Microsoft Foundry Resource (Cognitive Services/accounts, kind=AIServices) 3. Create account-level connections: - Deploy GPT-4o or other agent-compatible model -4. Create private endpoints with DNS resolution for the Azure Resources: Azure Cosmos DB Account, Azure Storage Storage, Azure AI Search, and Azure AI Foundry +4. Create private endpoints with DNS resolution for the Azure Resources: Azure Cosmos DB Account, Azure Storage Storage, Azure AI Search, and Microsoft Foundry 5. Create Project (Cognitive Services/accounts/project) @@ -188,7 +188,7 @@ The architecture this deployment supports is pictured below with the resources d The deployment creates an isolated network environment: - **Private Endpoints:** - - AI Foundry + - Microsoft Foundry - AI Search - CosmosDB - Storage @@ -199,7 +199,7 @@ The deployment creates an isolated network environment: ### Core Components -1. **AI Foundry Resource** +1. **Microsoft Foundry Resource** - Central orchestration point - Manages service connections - Network-isolated capability hosts @@ -286,8 +286,8 @@ code/ ## References -- [Azure AI Foundry Networking Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-link?tabs=azure-portal&pivots=fdp-project) -- [Azure AI Foundry RBAC Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/rbac-azure-ai-foundry?pivots=fdp-project) +- [Microsoft Foundry Networking Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-link?tabs=azure-portal&pivots=fdp-project) +- [Microsoft Foundry RBAC Documentation](https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/rbac-azure-ai-foundry?pivots=fdp-project) - [Private Endpoint Documentation](https://learn.microsoft.com/en-us/azure/private-link/) - [RBAC Documentation](https://learn.microsoft.com/en-us/azure/role-based-access-control/) - [Network Security Best Practices](https://learn.microsoft.com/en-us/azure/security/fundamentals/network-best-practices) diff --git a/infrastructure/infrastructure-setup-terraform/15b-private-network-standard-agent-setup-byovnet/code/main.tf b/infrastructure/infrastructure-setup-terraform/15b-private-network-standard-agent-setup-byovnet/code/main.tf index ec160098d..564e53325 100644 --- a/infrastructure/infrastructure-setup-terraform/15b-private-network-standard-agent-setup-byovnet/code/main.tf +++ b/infrastructure/infrastructure-setup-terraform/15b-private-network-standard-agent-setup-byovnet/code/main.tf @@ -102,7 +102,7 @@ resource "azapi_resource" "ai_search" { # Search-specific properties replicaCount = 1 partitionCount = 1 - hostingMode = "default" + hostingMode = "Default" semanticSearch = "disabled" # Identity-related controls diff --git a/infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/README.md b/infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/README.md new file mode 100644 index 000000000..c98397298 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/README.md @@ -0,0 +1,60 @@ +# Private Network Standard Agent with API Management Setup (Preview) + +This folder provides a Terraform implementation for private network standard agent setup with Azure API Management integration. + +## Overview + +This advanced scenario combines: +- Private network configuration with VNet injection +- Standard agent setup with BYOS (bring-your-own-storage/search) +- Azure API Management integration for controlled access +- Private endpoints for all services + +## Status + +**🚧 PARTIALLY IMPLEMENTED** - This is a complex scenario requiring: + +1. Private networking infrastructure (VNets, subnets, private endpoints) +2. Standard agent setup with Storage, Search, and Cosmos DB +3. Azure API Management configuration +4. Private DNS zones and network integration +5. Complex RBAC and connection setup + +## Reference + +For implementation guidance, see the Bicep reference: +- `infrastructure-setup-bicep/16-private-network-standard-agent-apim-setup-preview` + +For a working private network example, see: +- `../15a-private-network-standard-agent-setup` + +## Prerequisites + +- Advanced networking knowledge +- API Management experience +- Understanding of private endpoints and DNS +- Familiarity with standard agent setup + +## AzAPI Usage Rationale + +This scenario requires extensive use of AzAPI provider for: +- Microsoft Foundry account and project configuration +- Connection resources (not yet in AzureRM) +- Advanced networking configurations + +## Contributing + +If you implement this scenario, please: +1. Follow the existing Terraform patterns in this repository +2. Use AzAPI for unsupported resources +3. Include comprehensive README documentation +4. Add example.tfvars with all required variables + +## Documentation + +- [Configure private link for Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-link) +- [Azure API Management with virtual networks](https://learn.microsoft.com/en-us/azure/api-management/api-management-using-with-vnet) +- [azurerm_api_management - Terraform](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/api_management) +- [AzAPI Provider](https://registry.terraform.io/providers/azure/azapi/latest/docs) + +`Tags: Private Network, APIM, Standard Agent, Advanced, Preview` diff --git a/infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/code/example.tfvars b/infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/code/example.tfvars new file mode 100644 index 000000000..26fa99143 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/code/example.tfvars @@ -0,0 +1,29 @@ +# Example configuration for private network standard agent with API Management + +# Azure region +location = "eastus2" + +# AI Foundry configuration +ai_services_name_prefix = "foundry" +project_name = "private-apim-agent-project" + +# Network configuration +vnet_address_space = ["10.0.0.0/16"] +subnet_private_endpoints_prefix = "10.0.1.0/24" +subnet_apim_prefix = "10.0.2.0/24" + +# API Management configuration +apim_sku = "Developer" # Use Developer for testing, Premium for production +apim_publisher_name = "AI Foundry Publisher" +apim_publisher_email = "admin@example.com" + +# Model configuration +model_name = "gpt-4.1" +model_version = "2025-04-14" +model_capacity = 40 + +# IMPORTANT NOTES: +# 1. API Management deployment can take 30-45 minutes +# 2. APIM with Internal VNet requires additional DNS configuration +# 3. Additional APIM policies and configurations need to be set up manually or with additional Terraform resources +# 4. Consider using Application Gateway if you need external access to internal APIM diff --git a/infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/code/main.tf b/infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/code/main.tf new file mode 100644 index 000000000..b828beb6b --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/code/main.tf @@ -0,0 +1,529 @@ +########## Create private network infrastructure with Standard Agent and API Management +########## + +## NOTE: This is a complex advanced scenario combining private networking, +## standard agent setup (BYOS), and Azure API Management integration + +## Get subscription data +data "azurerm_client_config" "current" {} + +## Create a random string for unique naming +resource "random_string" "unique" { + length = 4 + min_numeric = 4 + numeric = true + special = false + lower = true + upper = false +} + +locals { + account_name = lower("${var.ai_services_name_prefix}${random_string.unique.result}") +} + +## Create a resource group +resource "azurerm_resource_group" "rg" { + name = "rg-aifoundry${random_string.unique.result}" + location = var.location +} + +## Create Virtual Network +resource "azurerm_virtual_network" "vnet" { + name = "vnet-aifoundry${random_string.unique.result}" + address_space = var.vnet_address_space + location = var.location + resource_group_name = azurerm_resource_group.rg.name +} + +## Create Subnet for private endpoints +resource "azurerm_subnet" "subnet_pe" { + name = "subnet-private-endpoints" + resource_group_name = azurerm_resource_group.rg.name + virtual_network_name = azurerm_virtual_network.vnet.name + address_prefixes = [var.subnet_private_endpoints_prefix] +} + +## Create Subnet for API Management +resource "azurerm_subnet" "subnet_apim" { + name = "subnet-apim" + resource_group_name = azurerm_resource_group.rg.name + virtual_network_name = azurerm_virtual_network.vnet.name + address_prefixes = [var.subnet_apim_prefix] +} + +## NSG required for APIM internal VNet deployment +resource "azurerm_network_security_group" "apim" { + name = "nsg-apim-${random_string.unique.result}" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + + ## Inbound rules + security_rule { + name = "AllowAPIMManagement" + priority = 100 + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "3443" + source_address_prefix = "ApiManagement" + destination_address_prefix = "VirtualNetwork" + } + + security_rule { + name = "AllowAzureLoadBalancer" + priority = 110 + direction = "Inbound" + access = "Allow" + protocol = "*" + source_port_range = "*" + destination_port_range = "6390" + source_address_prefix = "AzureLoadBalancer" + destination_address_prefix = "VirtualNetwork" + } + + security_rule { + name = "AllowHTTPSInbound" + priority = 120 + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "443" + source_address_prefix = "VirtualNetwork" + destination_address_prefix = "VirtualNetwork" + } + + ## Outbound rules required for APIM dependencies + security_rule { + name = "AllowStorageOutbound" + priority = 100 + direction = "Outbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "443" + source_address_prefix = "VirtualNetwork" + destination_address_prefix = "Storage" + } + + security_rule { + name = "AllowSQLOutbound" + priority = 110 + direction = "Outbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "1433" + source_address_prefix = "VirtualNetwork" + destination_address_prefix = "Sql" + } + + security_rule { + name = "AllowKeyVaultOutbound" + priority = 120 + direction = "Outbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "443" + source_address_prefix = "VirtualNetwork" + destination_address_prefix = "AzureKeyVault" + } + + security_rule { + name = "AllowMonitorOutbound" + priority = 130 + direction = "Outbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_ranges = ["443", "1886"] + source_address_prefix = "VirtualNetwork" + destination_address_prefix = "AzureMonitor" + } + + security_rule { + name = "AllowEntraIDOutbound" + priority = 140 + direction = "Outbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "443" + source_address_prefix = "VirtualNetwork" + destination_address_prefix = "AzureActiveDirectory" + } +} + +resource "azurerm_subnet_network_security_group_association" "apim" { + subnet_id = azurerm_subnet.subnet_apim.id + network_security_group_id = azurerm_network_security_group.apim.id +} + +## Create Private DNS Zones +resource "azurerm_private_dns_zone" "ai_foundry" { + name = "privatelink.cognitiveservices.azure.com" + resource_group_name = azurerm_resource_group.rg.name +} + +resource "azurerm_private_dns_zone" "storage" { + name = "privatelink.blob.core.windows.net" + resource_group_name = azurerm_resource_group.rg.name +} + +resource "azurerm_private_dns_zone" "search" { + name = "privatelink.search.windows.net" + resource_group_name = azurerm_resource_group.rg.name +} + +resource "azurerm_private_dns_zone" "cosmos" { + name = "privatelink.documents.azure.com" + resource_group_name = azurerm_resource_group.rg.name +} + +## Link DNS zones to VNet +resource "azurerm_private_dns_zone_virtual_network_link" "ai_foundry" { + name = "vnet-link-ai-foundry" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.ai_foundry.name + virtual_network_id = azurerm_virtual_network.vnet.id +} + +resource "azurerm_private_dns_zone_virtual_network_link" "storage" { + name = "vnet-link-storage" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.storage.name + virtual_network_id = azurerm_virtual_network.vnet.id +} + +resource "azurerm_private_dns_zone_virtual_network_link" "search" { + name = "vnet-link-search" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.search.name + virtual_network_id = azurerm_virtual_network.vnet.id +} + +resource "azurerm_private_dns_zone_virtual_network_link" "cosmos" { + name = "vnet-link-cosmos" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.cosmos.name + virtual_network_id = azurerm_virtual_network.vnet.id +} + +## Create Storage Account with private endpoint +resource "azurerm_storage_account" "storage" { + name = "aifoundry${random_string.unique.result}stor" + resource_group_name = azurerm_resource_group.rg.name + location = var.location + account_kind = "StorageV2" + account_tier = "Standard" + account_replication_type = "ZRS" + + shared_access_key_enabled = false + min_tls_version = "TLS1_2" + allow_nested_items_to_be_public = false + public_network_access_enabled = false + + network_rules { + default_action = "Deny" + bypass = ["AzureServices"] + } +} + +resource "azurerm_private_endpoint" "storage" { + name = "pe-storage-${random_string.unique.result}" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + subnet_id = azurerm_subnet.subnet_pe.id + + private_service_connection { + name = "psc-storage" + private_connection_resource_id = azurerm_storage_account.storage.id + is_manual_connection = false + subresource_names = ["blob"] + } + + private_dns_zone_group { + name = "storage-dns-zone-group" + private_dns_zone_ids = [azurerm_private_dns_zone.storage.id] + } +} + +## Create AI Search with private endpoint +resource "azurerm_search_service" "search" { + name = replace("aifoundry-${random_string.unique.result}-search", "_", "-") + resource_group_name = azurerm_resource_group.rg.name + location = var.location + sku = "standard" + + local_authentication_enabled = true + authentication_failure_mode = "http401WithBearerChallenge" + public_network_access_enabled = false +} + +resource "azurerm_private_endpoint" "search" { + name = "pe-search-${random_string.unique.result}" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + subnet_id = azurerm_subnet.subnet_pe.id + + private_service_connection { + name = "psc-search" + private_connection_resource_id = azurerm_search_service.search.id + is_manual_connection = false + subresource_names = ["searchService"] + } + + private_dns_zone_group { + name = "search-dns-zone-group" + private_dns_zone_ids = [azurerm_private_dns_zone.search.id] + } +} + +## Create Cosmos DB with private endpoint +resource "azurerm_cosmosdb_account" "cosmos" { + name = "aifoundry${random_string.unique.result}cosmos" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + offer_type = "Standard" + kind = "GlobalDocumentDB" + public_network_access_enabled = false + is_virtual_network_filter_enabled = true + + consistency_policy { + consistency_level = "Session" + } + + geo_location { + location = var.location + failover_priority = 0 + } +} + +resource "azurerm_private_endpoint" "cosmos" { + name = "pe-cosmos-${random_string.unique.result}" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + subnet_id = azurerm_subnet.subnet_pe.id + + private_service_connection { + name = "psc-cosmos" + private_connection_resource_id = azurerm_cosmosdb_account.cosmos.id + is_manual_connection = false + subresource_names = ["Sql"] + } + + private_dns_zone_group { + name = "cosmos-dns-zone-group" + private_dns_zone_ids = [azurerm_private_dns_zone.cosmos.id] + } +} + +## Create AI Foundry account with private endpoint +resource "azapi_resource" "ai_foundry" { + type = "Microsoft.CognitiveServices/accounts@2025-04-01-preview" + name = local.account_name + location = var.location + parent_id = azurerm_resource_group.rg.id + + identity { + type = "SystemAssigned" + } + + body = { + kind = "AIServices" + sku = { + name = "S0" + } + properties = { + allowProjectManagement = true + customSubDomainName = local.account_name + publicNetworkAccess = "Disabled" + disableLocalAuth = true + networkAcls = { + defaultAction = "Deny" + virtualNetworkRules = [] + ipRules = [] + } + } + } +} + +resource "azurerm_private_endpoint" "ai_foundry" { + name = "pe-ai-foundry-${random_string.unique.result}" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + subnet_id = azurerm_subnet.subnet_pe.id + + private_service_connection { + name = "psc-ai-foundry" + private_connection_resource_id = azapi_resource.ai_foundry.id + is_manual_connection = false + subresource_names = ["account"] + } + + private_dns_zone_group { + name = "ai-foundry-dns-zone-group" + private_dns_zone_ids = [azurerm_private_dns_zone.ai_foundry.id] + } +} + +## Create API Management with VNet integration +resource "azurerm_api_management" "apim" { + name = "apim-${random_string.unique.result}" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + publisher_name = var.apim_publisher_name + publisher_email = var.apim_publisher_email + sku_name = "${var.apim_sku}_1" + + virtual_network_type = "Internal" + + virtual_network_configuration { + subnet_id = azurerm_subnet.subnet_apim.id + } + + depends_on = [azurerm_subnet_network_security_group_association.apim] +} + +## Grant AI Foundry access to Storage +resource "azurerm_role_assignment" "storage_blob_data_contributor" { + scope = azurerm_storage_account.storage.id + role_definition_name = "Storage Blob Data Contributor" + principal_id = azapi_resource.ai_foundry.identity[0].principal_id +} + +## Grant AI Foundry access to AI Search +resource "azurerm_role_assignment" "search_index_data_contributor" { + scope = azurerm_search_service.search.id + role_definition_name = "Search Index Data Contributor" + principal_id = azapi_resource.ai_foundry.identity[0].principal_id +} + +resource "azurerm_role_assignment" "search_service_contributor" { + scope = azurerm_search_service.search.id + role_definition_name = "Search Service Contributor" + principal_id = azapi_resource.ai_foundry.identity[0].principal_id +} + +## Grant AI Foundry access to Cosmos DB +resource "azurerm_cosmosdb_sql_role_assignment" "cosmos_contributor" { + resource_group_name = azurerm_resource_group.rg.name + account_name = azurerm_cosmosdb_account.cosmos.name + role_definition_id = "${azurerm_cosmosdb_account.cosmos.id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002" + principal_id = azapi_resource.ai_foundry.identity[0].principal_id + scope = azurerm_cosmosdb_account.cosmos.id +} + +## Wait for role assignments and private endpoints to propagate +resource "time_sleep" "wait_for_resources" { + depends_on = [ + azurerm_role_assignment.storage_blob_data_contributor, + azurerm_role_assignment.search_index_data_contributor, + azurerm_role_assignment.search_service_contributor, + azurerm_cosmosdb_sql_role_assignment.cosmos_contributor, + azurerm_private_endpoint.ai_foundry, + azurerm_private_endpoint.storage, + azurerm_private_endpoint.search, + azurerm_private_endpoint.cosmos, + azurerm_api_management.apim + ] + create_duration = "120s" +} + +## Create AI Foundry project +resource "azapi_resource" "ai_project" { + type = "Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview" + name = var.project_name + location = var.location + parent_id = azapi_resource.ai_foundry.id + + identity { + type = "SystemAssigned" + } + + body = { + properties = {} + } + + depends_on = [time_sleep.wait_for_resources] +} + +## Create connections +resource "azapi_resource" "storage_connection" { + type = "Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview" + name = "storage-connection" + parent_id = azapi_resource.ai_foundry.id + + body = { + properties = { + category = "AzureBlob" + target = azurerm_storage_account.storage.primary_blob_endpoint + authType = "AccessKey" + isSharedToAll = true + metadata = { + ResourceId = azurerm_storage_account.storage.id + } + } + } + + depends_on = [azapi_resource.ai_project] +} + +resource "azapi_resource" "search_connection" { + type = "Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview" + name = "search-connection" + parent_id = azapi_resource.ai_foundry.id + + body = { + properties = { + category = "CognitiveSearch" + target = "https://${azurerm_search_service.search.name}.search.windows.net" + authType = "AAD" + isSharedToAll = true + metadata = { + ResourceId = azurerm_search_service.search.id + } + } + } + + depends_on = [azapi_resource.ai_project] +} + +## Deploy model +resource "azapi_resource" "model_deployment" { + type = "Microsoft.CognitiveServices/accounts/deployments@2025-04-01-preview" + name = var.model_name + parent_id = azapi_resource.ai_foundry.id + + body = { + sku = { + capacity = var.model_capacity + name = "GlobalStandard" + } + properties = { + model = { + name = var.model_name + format = "OpenAI" + version = var.model_version + } + } + } + + depends_on = [azapi_resource.ai_project] +} + +## Create APIM API for AI Foundry (placeholder - requires additional configuration) +resource "azurerm_api_management_api" "ai_foundry_api" { + name = "ai-foundry-api" + resource_group_name = azurerm_resource_group.rg.name + api_management_name = azurerm_api_management.apim.name + revision = "1" + display_name = "AI Foundry API" + path = "ai" + protocols = ["https"] + service_url = azapi_resource.ai_foundry.output.properties.endpoint + + depends_on = [azapi_resource.ai_foundry] +} diff --git a/infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/code/outputs.tf b/infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/code/outputs.tf new file mode 100644 index 000000000..0dd32fc30 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/code/outputs.tf @@ -0,0 +1,54 @@ +output "resource_group_name" { + description = "The name of the resource group" + value = azurerm_resource_group.rg.name +} + +output "vnet_id" { + description = "The ID of the virtual network" + value = azurerm_virtual_network.vnet.id +} + +output "ai_foundry_id" { + description = "The ID of the AI Foundry account" + value = azapi_resource.ai_foundry.id +} + +output "ai_project_id" { + description = "The ID of the AI Foundry project" + value = azapi_resource.ai_project.id +} + +output "storage_account_id" { + description = "The ID of the storage account" + value = azurerm_storage_account.storage.id +} + +output "search_service_id" { + description = "The ID of the AI Search service" + value = azurerm_search_service.search.id +} + +output "cosmos_db_id" { + description = "The ID of the Cosmos DB account" + value = azurerm_cosmosdb_account.cosmos.id +} + +output "apim_id" { + description = "The ID of the API Management instance" + value = azurerm_api_management.apim.id +} + +output "apim_gateway_url" { + description = "The gateway URL of the API Management instance" + value = azurerm_api_management.apim.gateway_url +} + +output "apim_portal_url" { + description = "The portal URL of the API Management instance" + value = azurerm_api_management.apim.portal_url +} + +output "notes" { + description = "Important notes about this deployment" + value = "This is a complex scenario. Additional APIM API configuration, policies, and subscriptions need to be configured manually or via additional Terraform resources." +} diff --git a/infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/code/providers.tf b/infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/code/providers.tf new file mode 100644 index 000000000..9a71e9677 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/code/providers.tf @@ -0,0 +1,8 @@ +# Setup providers +provider "azapi" { +} + +provider "azurerm" { + features {} + storage_use_azuread = true +} diff --git a/infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/code/variables.tf b/infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/code/variables.tf new file mode 100644 index 000000000..d0763cb4f --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/code/variables.tf @@ -0,0 +1,75 @@ +variable "location" { + description = "The Azure region where resources will be deployed" + type = string + default = "eastus2" +} + +variable "ai_services_name_prefix" { + description = "Prefix for AI Foundry account name" + type = string + default = "foundry" +} + +variable "project_name" { + description = "The name of the project" + type = string + default = "private-apim-agent-project" +} + +variable "vnet_address_space" { + description = "Address space for the virtual network" + type = list(string) + default = ["10.0.0.0/16"] +} + +variable "subnet_private_endpoints_prefix" { + description = "Address prefix for private endpoints subnet" + type = string + default = "10.0.1.0/24" +} + +variable "subnet_apim_prefix" { + description = "Address prefix for APIM subnet" + type = string + default = "10.0.2.0/24" +} + +variable "apim_sku" { + description = "SKU for API Management (Developer, Standard, Premium)" + type = string + default = "Developer" + validation { + condition = contains(["Developer", "Standard", "Premium"], var.apim_sku) + error_message = "APIM SKU must be Developer, Standard, or Premium for VNet integration" + } +} + +variable "apim_publisher_name" { + description = "Publisher name for API Management" + type = string + default = "AI Foundry Publisher" +} + +variable "apim_publisher_email" { + description = "Publisher email for API Management" + type = string + default = "admin@example.com" +} + +variable "model_name" { + description = "The model to deploy" + type = string + default = "gpt-4.1" +} + +variable "model_version" { + description = "The version of the model" + type = string + default = "2025-04-14" +} + +variable "model_capacity" { + description = "The capacity of the model deployment" + type = number + default = 40 +} diff --git a/infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/code/versions.tf b/infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/code/versions.tf new file mode 100644 index 000000000..6eef7e581 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/16-private-network-standard-agent-apim-setup-preview/code/versions.tf @@ -0,0 +1,22 @@ +# Configure the AzApi and AzureRM providers +terraform { + 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.12" + } + } + required_version = ">= 1.10.0, < 2.0.0" +} diff --git a/infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/README.md b/infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/README.md new file mode 100644 index 000000000..5a6df7c82 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/README.md @@ -0,0 +1,58 @@ +# Private Network Standard Agent with User-Assigned Identity Setup + +This folder provides a Terraform implementation for private network standard agent setup with User-Assigned Managed Identity. + +## Overview + +This scenario combines: +- Private network configuration (VNet, private endpoints) +- Standard agent setup (BYOS) +- User-Assigned Managed Identity instead of System-Assigned +- Private DNS configuration + +## Status + +**🚧 PARTIALLY IMPLEMENTED** - This scenario requires: + +1. User-Assigned Managed Identity creation +2. Private networking infrastructure +3. Standard agent resources (Storage, Search, Cosmos DB) +4. RBAC assignments for UAI +5. Private endpoint configuration +6. Connection resources using UAI authentication + +## Reference + +For implementation guidance, see: +- Bicep: `infrastructure-setup-bicep/17-private-network-standard-user-assigned-identity-agent-setup` +- Similar Terraform: `../15a-private-network-standard-agent-setup` (uses system-assigned identity) +- UAI example: `../20-user-assigned-identity` (without private networking) + +## Key Differences from System-Assigned + +- UAI must be created before Microsoft Foundry account +- All resources must reference the UAI resource ID +- RBAC assignments use the UAI principal ID +- UAI can be shared across multiple resources + +## AzAPI Usage Rationale + +Requires AzAPI for: +- Microsoft Foundry account configuration with UAI +- Project configuration with UAI +- Connection resources + +## Prerequisites + +- Understanding of Azure Managed Identities +- Private networking knowledge +- Standard agent setup familiarity + +## Documentation + +- [Configure private link for Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-link) +- [Managed identities for Azure resources](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) +- [azurerm_user_assigned_identity - Terraform](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/user_assigned_identity) +- [AzAPI Provider](https://registry.terraform.io/providers/azure/azapi/latest/docs) + +`Tags: Private Network, User-Assigned Identity, Standard Agent` diff --git a/infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/code/example.tfvars b/infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/code/example.tfvars new file mode 100644 index 000000000..16c319a5c --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/code/example.tfvars @@ -0,0 +1,22 @@ +# Example configuration for private network standard agent with UAI + +# Azure region +location = "eastus2" + +# AI Foundry configuration +ai_services_name_prefix = "foundry" +project_name = "private-uai-agent-project" + +# User-Assigned Identity configuration +create_user_assigned_identity = true +user_assigned_identity_name = "foundry-private-uai" +# user_assigned_identity_resource_group = "my-existing-rg" # If using existing UAI + +# Network configuration +vnet_address_space = ["10.0.0.0/16"] +subnet_address_prefix = "10.0.1.0/24" + +# Model configuration +model_name = "gpt-4.1" +model_version = "2025-04-14" +model_capacity = 40 diff --git a/infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/code/main.tf b/infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/code/main.tf new file mode 100644 index 000000000..ed8ed2dd4 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/code/main.tf @@ -0,0 +1,397 @@ +########## Create private network infrastructure with UAI and standard agent +########## + +## Get subscription data +data "azurerm_client_config" "current" {} + +## Create a random string for unique naming +resource "random_string" "unique" { + length = 4 + min_numeric = 4 + numeric = true + special = false + lower = true + upper = false +} + +locals { + account_name = lower("${var.ai_services_name_prefix}${random_string.unique.result}") +} + +## Create a resource group +resource "azurerm_resource_group" "rg" { + name = "rg-aifoundry${random_string.unique.result}" + location = var.location +} + +## Reference existing User-Assigned Identity (if not creating new) +data "azurerm_user_assigned_identity" "existing" { + count = var.create_user_assigned_identity ? 0 : 1 + name = var.user_assigned_identity_name + resource_group_name = var.user_assigned_identity_resource_group +} + +## Create new User-Assigned Identity (if requested) +resource "azurerm_user_assigned_identity" "uai" { + count = var.create_user_assigned_identity ? 1 : 0 + name = var.user_assigned_identity_name + location = var.location + resource_group_name = azurerm_resource_group.rg.name +} + +locals { + uai_id = var.create_user_assigned_identity ? azurerm_user_assigned_identity.uai[0].id : data.azurerm_user_assigned_identity.existing[0].id + uai_principal_id = var.create_user_assigned_identity ? azurerm_user_assigned_identity.uai[0].principal_id : data.azurerm_user_assigned_identity.existing[0].principal_id +} + +## Create Virtual Network +resource "azurerm_virtual_network" "vnet" { + name = "vnet-aifoundry${random_string.unique.result}" + address_space = var.vnet_address_space + location = var.location + resource_group_name = azurerm_resource_group.rg.name +} + +## Create Subnet for private endpoints +resource "azurerm_subnet" "subnet" { + name = "subnet-private-endpoints" + resource_group_name = azurerm_resource_group.rg.name + virtual_network_name = azurerm_virtual_network.vnet.name + address_prefixes = [var.subnet_address_prefix] +} + +## Create Private DNS Zones +resource "azurerm_private_dns_zone" "ai_foundry" { + name = "privatelink.cognitiveservices.azure.com" + resource_group_name = azurerm_resource_group.rg.name +} + +resource "azurerm_private_dns_zone" "storage" { + name = "privatelink.blob.core.windows.net" + resource_group_name = azurerm_resource_group.rg.name +} + +resource "azurerm_private_dns_zone" "search" { + name = "privatelink.search.windows.net" + resource_group_name = azurerm_resource_group.rg.name +} + +resource "azurerm_private_dns_zone" "cosmos" { + name = "privatelink.documents.azure.com" + resource_group_name = azurerm_resource_group.rg.name +} + +## Link DNS zones to VNet +resource "azurerm_private_dns_zone_virtual_network_link" "ai_foundry" { + name = "vnet-link-ai-foundry" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.ai_foundry.name + virtual_network_id = azurerm_virtual_network.vnet.id +} + +resource "azurerm_private_dns_zone_virtual_network_link" "storage" { + name = "vnet-link-storage" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.storage.name + virtual_network_id = azurerm_virtual_network.vnet.id +} + +resource "azurerm_private_dns_zone_virtual_network_link" "search" { + name = "vnet-link-search" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.search.name + virtual_network_id = azurerm_virtual_network.vnet.id +} + +resource "azurerm_private_dns_zone_virtual_network_link" "cosmos" { + name = "vnet-link-cosmos" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.cosmos.name + virtual_network_id = azurerm_virtual_network.vnet.id +} + +## Create Storage Account with private endpoint +resource "azurerm_storage_account" "storage" { + name = "aifoundry${random_string.unique.result}stor" + resource_group_name = azurerm_resource_group.rg.name + location = var.location + account_kind = "StorageV2" + account_tier = "Standard" + account_replication_type = "ZRS" + + shared_access_key_enabled = false + min_tls_version = "TLS1_2" + allow_nested_items_to_be_public = false + public_network_access_enabled = false + + network_rules { + default_action = "Deny" + bypass = ["AzureServices"] + } +} + +resource "azurerm_private_endpoint" "storage" { + name = "pe-storage-${random_string.unique.result}" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + subnet_id = azurerm_subnet.subnet.id + + private_service_connection { + name = "psc-storage" + private_connection_resource_id = azurerm_storage_account.storage.id + is_manual_connection = false + subresource_names = ["blob"] + } + + private_dns_zone_group { + name = "storage-dns-zone-group" + private_dns_zone_ids = [azurerm_private_dns_zone.storage.id] + } +} + +## Create AI Search with private endpoint +resource "azurerm_search_service" "search" { + name = replace("aifoundry-${random_string.unique.result}-search", "_", "-") + resource_group_name = azurerm_resource_group.rg.name + location = var.location + sku = "standard" + + local_authentication_enabled = true + authentication_failure_mode = "http401WithBearerChallenge" + public_network_access_enabled = false +} + +resource "azurerm_private_endpoint" "search" { + name = "pe-search-${random_string.unique.result}" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + subnet_id = azurerm_subnet.subnet.id + + private_service_connection { + name = "psc-search" + private_connection_resource_id = azurerm_search_service.search.id + is_manual_connection = false + subresource_names = ["searchService"] + } + + private_dns_zone_group { + name = "search-dns-zone-group" + private_dns_zone_ids = [azurerm_private_dns_zone.search.id] + } +} + +## Create Cosmos DB with private endpoint +resource "azurerm_cosmosdb_account" "cosmos" { + name = "aifoundry${random_string.unique.result}cosmos" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + offer_type = "Standard" + kind = "GlobalDocumentDB" + public_network_access_enabled = false + is_virtual_network_filter_enabled = true + + consistency_policy { + consistency_level = "Session" + } + + geo_location { + location = var.location + failover_priority = 0 + } +} + +resource "azurerm_private_endpoint" "cosmos" { + name = "pe-cosmos-${random_string.unique.result}" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + subnet_id = azurerm_subnet.subnet.id + + private_service_connection { + name = "psc-cosmos" + private_connection_resource_id = azurerm_cosmosdb_account.cosmos.id + is_manual_connection = false + subresource_names = ["Sql"] + } + + private_dns_zone_group { + name = "cosmos-dns-zone-group" + private_dns_zone_ids = [azurerm_private_dns_zone.cosmos.id] + } +} + +## Create AI Foundry account with UAI and private endpoint +resource "azapi_resource" "ai_foundry" { + type = "Microsoft.CognitiveServices/accounts@2025-04-01-preview" + name = local.account_name + location = var.location + parent_id = azurerm_resource_group.rg.id + + identity { + type = "UserAssigned" + identity_ids = [local.uai_id] + } + + body = { + kind = "AIServices" + sku = { + name = "S0" + } + properties = { + allowProjectManagement = true + customSubDomainName = local.account_name + publicNetworkAccess = "Disabled" + disableLocalAuth = true + networkAcls = { + defaultAction = "Deny" + virtualNetworkRules = [] + ipRules = [] + } + } + } +} + +resource "azurerm_private_endpoint" "ai_foundry" { + name = "pe-ai-foundry-${random_string.unique.result}" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + subnet_id = azurerm_subnet.subnet.id + + private_service_connection { + name = "psc-ai-foundry" + private_connection_resource_id = azapi_resource.ai_foundry.id + is_manual_connection = false + subresource_names = ["account"] + } + + private_dns_zone_group { + name = "ai-foundry-dns-zone-group" + private_dns_zone_ids = [azurerm_private_dns_zone.ai_foundry.id] + } +} + +## Grant UAI access to Storage +resource "azurerm_role_assignment" "storage_blob_data_contributor" { + scope = azurerm_storage_account.storage.id + role_definition_name = "Storage Blob Data Contributor" + principal_id = local.uai_principal_id +} + +## Grant UAI access to AI Search +resource "azurerm_role_assignment" "search_index_data_contributor" { + scope = azurerm_search_service.search.id + role_definition_name = "Search Index Data Contributor" + principal_id = local.uai_principal_id +} + +resource "azurerm_role_assignment" "search_service_contributor" { + scope = azurerm_search_service.search.id + role_definition_name = "Search Service Contributor" + principal_id = local.uai_principal_id +} + +## Grant UAI access to Cosmos DB +resource "azurerm_cosmosdb_sql_role_assignment" "cosmos_contributor" { + resource_group_name = azurerm_resource_group.rg.name + account_name = azurerm_cosmosdb_account.cosmos.name + role_definition_id = "${azurerm_cosmosdb_account.cosmos.id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002" + principal_id = local.uai_principal_id + scope = azurerm_cosmosdb_account.cosmos.id +} + +## Wait for role assignments to propagate +resource "time_sleep" "wait_for_rbac" { + depends_on = [ + azurerm_role_assignment.storage_blob_data_contributor, + azurerm_role_assignment.search_index_data_contributor, + azurerm_role_assignment.search_service_contributor, + azurerm_cosmosdb_sql_role_assignment.cosmos_contributor, + azurerm_private_endpoint.ai_foundry, + azurerm_private_endpoint.storage, + azurerm_private_endpoint.search, + azurerm_private_endpoint.cosmos + ] + create_duration = "90s" +} + +## Create AI Foundry project with UAI +resource "azapi_resource" "ai_project" { + type = "Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview" + name = var.project_name + location = var.location + parent_id = azapi_resource.ai_foundry.id + + identity { + type = "UserAssigned" + identity_ids = [local.uai_id] + } + + body = { + properties = {} + } + + depends_on = [time_sleep.wait_for_rbac] +} + +## Create connections +resource "azapi_resource" "storage_connection" { + type = "Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview" + name = "storage-connection" + parent_id = azapi_resource.ai_foundry.id + + body = { + properties = { + category = "AzureBlob" + target = azurerm_storage_account.storage.primary_blob_endpoint + authType = "AccessKey" + isSharedToAll = true + metadata = { + ResourceId = azurerm_storage_account.storage.id + } + } + } + + depends_on = [azapi_resource.ai_project] +} + +resource "azapi_resource" "search_connection" { + type = "Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview" + name = "search-connection" + parent_id = azapi_resource.ai_foundry.id + + body = { + properties = { + category = "CognitiveSearch" + target = "https://${azurerm_search_service.search.name}.search.windows.net" + authType = "AAD" + isSharedToAll = true + metadata = { + ResourceId = azurerm_search_service.search.id + } + } + } + + depends_on = [azapi_resource.ai_project] +} + +## Deploy model +resource "azapi_resource" "model_deployment" { + type = "Microsoft.CognitiveServices/accounts/deployments@2025-04-01-preview" + name = var.model_name + parent_id = azapi_resource.ai_foundry.id + + body = { + sku = { + capacity = var.model_capacity + name = "GlobalStandard" + } + properties = { + model = { + name = var.model_name + format = "OpenAI" + version = var.model_version + } + } + } + + depends_on = [azapi_resource.ai_project] +} diff --git a/infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/code/outputs.tf b/infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/code/outputs.tf new file mode 100644 index 000000000..893e337dd --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/code/outputs.tf @@ -0,0 +1,49 @@ +output "resource_group_name" { + description = "The name of the resource group" + value = azurerm_resource_group.rg.name +} + +output "user_assigned_identity_id" { + description = "The ID of the User-Assigned Identity" + value = local.uai_id +} + +output "vnet_id" { + description = "The ID of the virtual network" + value = azurerm_virtual_network.vnet.id +} + +output "ai_foundry_id" { + description = "The ID of the AI Foundry account" + value = azapi_resource.ai_foundry.id +} + +output "ai_project_id" { + description = "The ID of the AI Foundry project" + value = azapi_resource.ai_project.id +} + +output "storage_account_id" { + description = "The ID of the storage account" + value = azurerm_storage_account.storage.id +} + +output "search_service_id" { + description = "The ID of the AI Search service" + value = azurerm_search_service.search.id +} + +output "cosmos_db_id" { + description = "The ID of the Cosmos DB account" + value = azurerm_cosmosdb_account.cosmos.id +} + +output "private_endpoint_ips" { + description = "Private endpoint IP addresses" + value = { + ai_foundry = azurerm_private_endpoint.ai_foundry.private_service_connection[0].private_ip_address + storage = azurerm_private_endpoint.storage.private_service_connection[0].private_ip_address + search = azurerm_private_endpoint.search.private_service_connection[0].private_ip_address + cosmos = azurerm_private_endpoint.cosmos.private_service_connection[0].private_ip_address + } +} diff --git a/infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/code/providers.tf b/infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/code/providers.tf new file mode 100644 index 000000000..9a71e9677 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/code/providers.tf @@ -0,0 +1,8 @@ +# Setup providers +provider "azapi" { +} + +provider "azurerm" { + features {} + storage_use_azuread = true +} diff --git a/infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/code/variables.tf b/infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/code/variables.tf new file mode 100644 index 000000000..fa2de1118 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/code/variables.tf @@ -0,0 +1,65 @@ +variable "location" { + description = "The Azure region where resources will be deployed" + type = string + default = "eastus2" +} + +variable "ai_services_name_prefix" { + description = "Prefix for AI Foundry account name" + type = string + default = "foundry" +} + +variable "project_name" { + description = "The name of the project" + type = string + default = "private-uai-agent-project" +} + +variable "create_user_assigned_identity" { + description = "Whether to create a new User-Assigned Identity" + type = bool + default = true +} + +variable "user_assigned_identity_name" { + description = "Name of the User-Assigned Identity" + type = string + default = "foundry-private-uai" +} + +variable "user_assigned_identity_resource_group" { + description = "Resource group of existing UAI (if not creating new)" + type = string + default = "" +} + +variable "vnet_address_space" { + description = "Address space for the virtual network" + type = list(string) + default = ["10.0.0.0/16"] +} + +variable "subnet_address_prefix" { + description = "Address prefix for the subnet" + type = string + default = "10.0.1.0/24" +} + +variable "model_name" { + description = "The model to deploy" + type = string + default = "gpt-4.1" +} + +variable "model_version" { + description = "The version of the model" + type = string + default = "2025-04-14" +} + +variable "model_capacity" { + description = "The capacity of the model deployment" + type = number + default = 40 +} diff --git a/infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/code/versions.tf b/infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/code/versions.tf new file mode 100644 index 000000000..6eef7e581 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/17-private-network-standard-user-assigned-identity-agent-setup/code/versions.tf @@ -0,0 +1,22 @@ +# Configure the AzApi and AzureRM providers +terraform { + 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.12" + } + } + required_version = ">= 1.10.0, < 2.0.0" +} diff --git a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/ai-foundry.tf b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/ai-foundry.tf index 89cf99c7e..a7704b9bc 100644 --- a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/ai-foundry.tf +++ b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/ai-foundry.tf @@ -19,13 +19,13 @@ resource "azapi_resource" "cognitive_account" { properties = merge( { allowProjectManagement = true - apiProperties = {} - customSubDomainName = local.foundry_name - disableLocalAuth = true + apiProperties = {} + customSubDomainName = local.foundry_name + disableLocalAuth = true networkAcls = { - defaultAction = "Deny" - virtualNetworkRules = [] - ipRules = [] + defaultAction = "Deny" + virtualNetworkRules = [] + ipRules = [] } networkInjections = [ { @@ -411,7 +411,7 @@ resource "azapi_resource" "conn_storage" { locals { # Extract the workspace ID from the project output project_workspace_id_raw = try(jsondecode(azapi_resource.ai_foundry_project.output).properties.internalId, "") - + # Format as GUID if we have a valid 32-character string project_workspace_id_guid = length(local.project_workspace_id_raw) == 32 ? format( "%s-%s-%s-%s-%s", @@ -431,7 +431,7 @@ resource "azurerm_role_assignment" "project_storage_blob_owner_containers" { scope = azurerm_storage_account.main[0].id role_definition_name = "Storage Blob Data Owner" principal_id = azapi_resource.ai_foundry_project.identity[0].principal_id - + # ABAC condition matching Bicep template condition = "((!(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_workspace_id_guid}' AND @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringLikeIgnoreCase '*-azureml-agent'))" condition_version = "2.0" @@ -447,10 +447,10 @@ resource "azurerm_cosmosdb_sql_role_assignment" "project_cosmos_builtin_contribu count = var.enable_cosmos ? 1 : 0 resource_group_name = azurerm_resource_group.main.name account_name = azurerm_cosmosdb_account.main[0].name - + # Cosmos DB Built-in Data Contributor role role_definition_id = "${azurerm_cosmosdb_account.main[0].id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002" - + principal_id = azapi_resource.ai_foundry_project.identity[0].principal_id scope = azurerm_cosmosdb_account.main[0].id diff --git a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/aisearch.tf b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/aisearch.tf index ebc78870a..1380f4134 100644 --- a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/aisearch.tf +++ b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/aisearch.tf @@ -9,7 +9,7 @@ resource "azurerm_search_service" "main" { partition_count = 1 public_network_access_enabled = true - + identity { type = "SystemAssigned" } diff --git a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/cosmos.tf b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/cosmos.tf index 2f00bb0c0..fd6c8d210 100644 --- a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/cosmos.tf +++ b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/cosmos.tf @@ -18,9 +18,9 @@ resource "azurerm_cosmosdb_account" "main" { failover_priority = 0 } - public_network_access_enabled = false + public_network_access_enabled = false network_acl_bypass_for_azure_services = false - local_authentication_disabled = true + local_authentication_disabled = true tags = merge( var.tags, diff --git a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/network.tf b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/network.tf index 45f9c492f..1d9ca355b 100644 --- a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/network.tf +++ b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/network.tf @@ -9,30 +9,30 @@ resource "azurerm_virtual_network" "main" { # Subnets resource "azurerm_subnet" "private_endpoints" { - count = var.enable_networking ? 1 : 0 - name = var.private_endpoints_subnet_name - resource_group_name = azurerm_resource_group.main.name - virtual_network_name = azurerm_virtual_network.main[0].name - address_prefixes = [var.private_endpoints_subnet_prefix] - default_outbound_access_enabled = true + count = var.enable_networking ? 1 : 0 + name = var.private_endpoints_subnet_name + resource_group_name = azurerm_resource_group.main.name + virtual_network_name = azurerm_virtual_network.main[0].name + address_prefixes = [var.private_endpoints_subnet_prefix] + default_outbound_access_enabled = true } resource "azurerm_subnet" "vms" { - count = var.enable_vm ? 1 : 0 - name = var.vm_subnet_name - resource_group_name = azurerm_resource_group.main.name - virtual_network_name = azurerm_virtual_network.main[0].name - address_prefixes = [var.vm_subnet_prefix] - default_outbound_access_enabled = true + count = var.enable_vm ? 1 : 0 + name = var.vm_subnet_name + resource_group_name = azurerm_resource_group.main.name + virtual_network_name = azurerm_virtual_network.main[0].name + address_prefixes = [var.vm_subnet_prefix] + default_outbound_access_enabled = true } resource "azurerm_subnet" "bastion" { - count = var.enable_vm ? 1 : 0 - name = "AzureBastionSubnet" - resource_group_name = azurerm_resource_group.main.name - virtual_network_name = azurerm_virtual_network.main[0].name - address_prefixes = [var.bastion_subnet_prefix] - default_outbound_access_enabled = true + count = var.enable_vm ? 1 : 0 + name = "AzureBastionSubnet" + resource_group_name = azurerm_resource_group.main.name + virtual_network_name = azurerm_virtual_network.main[0].name + address_prefixes = [var.bastion_subnet_prefix] + default_outbound_access_enabled = true } # Bastion Public IP diff --git a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/outputs.tf b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/outputs.tf index 118b8bb3a..ee30382cd 100644 --- a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/outputs.tf +++ b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/outputs.tf @@ -138,18 +138,18 @@ output "ai_foundry_custom_subdomain" { output "private_dns_zone_ids" { description = "Map of private DNS zone IDs" value = var.enable_dns ? { - cognitive_services = azurerm_private_dns_zone.cognitive_services[0].id - storage_blob = azurerm_private_dns_zone.storage_blob[0].id - storage_file = azurerm_private_dns_zone.storage_file[0].id - storage_table = azurerm_private_dns_zone.storage_table[0].id - storage_queue = azurerm_private_dns_zone.storage_queue[0].id - key_vault = azurerm_private_dns_zone.key_vault[0].id - container_registry = azurerm_private_dns_zone.container_registry[0].id - openai = azurerm_private_dns_zone.openai[0].id - aifoundry_api = azurerm_private_dns_zone.aifoundry_api[0].id - aifoundry_notebooks = azurerm_private_dns_zone.aifoundry_notebooks[0].id - aifoundry_services = azurerm_private_dns_zone.aifoundry_services[0].id - cosmos = azurerm_private_dns_zone.cosmos[0].id - aisearch = azurerm_private_dns_zone.aisearch[0].id + cognitive_services = azurerm_private_dns_zone.cognitive_services[0].id + storage_blob = azurerm_private_dns_zone.storage_blob[0].id + storage_file = azurerm_private_dns_zone.storage_file[0].id + storage_table = azurerm_private_dns_zone.storage_table[0].id + storage_queue = azurerm_private_dns_zone.storage_queue[0].id + key_vault = azurerm_private_dns_zone.key_vault[0].id + container_registry = azurerm_private_dns_zone.container_registry[0].id + openai = azurerm_private_dns_zone.openai[0].id + aifoundry_api = azurerm_private_dns_zone.aifoundry_api[0].id + aifoundry_notebooks = azurerm_private_dns_zone.aifoundry_notebooks[0].id + aifoundry_services = azurerm_private_dns_zone.aifoundry_services[0].id + cosmos = azurerm_private_dns_zone.cosmos[0].id + aisearch = azurerm_private_dns_zone.aisearch[0].id } : {} } diff --git a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/providers.tf b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/providers.tf index c8e020830..7bbdebe8b 100644 --- a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/providers.tf +++ b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/providers.tf @@ -1,6 +1,6 @@ terraform { required_version = ">= 1.0" - + required_providers { azurerm = { source = "hashicorp/azurerm" @@ -18,9 +18,9 @@ terraform { } provider "azurerm" { - subscription_id = var.subscription_id + subscription_id = var.subscription_id storage_use_azuread = true - + features { resource_group { prevent_deletion_if_contains_resources = false diff --git a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/storage.tf b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/storage.tf index 8e70eae32..6e1be16a2 100644 --- a/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/storage.tf +++ b/infrastructure/infrastructure-setup-terraform/18-managed-virtual-network-preview/storage.tf @@ -1,13 +1,13 @@ # Storage Account resource "azurerm_storage_account" "main" { - count = var.enable_storage ? 1 : 0 - name = local.storage_name - resource_group_name = azurerm_resource_group.main.name - location = azurerm_resource_group.main.location - account_tier = "Standard" - account_replication_type = "LRS" - account_kind = "StorageV2" - shared_access_key_enabled = false + count = var.enable_storage ? 1 : 0 + name = local.storage_name + resource_group_name = azurerm_resource_group.main.name + location = azurerm_resource_group.main.location + account_tier = "Standard" + account_replication_type = "LRS" + account_kind = "StorageV2" + shared_access_key_enabled = false allow_nested_items_to_be_public = false # Disable public network access - only accessible via private endpoints diff --git a/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/README.md b/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/README.md new file mode 100644 index 000000000..f2bea41d3 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/README.md @@ -0,0 +1,55 @@ +# Hybrid Private Resources Agent Setup + +This folder provides a Terraform implementation for a hybrid setup with a mix of private and public resources. + +## Overview + +This advanced scenario demonstrates: +- Microsoft Foundry with selective private/public access +- Some resources with private endpoints, others public +- Agent setup with hybrid networking +- Complex routing and DNS configuration + +## Status + +**🚧 PARTIALLY IMPLEMENTED** - This complex scenario requires: + +1. Hybrid networking design +2. Selective private endpoint deployment +3. DNS configuration for mixed access +4. Network security group rules +5. Firewall and routing configuration + +## Use Cases + +- Migration from public to private (gradual transition) +- Development environment (public) vs Production (private) +- Selective resource isolation +- Cost optimization (private only where needed) + +## Reference + +For implementation guidance, see the Bicep reference: +- `infrastructure-setup-bicep/19-hybrid-private-resources-agent-setup` + +## Prerequisites + +- Advanced Azure networking knowledge +- Understanding of hybrid networking patterns +- Network security expertise + +## Contributing + +This is a complex scenario. When implementing: +1. Document the reasoning for public vs private choices +2. Include network diagrams +3. Explain security implications +4. Provide migration guidance + +## Documentation + +- [Configure private link for Microsoft Foundry](https://learn.microsoft.com/en-us/azure/ai-foundry/how-to/configure-private-link) +- [azurerm_private_endpoint - Terraform](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/private_endpoint) +- [AzAPI Provider](https://registry.terraform.io/providers/azure/azapi/latest/docs) + +`Tags: Hybrid Networking, Private Endpoints, Advanced` diff --git a/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/example.tfvars b/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/example.tfvars new file mode 100644 index 000000000..5798fd48a --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/example.tfvars @@ -0,0 +1,30 @@ +# Example configuration for hybrid private resources agent setup + +# Azure region +location = "eastus2" + +# AI Foundry configuration +ai_services_name_prefix = "foundry" +project_name = "hybrid-agent-project" + +# Network configuration +vnet_address_space = ["10.0.0.0/16"] +subnet_address_prefix = "10.0.1.0/24" + +# Hybrid configuration - mix of public and private +# Scenario: Development setup with some public access for easier testing +ai_foundry_public_access = "Enabled" # Public for easy access +storage_public_access = false # Private for data security +search_public_access = true # Public for development +cosmos_public_access = true # Public for development + +# For production, you might flip these: +# ai_foundry_public_access = "Disabled" +# storage_public_access = false +# search_public_access = false +# cosmos_public_access = false + +# Model configuration +model_name = "gpt-4.1" +model_version = "2025-04-14" +model_capacity = 40 diff --git a/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/main.tf b/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/main.tf new file mode 100644 index 000000000..85bf290ee --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/main.tf @@ -0,0 +1,387 @@ +########## Create hybrid infrastructure (mix of private and public resources) +########## + +## Get subscription data +data "azurerm_client_config" "current" {} + +## Create a random string for unique naming +resource "random_string" "unique" { + length = 4 + min_numeric = 4 + numeric = true + special = false + lower = true + upper = false +} + +locals { + account_name = lower("${var.ai_services_name_prefix}${random_string.unique.result}") +} + +## Create a resource group +resource "azurerm_resource_group" "rg" { + name = "rg-aifoundry${random_string.unique.result}" + location = var.location +} + +## Create Virtual Network (always needed for private endpoints) +resource "azurerm_virtual_network" "vnet" { + name = "vnet-aifoundry${random_string.unique.result}" + address_space = var.vnet_address_space + location = var.location + resource_group_name = azurerm_resource_group.rg.name +} + +## Create Subnet for private endpoints +resource "azurerm_subnet" "subnet" { + name = "subnet-private-endpoints" + resource_group_name = azurerm_resource_group.rg.name + virtual_network_name = azurerm_virtual_network.vnet.name + address_prefixes = [var.subnet_address_prefix] +} + +## Create Private DNS Zones (for private resources) +resource "azurerm_private_dns_zone" "ai_foundry" { + count = var.ai_foundry_public_access == "Disabled" ? 1 : 0 + name = "privatelink.cognitiveservices.azure.com" + resource_group_name = azurerm_resource_group.rg.name +} + +resource "azurerm_private_dns_zone" "storage" { + count = var.storage_public_access ? 0 : 1 + name = "privatelink.blob.core.windows.net" + resource_group_name = azurerm_resource_group.rg.name +} + +resource "azurerm_private_dns_zone" "search" { + count = var.search_public_access ? 0 : 1 + name = "privatelink.search.windows.net" + resource_group_name = azurerm_resource_group.rg.name +} + +resource "azurerm_private_dns_zone" "cosmos" { + count = var.cosmos_public_access ? 0 : 1 + name = "privatelink.documents.azure.com" + resource_group_name = azurerm_resource_group.rg.name +} + +## Link DNS zones to VNet +resource "azurerm_private_dns_zone_virtual_network_link" "ai_foundry" { + count = var.ai_foundry_public_access == "Disabled" ? 1 : 0 + name = "vnet-link-ai-foundry" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.ai_foundry[0].name + virtual_network_id = azurerm_virtual_network.vnet.id +} + +resource "azurerm_private_dns_zone_virtual_network_link" "storage" { + count = var.storage_public_access ? 0 : 1 + name = "vnet-link-storage" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.storage[0].name + virtual_network_id = azurerm_virtual_network.vnet.id +} + +resource "azurerm_private_dns_zone_virtual_network_link" "search" { + count = var.search_public_access ? 0 : 1 + name = "vnet-link-search" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.search[0].name + virtual_network_id = azurerm_virtual_network.vnet.id +} + +resource "azurerm_private_dns_zone_virtual_network_link" "cosmos" { + count = var.cosmos_public_access ? 0 : 1 + name = "vnet-link-cosmos" + resource_group_name = azurerm_resource_group.rg.name + private_dns_zone_name = azurerm_private_dns_zone.cosmos[0].name + virtual_network_id = azurerm_virtual_network.vnet.id +} + +## Create Storage Account (public or private based on variable) +resource "azurerm_storage_account" "storage" { + name = "aifoundry${random_string.unique.result}stor" + resource_group_name = azurerm_resource_group.rg.name + location = var.location + account_kind = "StorageV2" + account_tier = "Standard" + account_replication_type = "ZRS" + + shared_access_key_enabled = false + min_tls_version = "TLS1_2" + allow_nested_items_to_be_public = false + public_network_access_enabled = var.storage_public_access + + network_rules { + default_action = var.storage_public_access ? "Allow" : "Deny" + bypass = ["AzureServices"] + } +} + +## Create private endpoint for Storage (if private) +resource "azurerm_private_endpoint" "storage" { + count = var.storage_public_access ? 0 : 1 + name = "pe-storage-${random_string.unique.result}" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + subnet_id = azurerm_subnet.subnet.id + + private_service_connection { + name = "psc-storage" + private_connection_resource_id = azurerm_storage_account.storage.id + is_manual_connection = false + subresource_names = ["blob"] + } + + private_dns_zone_group { + name = "storage-dns-zone-group" + private_dns_zone_ids = [azurerm_private_dns_zone.storage[0].id] + } +} + +## Create AI Search (public or private based on variable) +resource "azurerm_search_service" "search" { + name = replace("aifoundry-${random_string.unique.result}-search", "_", "-") + resource_group_name = azurerm_resource_group.rg.name + location = var.location + sku = "standard" + + local_authentication_enabled = true + authentication_failure_mode = "http401WithBearerChallenge" + public_network_access_enabled = var.search_public_access +} + +## Create private endpoint for Search (if private) +resource "azurerm_private_endpoint" "search" { + count = var.search_public_access ? 0 : 1 + name = "pe-search-${random_string.unique.result}" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + subnet_id = azurerm_subnet.subnet.id + + private_service_connection { + name = "psc-search" + private_connection_resource_id = azurerm_search_service.search.id + is_manual_connection = false + subresource_names = ["searchService"] + } + + private_dns_zone_group { + name = "search-dns-zone-group" + private_dns_zone_ids = [azurerm_private_dns_zone.search[0].id] + } +} + +## Create Cosmos DB (public or private based on variable) +resource "azurerm_cosmosdb_account" "cosmos" { + name = "aifoundry${random_string.unique.result}cosmos" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + offer_type = "Standard" + kind = "GlobalDocumentDB" + public_network_access_enabled = var.cosmos_public_access + is_virtual_network_filter_enabled = !var.cosmos_public_access + + consistency_policy { + consistency_level = "Session" + } + + geo_location { + location = var.location + failover_priority = 0 + } +} + +## Create private endpoint for Cosmos DB (if private) +resource "azurerm_private_endpoint" "cosmos" { + count = var.cosmos_public_access ? 0 : 1 + name = "pe-cosmos-${random_string.unique.result}" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + subnet_id = azurerm_subnet.subnet.id + + private_service_connection { + name = "psc-cosmos" + private_connection_resource_id = azurerm_cosmosdb_account.cosmos.id + is_manual_connection = false + subresource_names = ["Sql"] + } + + private_dns_zone_group { + name = "cosmos-dns-zone-group" + private_dns_zone_ids = [azurerm_private_dns_zone.cosmos[0].id] + } +} + +## Create AI Foundry account (public or private based on variable) +resource "azapi_resource" "ai_foundry" { + type = "Microsoft.CognitiveServices/accounts@2025-04-01-preview" + name = local.account_name + location = var.location + parent_id = azurerm_resource_group.rg.id + + identity { + type = "SystemAssigned" + } + + body = { + kind = "AIServices" + sku = { + name = "S0" + } + properties = { + allowProjectManagement = true + customSubDomainName = local.account_name + publicNetworkAccess = var.ai_foundry_public_access + disableLocalAuth = true + networkAcls = { + defaultAction = var.ai_foundry_public_access == "Enabled" ? "Allow" : "Deny" + virtualNetworkRules = [] + ipRules = [] + } + } + } +} + +## Create private endpoint for AI Foundry (if private) +resource "azurerm_private_endpoint" "ai_foundry" { + count = var.ai_foundry_public_access == "Disabled" ? 1 : 0 + name = "pe-ai-foundry-${random_string.unique.result}" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + subnet_id = azurerm_subnet.subnet.id + + private_service_connection { + name = "psc-ai-foundry" + private_connection_resource_id = azapi_resource.ai_foundry.id + is_manual_connection = false + subresource_names = ["account"] + } + + private_dns_zone_group { + name = "ai-foundry-dns-zone-group" + private_dns_zone_ids = [azurerm_private_dns_zone.ai_foundry[0].id] + } +} + +## Grant AI Foundry access to Storage +resource "azurerm_role_assignment" "storage_blob_data_contributor" { + scope = azurerm_storage_account.storage.id + role_definition_name = "Storage Blob Data Contributor" + principal_id = azapi_resource.ai_foundry.identity[0].principal_id +} + +## Grant AI Foundry access to AI Search +resource "azurerm_role_assignment" "search_index_data_contributor" { + scope = azurerm_search_service.search.id + role_definition_name = "Search Index Data Contributor" + principal_id = azapi_resource.ai_foundry.identity[0].principal_id +} + +resource "azurerm_role_assignment" "search_service_contributor" { + scope = azurerm_search_service.search.id + role_definition_name = "Search Service Contributor" + principal_id = azapi_resource.ai_foundry.identity[0].principal_id +} + +## Grant AI Foundry access to Cosmos DB +resource "azurerm_cosmosdb_sql_role_assignment" "cosmos_contributor" { + resource_group_name = azurerm_resource_group.rg.name + account_name = azurerm_cosmosdb_account.cosmos.name + role_definition_id = "${azurerm_cosmosdb_account.cosmos.id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002" + principal_id = azapi_resource.ai_foundry.identity[0].principal_id + scope = azurerm_cosmosdb_account.cosmos.id +} + +## Wait for role assignments and private endpoints +resource "time_sleep" "wait_for_resources" { + depends_on = [ + azurerm_role_assignment.storage_blob_data_contributor, + azurerm_role_assignment.search_index_data_contributor, + azurerm_role_assignment.search_service_contributor, + azurerm_cosmosdb_sql_role_assignment.cosmos_contributor + ] + create_duration = "60s" +} + +## Create AI Foundry project +resource "azapi_resource" "ai_project" { + type = "Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview" + name = var.project_name + location = var.location + parent_id = azapi_resource.ai_foundry.id + + identity { + type = "SystemAssigned" + } + + body = { + properties = {} + } + + depends_on = [time_sleep.wait_for_resources] +} + +## Create connections +resource "azapi_resource" "storage_connection" { + type = "Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview" + name = "storage-connection" + parent_id = azapi_resource.ai_foundry.id + + body = { + properties = { + category = "AzureBlob" + target = azurerm_storage_account.storage.primary_blob_endpoint + authType = "AccessKey" + isSharedToAll = true + metadata = { + ResourceId = azurerm_storage_account.storage.id + } + } + } + + depends_on = [azapi_resource.ai_project] +} + +resource "azapi_resource" "search_connection" { + type = "Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview" + name = "search-connection" + parent_id = azapi_resource.ai_foundry.id + + body = { + properties = { + category = "CognitiveSearch" + target = "https://${azurerm_search_service.search.name}.search.windows.net" + authType = "AAD" + isSharedToAll = true + metadata = { + ResourceId = azurerm_search_service.search.id + } + } + } + + depends_on = [azapi_resource.ai_project] +} + +## Deploy model +resource "azapi_resource" "model_deployment" { + type = "Microsoft.CognitiveServices/accounts/deployments@2025-04-01-preview" + name = var.model_name + parent_id = azapi_resource.ai_foundry.id + + body = { + sku = { + capacity = var.model_capacity + name = "GlobalStandard" + } + properties = { + model = { + name = var.model_name + format = "OpenAI" + version = var.model_version + } + } + } + + depends_on = [azapi_resource.ai_project] +} diff --git a/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/outputs.tf b/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/outputs.tf new file mode 100644 index 000000000..180abffc0 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/outputs.tf @@ -0,0 +1,44 @@ +output "resource_group_name" { + description = "The name of the resource group" + value = azurerm_resource_group.rg.name +} + +output "vnet_id" { + description = "The ID of the virtual network" + value = azurerm_virtual_network.vnet.id +} + +output "ai_foundry_id" { + description = "The ID of the AI Foundry account" + value = azapi_resource.ai_foundry.id +} + +output "ai_project_id" { + description = "The ID of the AI Foundry project" + value = azapi_resource.ai_project.id +} + +output "storage_account_id" { + description = "The ID of the storage account" + value = azurerm_storage_account.storage.id +} + +output "search_service_id" { + description = "The ID of the AI Search service" + value = azurerm_search_service.search.id +} + +output "cosmos_db_id" { + description = "The ID of the Cosmos DB account" + value = azurerm_cosmosdb_account.cosmos.id +} + +output "deployment_summary" { + description = "Summary of public vs private resources" + value = { + ai_foundry_access = var.ai_foundry_public_access + storage_access = var.storage_public_access ? "Public" : "Private" + search_access = var.search_public_access ? "Public" : "Private" + cosmos_access = var.cosmos_public_access ? "Public" : "Private" + } +} diff --git a/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/providers.tf b/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/providers.tf new file mode 100644 index 000000000..9a71e9677 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/providers.tf @@ -0,0 +1,8 @@ +# Setup providers +provider "azapi" { +} + +provider "azurerm" { + features {} + storage_use_azuread = true +} diff --git a/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/variables.tf b/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/variables.tf new file mode 100644 index 000000000..e76b4966f --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/variables.tf @@ -0,0 +1,75 @@ +variable "location" { + description = "The Azure region where resources will be deployed" + type = string + default = "eastus2" +} + +variable "ai_services_name_prefix" { + description = "Prefix for AI Foundry account name" + type = string + default = "foundry" +} + +variable "project_name" { + description = "The name of the project" + type = string + default = "hybrid-agent-project" +} + +variable "vnet_address_space" { + description = "Address space for the virtual network" + type = list(string) + default = ["10.0.0.0/16"] +} + +variable "subnet_address_prefix" { + description = "Address prefix for the subnet" + type = string + default = "10.0.1.0/24" +} + +variable "ai_foundry_public_access" { + description = "Whether AI Foundry should have public access (Enabled/Disabled)" + type = string + default = "Enabled" + validation { + condition = contains(["Enabled", "Disabled"], var.ai_foundry_public_access) + error_message = "Must be Enabled or Disabled" + } +} + +variable "storage_public_access" { + description = "Whether Storage should have public access (true/false)" + type = bool + default = false +} + +variable "search_public_access" { + description = "Whether AI Search should have public access (true/false)" + type = bool + default = true +} + +variable "cosmos_public_access" { + description = "Whether Cosmos DB should have public access (true/false)" + type = bool + default = true +} + +variable "model_name" { + description = "The model to deploy" + type = string + default = "gpt-4.1" +} + +variable "model_version" { + description = "The version of the model" + type = string + default = "2025-04-14" +} + +variable "model_capacity" { + description = "The capacity of the model deployment" + type = number + default = 40 +} diff --git a/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/versions.tf b/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/versions.tf new file mode 100644 index 000000000..6eef7e581 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/19-hybrid-private-resources-agent-setup/code/versions.tf @@ -0,0 +1,22 @@ +# Configure the AzApi and AzureRM providers +terraform { + 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.12" + } + } + required_version = ">= 1.10.0, < 2.0.0" +} diff --git a/infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/README.md b/infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/README.md new file mode 100644 index 000000000..bbd5097ed --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/README.md @@ -0,0 +1,57 @@ +# Set up Microsoft Foundry with User-Assigned Managed Identity + +This Terraform template deploys an Microsoft Foundry account and project configured with a User-Assigned Managed Identity instead of the default System-Assigned identity. + +## Description + +- Creates an Microsoft Foundry account with User-Assigned Managed Identity +- Creates a project with User-Assigned Managed Identity +- Deploys a GPT-4o model + +## Prerequisites + +- Azure CLI or Terraform installed +- An existing User-Assigned Managed Identity (or this template can create one) +- Appropriate Azure permissions + +## Limitations + +- When creating a project, managed identity type cannot be updated later +- User-Assigned Managed Identity is not supported with Customer Managed Keys + +## Deployment + +1. Navigate to the code directory: +```bash +cd code +``` + +2. Initialize Terraform: +```bash +terraform init +``` + +3. Configure variables (either in terraform.tfvars or via command line): + - Provide existing UAI name and resource group, OR + - Set `create_user_assigned_identity = true` to create a new one + +4. Deploy: +```bash +terraform plan +terraform apply +``` + +## Resources Created + +- User-Assigned Managed Identity (optional, if not provided) +- Microsoft Foundry account with UAI +- Microsoft Foundry project with UAI +- Model deployment (GPT-4o) + +## Documentation + +- [Managed identities for Azure resources](https://learn.microsoft.com/en-us/entra/identity/managed-identities-azure-resources/overview) +- [azurerm_user_assigned_identity - Terraform](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/user_assigned_identity) +- [AzAPI Provider](https://registry.terraform.io/providers/azure/azapi/latest/docs) + +`Tags: Microsoft.CognitiveServices/accounts/projects, Microsoft.ManagedIdentity/userAssignedIdentities` diff --git a/infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/code/example.tfvars b/infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/code/example.tfvars new file mode 100644 index 000000000..f2426d8fa --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/code/example.tfvars @@ -0,0 +1,11 @@ +# Example terraform.tfvars file +# Copy this to terraform.tfvars and customize for your deployment + +location = "eastus2" +ai_foundry_name = "foundry-uai" +ai_project_name = "foundry-uai-proj" +create_user_assigned_identity = true +user_assigned_identity_name = "foundry-uai" +model_name = "gpt-4o" +model_version = "2024-08-06" +model_capacity = 1 diff --git a/infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/code/main.tf b/infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/code/main.tf new file mode 100644 index 000000000..af0082018 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/code/main.tf @@ -0,0 +1,104 @@ +########## Create infrastructure resources +########## + +## Get subscription data +data "azurerm_client_config" "current" {} + +## Create a random string for unique naming +resource "random_string" "unique" { + length = 4 + min_numeric = 4 + numeric = true + special = false + lower = true + upper = false +} + +## Create a resource group +resource "azurerm_resource_group" "rg" { + name = "rg-aifoundry${random_string.unique.result}" + location = var.location +} + +## Reference existing User-Assigned Identity (if not creating new) +data "azurerm_user_assigned_identity" "existing" { + count = var.create_user_assigned_identity ? 0 : 1 + name = var.user_assigned_identity_name + resource_group_name = var.user_assigned_identity_resource_group +} + +## Create new User-Assigned Identity (if requested) +resource "azurerm_user_assigned_identity" "uai" { + count = var.create_user_assigned_identity ? 1 : 0 + name = var.user_assigned_identity_name + location = var.location + resource_group_name = azurerm_resource_group.rg.name +} + +locals { + uai_id = var.create_user_assigned_identity ? azurerm_user_assigned_identity.uai[0].id : data.azurerm_user_assigned_identity.existing[0].id +} + +## Create AI Foundry account with User-Assigned Identity +resource "azapi_resource" "ai_foundry" { + type = "Microsoft.CognitiveServices/accounts@2025-06-01" + name = var.ai_foundry_name + location = var.location + parent_id = azurerm_resource_group.rg.id + + identity { + type = "UserAssigned" + identity_ids = [local.uai_id] + } + + body = { + kind = "AIServices" + sku = { + name = "S0" + } + properties = { + allowProjectManagement = true + customSubDomainName = var.ai_foundry_name + disableLocalAuth = false + publicNetworkAccess = "Enabled" + } + } +} + +## Create AI Foundry project with User-Assigned Identity +resource "azapi_resource" "ai_project" { + type = "Microsoft.CognitiveServices/accounts/projects@2025-06-01" + name = coalesce(var.ai_project_name, "${var.ai_foundry_name}-proj") + location = var.location + parent_id = azapi_resource.ai_foundry.id + + identity { + type = "UserAssigned" + identity_ids = [local.uai_id] + } + + body = { + properties = {} + } +} + +## Deploy model +resource "azapi_resource" "model_deployment" { + type = "Microsoft.CognitiveServices/accounts/deployments@2025-06-01" + name = var.model_name + parent_id = azapi_resource.ai_foundry.id + + body = { + sku = { + capacity = var.model_capacity + name = "GlobalStandard" + } + properties = { + model = { + name = var.model_name + format = "OpenAI" + version = var.model_version + } + } + } +} diff --git a/infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/code/outputs.tf b/infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/code/outputs.tf new file mode 100644 index 000000000..e5a17bca7 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/code/outputs.tf @@ -0,0 +1,19 @@ +output "resource_group_name" { + description = "The name of the resource group" + value = azurerm_resource_group.rg.name +} + +output "user_assigned_identity_id" { + description = "The ID of the User-Assigned Identity" + value = local.uai_id +} + +output "ai_foundry_id" { + description = "The ID of the AI Foundry account" + value = azapi_resource.ai_foundry.id +} + +output "ai_project_id" { + description = "The ID of the AI Foundry project" + value = azapi_resource.ai_project.id +} diff --git a/infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/code/providers.tf b/infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/code/providers.tf new file mode 100644 index 000000000..9a71e9677 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/code/providers.tf @@ -0,0 +1,8 @@ +# Setup providers +provider "azapi" { +} + +provider "azurerm" { + features {} + storage_use_azuread = true +} diff --git a/infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/code/variables.tf b/infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/code/variables.tf new file mode 100644 index 000000000..a5d51aed0 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/code/variables.tf @@ -0,0 +1,53 @@ +variable "location" { + description = "The Azure region where resources will be deployed" + type = string + default = "eastus2" +} + +variable "ai_foundry_name" { + description = "The name of the AI Foundry account" + type = string + default = "foundry-uai" +} + +variable "ai_project_name" { + description = "The name of the AI Foundry project" + type = string + default = null # Will default to {ai_foundry_name}-proj +} + +variable "create_user_assigned_identity" { + description = "Whether to create a new User-Assigned Identity (true) or use an existing one (false)" + type = bool + default = true +} + +variable "user_assigned_identity_name" { + description = "The name of the User-Assigned Identity (for new or existing)" + type = string + default = "foundry-uai" +} + +variable "user_assigned_identity_resource_group" { + description = "The resource group of an existing User-Assigned Identity (only used if create_user_assigned_identity=false)" + type = string + default = null +} + +variable "model_name" { + description = "The model to deploy" + type = string + default = "gpt-4o" +} + +variable "model_version" { + description = "The version of the model" + type = string + default = "2024-08-06" +} + +variable "model_capacity" { + description = "The capacity (quota) for the model deployment" + type = number + default = 1 +} diff --git a/infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/code/versions.tf b/infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/code/versions.tf new file mode 100644 index 000000000..e597a8a69 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/20-user-assigned-identity/code/versions.tf @@ -0,0 +1,18 @@ +# Configure the AzApi and AzureRM providers +terraform { + required_providers { + azapi = { + source = "azure/azapi" + version = "~> 2.5" + } + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.37" + } + random = { + source = "hashicorp/random" + version = "~> 3.7" + } + } + required_version = ">= 1.10.0, < 2.0.0" +} diff --git a/infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/README.md b/infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/README.md new file mode 100644 index 000000000..ec08a9c89 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/README.md @@ -0,0 +1,58 @@ +# Entra ID Passthrough Authentication with Storage Connection + +This Terraform template sets up a connection to a storage account and assigns Entra ID passthrough for your storage resource. + +## Description + +- Creates a Storage Account with public network access disabled and shared key access disabled +- Creates a Microsoft Foundry account with public network access disabled +- Creates a project with an Azure Storage connection using Entra ID (AAD) passthrough authentication +- Assigns Storage Blob Data Owner role to the project managed identity on the storage account + +## Entra ID Passthrough + +The storage connection uses Entra ID (AAD) authentication instead of API keys, providing: +- Enhanced security with identity-based access +- Audit trails for user actions +- No API key management required + +## Prerequisites + +- Azure CLI or Terraform installed +- Entra ID tenant access +- Appropriate permissions + +## Deployment + +1. Navigate to the code directory: +```bash +cd code +``` + +2. Initialize Terraform: +```bash +terraform init +``` + +3. Deploy: +```bash +terraform plan +terraform apply +``` + +## Resources Created + +- Storage Account (private network, no shared key access) +- Microsoft Foundry account (private network) +- Microsoft Foundry project +- Azure Storage connection with Entra ID auth +- Storage Blob Data Owner role assignment + +## Documentation + +- [Microsoft Foundry RBAC](https://learn.microsoft.com/en-us/azure/ai-foundry/concepts/rbac-azure-ai-foundry) +- [Disable local authentication in Azure AI services](https://learn.microsoft.com/en-us/azure/ai-services/disable-local-auth) +- [Azure Storage Account - Terraform](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account) +- [AzAPI Provider](https://registry.terraform.io/providers/azure/azapi/latest/docs) + +`Tags: Microsoft.CognitiveServices/accounts, Entra ID, Authentication, Storage` diff --git a/infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/code/example.tfvars b/infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/code/example.tfvars new file mode 100644 index 000000000..a5bdd861e --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/code/example.tfvars @@ -0,0 +1,5 @@ +location = "eastus2" +ai_foundry_name = "foundry-entraid" +ai_project_name = "foundry-entraid-proj" +model_name = "gpt-4.1-mini" +model_version = "2025-04-14" diff --git a/infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/code/main.tf b/infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/code/main.tf new file mode 100644 index 000000000..40300b6e3 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/code/main.tf @@ -0,0 +1,114 @@ +########## Create infrastructure resources +########## + +data "azurerm_client_config" "current" {} + +resource "random_string" "unique" { + length = 4 + min_numeric = 4 + numeric = true + special = false + lower = true + upper = false +} + +locals { + storage_sku = contains(["southindia", "westus"], var.location) ? "GRS" : "ZRS" +} + +resource "azurerm_resource_group" "rg" { + name = "rg-aifoundry${random_string.unique.result}" + location = var.location +} + +## Step 1: Create Storage Account with Entra ID-only access +resource "azurerm_storage_account" "storage" { + name = var.storage_account_name + resource_group_name = azurerm_resource_group.rg.name + location = var.location + account_kind = "StorageV2" + account_tier = "Standard" + account_replication_type = local.storage_sku + + min_tls_version = "TLS1_2" + allow_nested_items_to_be_public = false + public_network_access_enabled = false + shared_access_key_enabled = false + + network_rules { + default_action = "Deny" + bypass = ["AzureServices"] + } +} + +## Step 2: Create AI Foundry account with public network access disabled +resource "azapi_resource" "ai_foundry" { + type = "Microsoft.CognitiveServices/accounts@2025-04-01-preview" + name = var.ai_foundry_name + location = var.location + parent_id = azurerm_resource_group.rg.id + + identity { + type = "SystemAssigned" + } + + body = { + kind = "AIServices" + sku = { + name = "S0" + } + properties = { + allowProjectManagement = true + customSubDomainName = var.ai_foundry_name + disableLocalAuth = false + publicNetworkAccess = "Disabled" + } + } +} + +## Step 3: Create AI Foundry project with storage connection +resource "azapi_resource" "ai_project" { + type = "Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview" + name = var.ai_project_name + location = var.location + parent_id = azapi_resource.ai_foundry.id + + identity { + type = "SystemAssigned" + } + + body = { + properties = { + description = var.project_description + displayName = var.project_display_name + } + } +} + +## Create project-level Azure Storage connection with Entra ID (AAD) auth +resource "azapi_resource" "storage_connection" { + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview" + name = var.storage_account_name + parent_id = azapi_resource.ai_project.id + + body = { + properties = { + category = "AzureStorageAccount" + target = azurerm_storage_account.storage.primary_blob_endpoint + authType = "AAD" + metadata = { + ApiType = "Azure" + ResourceId = azurerm_storage_account.storage.id + location = azurerm_storage_account.storage.location + } + } + } +} + +## Step 4: Assign Storage Blob Data Owner role to the project identity +resource "azurerm_role_assignment" "storage_blob_data_owner" { + scope = azurerm_storage_account.storage.id + role_definition_name = "Storage Blob Data Owner" + principal_id = azapi_resource.ai_project.identity[0].principal_id + principal_type = "ServicePrincipal" +} diff --git a/infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/code/outputs.tf b/infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/code/outputs.tf new file mode 100644 index 000000000..bb08b7bcc --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/code/outputs.tf @@ -0,0 +1,11 @@ +output "resource_group_name" { + value = azurerm_resource_group.rg.name +} + +output "ai_foundry_id" { + value = azapi_resource.ai_foundry.id +} + +output "ai_project_id" { + value = azapi_resource.ai_project.id +} diff --git a/infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/code/providers.tf b/infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/code/providers.tf new file mode 100644 index 000000000..9a71e9677 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/code/providers.tf @@ -0,0 +1,8 @@ +# Setup providers +provider "azapi" { +} + +provider "azurerm" { + features {} + storage_use_azuread = true +} diff --git a/infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/code/variables.tf b/infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/code/variables.tf new file mode 100644 index 000000000..0a518ad58 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/code/variables.tf @@ -0,0 +1,35 @@ +variable "location" { + description = "The Azure region where resources will be deployed" + type = string + default = "eastus2" +} + +variable "ai_foundry_name" { + description = "The name of the AI Foundry account" + type = string + default = "entraid-foundry" +} + +variable "ai_project_name" { + description = "The name of the AI Foundry project" + type = string + default = "entraid-foundry-proj" +} + +variable "project_description" { + description = "Description for the project" + type = string + default = "A project for the AI Foundry account with storage account" +} + +variable "project_display_name" { + description = "Display name for the project" + type = string + default = "project" +} + +variable "storage_account_name" { + description = "Name of the storage account" + type = string + default = "entraidfoundry" +} diff --git a/infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/code/versions.tf b/infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/code/versions.tf new file mode 100644 index 000000000..e597a8a69 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/25-entraid-passthrough/code/versions.tf @@ -0,0 +1,18 @@ +# Configure the AzApi and AzureRM providers +terraform { + required_providers { + azapi = { + source = "azure/azapi" + version = "~> 2.5" + } + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.37" + } + random = { + source = "hashicorp/random" + version = "~> 3.7" + } + } + required_version = ">= 1.10.0, < 2.0.0" +} diff --git a/infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/README.md b/infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/README.md new file mode 100644 index 000000000..b80fec75e --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/README.md @@ -0,0 +1,65 @@ +# Microsoft Foundry with Customer Managed Keys (CMK) + +This Terraform template deploys Microsoft Foundry with Customer-Managed Key (CMK) encryption for data at rest. + +## Description + +- Creates an Microsoft Foundry account +- Configures Customer-Managed Key encryption using Azure Key Vault +- Creates a project +- Deploys a model + +## Important Notes + +- Agent APIs do not support customer-managed key encryption in basic setup +- To use CMK with Agents, you must bring your own storage resources using 'standard' agent setup (see example 31) +- Due to role assignment propagation delays, the initial deployment may fail if the managed identity doesn't have Key Vault access yet - retry if this occurs + +## Prerequisites + +- Azure CLI or Terraform installed +- An existing Azure Key Vault with a key +- Appropriate Azure permissions (Key Vault Administrator or Contributor) + +## Deployment + +1. Navigate to the code directory: +```bash +cd code +``` + +2. Initialize Terraform: +```bash +terraform init +``` + +3. Configure variables with your Key Vault details: + - Key Vault name + - Key name + - Key version + +4. Deploy: +```bash +terraform plan +terraform apply +``` + +## Resources Created + +- Microsoft Foundry account (with CMK encryption) +- Microsoft Foundry project +- Model deployment +- Role assignments for Key Vault access + +## AzAPI Usage + +Uses AzAPI for: +- Customer-managed key configuration on Microsoft Foundry account (not yet available in AzureRM provider) + +## Documentation + +- [Encrypt data at rest with customer-managed keys](https://learn.microsoft.com/en-us/azure/ai-services/encrypt-data-at-rest) +- [azurerm_key_vault - Terraform](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault) +- [AzAPI Provider](https://registry.terraform.io/providers/azure/azapi/latest/docs) + +`Tags: Microsoft.CognitiveServices/accounts, Customer Managed Keys, Key Vault` diff --git a/infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/code/example.tfvars b/infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/code/example.tfvars new file mode 100644 index 000000000..2224815b6 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/code/example.tfvars @@ -0,0 +1,8 @@ +# Example terraform.tfvars file +# Copy this to terraform.tfvars and customize for your deployment + +location = "eastus2" +ai_foundry_name = "foundry-cmk" +ai_project_name = "foundry-cmk-proj" +model_name = "gpt-4.1-mini" +model_version = "2025-04-14" diff --git a/infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/code/main.tf b/infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/code/main.tf new file mode 100644 index 000000000..4a1e90b5c --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/code/main.tf @@ -0,0 +1,154 @@ +########## Create infrastructure resources +########## + +## Get subscription data +data "azurerm_client_config" "current" {} + +## Create a random string for unique naming +resource "random_string" "unique" { + length = 4 + min_numeric = 4 + numeric = true + special = false + lower = true + upper = false +} + +## Create a resource group +resource "azurerm_resource_group" "rg" { + name = "rg-aifoundry${random_string.unique.result}" + location = var.location +} + +## Create Key Vault with soft delete and purge protection for CMK +resource "azurerm_key_vault" "kv" { + name = "kv-${random_string.unique.result}" + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + soft_delete_retention_days = 7 + purge_protection_enabled = true + rbac_authorization_enabled = true +} + +## Grant the deployer Key Vault Administrator to create keys +resource "azurerm_role_assignment" "kv_admin" { + scope = azurerm_key_vault.kv.id + role_definition_name = "Key Vault Administrator" + principal_id = data.azurerm_client_config.current.object_id +} + +## Create encryption key in Key Vault +resource "azurerm_key_vault_key" "cmk" { + name = "cmk-encryption-key" + key_vault_id = azurerm_key_vault.kv.id + key_type = "RSA" + key_size = 2048 + key_opts = ["decrypt", "encrypt", "sign", "unwrapKey", "verify", "wrapKey"] + + depends_on = [azurerm_role_assignment.kv_admin] +} + +## Create AI Foundry account (initially without CMK) +resource "azapi_resource" "ai_foundry" { + type = "Microsoft.CognitiveServices/accounts@2025-06-01" + name = var.ai_foundry_name + location = var.location + parent_id = azurerm_resource_group.rg.id + + identity { + type = "SystemAssigned" + } + + body = { + kind = "AIServices" + sku = { + name = "S0" + } + properties = { + allowProjectManagement = true + customSubDomainName = "aifoundry${random_string.unique.result}" + disableLocalAuth = false + publicNetworkAccess = "Enabled" + } + } +} + +## Grant AI Foundry access to Key Vault +resource "azurerm_role_assignment" "kv_crypto_user" { + scope = azurerm_key_vault.kv.id + role_definition_name = "Key Vault Crypto User" + principal_id = azapi_resource.ai_foundry.identity[0].principal_id +} + +## Wait for role assignment to propagate +resource "time_sleep" "wait_for_rbac" { + depends_on = [azurerm_role_assignment.kv_crypto_user] + create_duration = "60s" +} + +## Update AI Foundry with CMK encryption +resource "azapi_update_resource" "ai_foundry_cmk" { + type = "Microsoft.CognitiveServices/accounts@2025-06-01" + resource_id = azapi_resource.ai_foundry.id + + body = { + properties = { + encryption = { + keySource = "Microsoft.KeyVault" + keyVaultProperties = { + keyName = azurerm_key_vault_key.cmk.name + keyVersion = azurerm_key_vault_key.cmk.version + keyVaultUri = azurerm_key_vault.kv.vault_uri + } + } + } + } + + depends_on = [time_sleep.wait_for_rbac] +} + +## Create AI Foundry project +resource "azapi_resource" "ai_project" { + type = "Microsoft.CognitiveServices/accounts/projects@2025-06-01" + name = coalesce(var.ai_project_name, "${var.ai_foundry_name}-proj") + location = var.location + parent_id = azapi_resource.ai_foundry.id + + identity { + type = "SystemAssigned" + } + + body = { + properties = { + displayName = "project" + description = "My first project" + } + } + + depends_on = [azapi_update_resource.ai_foundry_cmk] +} + +## Deploy model +resource "azapi_resource" "model_deployment" { + type = "Microsoft.CognitiveServices/accounts/deployments@2025-06-01" + name = var.model_name + parent_id = azapi_resource.ai_foundry.id + + body = { + sku = { + capacity = 1 + name = "GlobalStandard" + } + properties = { + model = { + name = var.model_name + format = "OpenAI" + version = var.model_version + } + } + } + + depends_on = [azapi_resource.ai_project] +} diff --git a/infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/code/outputs.tf b/infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/code/outputs.tf new file mode 100644 index 000000000..6af885c25 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/code/outputs.tf @@ -0,0 +1,24 @@ +output "resource_group_name" { + description = "The name of the resource group" + value = azurerm_resource_group.rg.name +} + +output "ai_foundry_id" { + description = "The ID of the AI Foundry account" + value = azapi_resource.ai_foundry.id +} + +output "ai_project_id" { + description = "The ID of the AI Foundry project" + value = azapi_resource.ai_project.id +} + +output "key_vault_uri" { + description = "The URI of the Key Vault" + value = azurerm_key_vault.kv.vault_uri +} + +output "key_vault_name" { + description = "The name of the Key Vault" + value = azurerm_key_vault.kv.name +} diff --git a/infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/code/providers.tf b/infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/code/providers.tf new file mode 100644 index 000000000..9a71e9677 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/code/providers.tf @@ -0,0 +1,8 @@ +# Setup providers +provider "azapi" { +} + +provider "azurerm" { + features {} + storage_use_azuread = true +} diff --git a/infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/code/variables.tf b/infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/code/variables.tf new file mode 100644 index 000000000..d90f8cd09 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/code/variables.tf @@ -0,0 +1,29 @@ +variable "location" { + description = "The Azure region where resources will be deployed" + type = string + default = "eastus2" +} + +variable "ai_foundry_name" { + description = "The name of the AI Foundry account" + type = string + default = "foundry-cmk" +} + +variable "ai_project_name" { + description = "The name of the AI Foundry project" + type = string + default = null # Will default to {ai_foundry_name}-proj +} + +variable "model_name" { + description = "The model to deploy" + type = string + default = "gpt-4.1-mini" +} + +variable "model_version" { + description = "The version of the model" + type = string + default = "2025-04-14" +} diff --git a/infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/code/versions.tf b/infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/code/versions.tf new file mode 100644 index 000000000..15755402c --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/30-customer-managed-keys/code/versions.tf @@ -0,0 +1,22 @@ +# Configure the AzApi and AzureRM providers +terraform { + 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" + } + } + required_version = ">= 1.10.0, < 2.0.0" +} diff --git a/infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/README.md b/infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/README.md new file mode 100644 index 000000000..cdd594bde --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/README.md @@ -0,0 +1,59 @@ +# Customer Managed Keys with Standard Agent Setup + +This folder provides a Terraform implementation for CMK encryption with standard agent setup (BYOS). + +## Overview + +Combines Customer-Managed Key (CMK) encryption with standard agent setup: +- Microsoft Foundry with CMK encryption +- Standard agent resources (Storage, Search, Cosmos DB) with CMK +- Key Vault integration +- Proper key rotation support + +## Status + +**🚧 PARTIALLY IMPLEMENTED** - This scenario requires: + +1. Key Vault with keys for all resources +2. Microsoft Foundry account with CMK +3. Storage Account with CMK +4. Cosmos DB with CMK (if supported) +5. Proper RBAC for key access +6. Role propagation handling + +## Important Notes + +- Agent APIs **require** standard setup (BYOS) for CMK support +- All encryption keys can be in the same Key Vault or separate +- Consider key rotation policies +- Monitor key access patterns + +## Reference + +For guidance, see: +- Bicep: `infrastructure-setup-bicep/31-customer-managed-keys-standard-agent` +- CMK without agents: `../30-customer-managed-keys` +- Standard agent: `../41-standard-agent-setup` + +## Prerequisites + +- Key Vault with keys created +- Cryptographic permissions in Key Vault +- Understanding of Azure encryption +- Knowledge of key rotation + +## AzAPI Usage Rationale + +Uses AzAPI for: +- CMK configuration on Microsoft Foundry (not in AzureRM) +- Advanced encryption settings +- Key Vault property updates + +## Documentation + +- [Encrypt data at rest with customer-managed keys](https://learn.microsoft.com/en-us/azure/ai-services/encrypt-data-at-rest) +- [Set up your agent environment](https://learn.microsoft.com/en-us/azure/ai-services/agents/environment-setup) +- [azurerm_key_vault - Terraform](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault) +- [AzAPI Provider](https://registry.terraform.io/providers/azure/azapi/latest/docs) + +`Tags: Customer Managed Keys, Standard Agent, Encryption, BYOS` diff --git a/infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/code/example.tfvars b/infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/code/example.tfvars new file mode 100644 index 000000000..5b8cdcb89 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/code/example.tfvars @@ -0,0 +1,13 @@ +# Example configuration for customer-managed keys with standard agent + +# Azure region +location = "eastus2" + +# AI Foundry configuration +ai_services_name_prefix = "foundry" +project_name = "cmk-standard-agent-project" + +# Model configuration +model_name = "gpt-4.1" +model_version = "2025-04-14" +model_capacity = 40 diff --git a/infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/code/main.tf b/infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/code/main.tf new file mode 100644 index 000000000..6900e1786 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/code/main.tf @@ -0,0 +1,347 @@ +########## Create infrastructure resources with CMK and Standard Agent +########## + +## Get current client and subscription data +data "azurerm_client_config" "current" {} + +## Create a random string for unique naming +resource "random_string" "unique" { + length = 4 + min_numeric = 4 + numeric = true + special = false + lower = true + upper = false +} + +locals { + account_name = lower("${var.ai_services_name_prefix}${random_string.unique.result}") +} + +## Create a resource group +resource "azurerm_resource_group" "rg" { + name = "rg-aifoundry${random_string.unique.result}" + location = var.location +} + +## Create Key Vault with soft delete and purge protection for CMK +resource "azurerm_key_vault" "kv" { + name = "kv-${random_string.unique.result}" + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + soft_delete_retention_days = 7 + purge_protection_enabled = true + rbac_authorization_enabled = true +} + +## Grant the deployer Key Vault Administrator to create keys +resource "azurerm_role_assignment" "kv_admin" { + scope = azurerm_key_vault.kv.id + role_definition_name = "Key Vault Administrator" + principal_id = data.azurerm_client_config.current.object_id +} + +## Create encryption key in Key Vault +resource "azurerm_key_vault_key" "cmk" { + name = "cmk-encryption-key" + key_vault_id = azurerm_key_vault.kv.id + key_type = "RSA" + key_size = 2048 + key_opts = ["decrypt", "encrypt", "sign", "unwrapKey", "verify", "wrapKey"] + + depends_on = [azurerm_role_assignment.kv_admin] +} + +## Create Storage Account for standard agent +resource "azurerm_storage_account" "storage" { + name = "aifoundry${random_string.unique.result}stor" + resource_group_name = azurerm_resource_group.rg.name + location = var.location + account_kind = "StorageV2" + account_tier = "Standard" + account_replication_type = "ZRS" + + shared_access_key_enabled = false + min_tls_version = "TLS1_2" + allow_nested_items_to_be_public = false + + network_rules { + default_action = "Deny" + bypass = ["AzureServices"] + } +} + +## Create AI Search for standard agent +resource "azurerm_search_service" "search" { + name = replace("aifoundry-${random_string.unique.result}-search", "_", "-") + resource_group_name = azurerm_resource_group.rg.name + location = var.location + sku = "standard" + + local_authentication_enabled = true + authentication_failure_mode = "http401WithBearerChallenge" + public_network_access_enabled = true +} + +## Create Cosmos DB for standard agent +resource "azurerm_cosmosdb_account" "cosmos" { + name = "aifoundry${random_string.unique.result}cosmos" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + offer_type = "Standard" + kind = "GlobalDocumentDB" + + consistency_policy { + consistency_level = "Session" + } + + geo_location { + location = var.location + failover_priority = 0 + } + + public_network_access_enabled = true +} + +## Create AI Foundry account (initially without CMK) +resource "azapi_resource" "ai_foundry" { + type = "Microsoft.CognitiveServices/accounts@2025-06-01" + name = local.account_name + location = var.location + parent_id = azurerm_resource_group.rg.id + + identity { + type = "SystemAssigned" + } + + body = { + kind = "AIServices" + sku = { + name = "S0" + } + properties = { + allowProjectManagement = true + customSubDomainName = local.account_name + publicNetworkAccess = "Enabled" + disableLocalAuth = false + networkAcls = { + defaultAction = "Allow" + virtualNetworkRules = [] + ipRules = [] + } + } + } +} + +## Wait for AI Foundry identity to be available +resource "time_sleep" "wait_for_identity" { + depends_on = [azapi_resource.ai_foundry] + create_duration = "30s" +} + +## Grant AI Foundry identity access to Key Vault for encryption +resource "azurerm_role_assignment" "kv_crypto_user" { + scope = azurerm_key_vault.kv.id + role_definition_name = "Key Vault Crypto User" + principal_id = azapi_resource.ai_foundry.identity[0].principal_id + + depends_on = [time_sleep.wait_for_identity] +} + +## Wait for Key Vault permissions to propagate +resource "time_sleep" "wait_for_kv_permissions" { + depends_on = [azurerm_role_assignment.kv_crypto_user] + create_duration = "30s" +} + +## Update AI Foundry with CMK encryption +resource "azapi_update_resource" "ai_foundry_cmk" { + type = "Microsoft.CognitiveServices/accounts@2025-06-01" + resource_id = azapi_resource.ai_foundry.id + + body = { + properties = { + encryption = { + keySource = "Microsoft.KeyVault" + keyVaultProperties = { + keyName = azurerm_key_vault_key.cmk.name + keyVersion = azurerm_key_vault_key.cmk.version + keyVaultUri = azurerm_key_vault.kv.vault_uri + } + } + } + } + + depends_on = [time_sleep.wait_for_kv_permissions] +} + +## Grant AI Foundry access to Storage +resource "azurerm_role_assignment" "storage_blob_data_contributor" { + scope = azurerm_storage_account.storage.id + role_definition_name = "Storage Blob Data Contributor" + principal_id = azapi_resource.ai_foundry.identity[0].principal_id +} + +## Grant AI Foundry access to AI Search +resource "azurerm_role_assignment" "search_index_data_contributor" { + scope = azurerm_search_service.search.id + role_definition_name = "Search Index Data Contributor" + principal_id = azapi_resource.ai_foundry.identity[0].principal_id +} + +resource "azurerm_role_assignment" "search_service_contributor" { + scope = azurerm_search_service.search.id + role_definition_name = "Search Service Contributor" + principal_id = azapi_resource.ai_foundry.identity[0].principal_id +} + +## Grant AI Foundry access to Cosmos DB +resource "azurerm_cosmosdb_sql_role_assignment" "cosmos_contributor" { + resource_group_name = azurerm_resource_group.rg.name + account_name = azurerm_cosmosdb_account.cosmos.name + role_definition_id = "${azurerm_cosmosdb_account.cosmos.id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002" + principal_id = azapi_resource.ai_foundry.identity[0].principal_id + scope = azurerm_cosmosdb_account.cosmos.id +} + +## Wait for role assignments to propagate +resource "time_sleep" "wait_for_rbac" { + depends_on = [ + azurerm_role_assignment.storage_blob_data_contributor, + azurerm_role_assignment.search_index_data_contributor, + azurerm_role_assignment.search_service_contributor, + azurerm_cosmosdb_sql_role_assignment.cosmos_contributor + ] + create_duration = "60s" +} + +## Create AI Foundry project +resource "azapi_resource" "ai_project" { + type = "Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview" + name = var.project_name + location = var.location + parent_id = azapi_resource.ai_foundry.id + + identity { + type = "SystemAssigned" + } + + body = { + properties = {} + } + + depends_on = [ + time_sleep.wait_for_rbac, + azapi_update_resource.ai_foundry_cmk + ] +} + +## Create connections +resource "azapi_resource" "storage_connection" { + type = "Microsoft.CognitiveServices/accounts/connections@2025-06-01" + name = "storage-connection" + parent_id = azapi_resource.ai_foundry.id + schema_validation_enabled = false + + body = { + properties = { + category = "AzureStorageAccount" + target = azurerm_storage_account.storage.primary_blob_endpoint + authType = "AAD" + isSharedToAll = true + metadata = { + ApiType = "Azure" + ResourceId = azurerm_storage_account.storage.id + location = var.location + } + } + } + + depends_on = [azapi_resource.ai_project] +} + +resource "azapi_resource" "search_connection" { + type = "Microsoft.CognitiveServices/accounts/connections@2025-06-01" + name = "search-connection" + parent_id = azapi_resource.ai_foundry.id + schema_validation_enabled = false + + body = { + properties = { + category = "CognitiveSearch" + target = "https://${azurerm_search_service.search.name}.search.windows.net" + authType = "AAD" + isSharedToAll = true + metadata = { + ApiType = "Azure" + ApiVersion = "2025-05-01-preview" + ResourceId = azurerm_search_service.search.id + location = var.location + } + } + } + + depends_on = [azapi_resource.ai_project] +} + +## Deploy model +resource "azapi_resource" "model_deployment" { + type = "Microsoft.CognitiveServices/accounts/deployments@2025-04-01-preview" + name = var.model_name + parent_id = azapi_resource.ai_foundry.id + + body = { + sku = { + capacity = var.model_capacity + name = "GlobalStandard" + } + properties = { + model = { + name = var.model_name + format = "OpenAI" + version = var.model_version + } + } + } + + depends_on = [azapi_resource.ai_project] +} + +## Set up capability hosts for agent support +resource "azapi_resource" "account_capability_host" { + type = "Microsoft.CognitiveServices/accounts/capabilityHosts@2025-04-01-preview" + name = "${local.account_name}-capHost" + parent_id = azapi_resource.ai_foundry.id + schema_validation_enabled = false + + body = { + properties = { + capabilityHostKind = "Agents" + } + } + + depends_on = [ + azapi_resource.ai_project, + azapi_resource.storage_connection, + azapi_resource.search_connection + ] +} + +resource "azapi_resource" "project_capability_host" { + type = "Microsoft.CognitiveServices/accounts/projects/capabilityHosts@2025-04-01-preview" + name = "${var.project_name}-capHost" + parent_id = azapi_resource.ai_project.id + schema_validation_enabled = false + + body = { + properties = { + capabilityHostKind = "Agents" + storageConnections = ["storage-connection"] + vectorStoreConnections = ["search-connection"] + } + } + + depends_on = [azapi_resource.account_capability_host] +} diff --git a/infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/code/outputs.tf b/infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/code/outputs.tf new file mode 100644 index 000000000..76f43d11f --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/code/outputs.tf @@ -0,0 +1,49 @@ +output "resource_group_name" { + description = "The name of the resource group" + value = azurerm_resource_group.rg.name +} + +output "ai_foundry_id" { + description = "The ID of the AI Foundry account" + value = azapi_resource.ai_foundry.id +} + +output "ai_foundry_name" { + description = "The name of the AI Foundry account" + value = local.account_name +} + +output "ai_project_id" { + description = "The ID of the AI Foundry project" + value = azapi_resource.ai_project.id +} + +output "key_vault_id" { + description = "The ID of the Key Vault" + value = azurerm_key_vault.kv.id +} + +output "key_vault_name" { + description = "The name of the Key Vault" + value = azurerm_key_vault.kv.name +} + +output "encryption_key_id" { + description = "The ID of the encryption key" + value = azurerm_key_vault_key.cmk.id +} + +output "storage_account_id" { + description = "The ID of the storage account" + value = azurerm_storage_account.storage.id +} + +output "search_service_id" { + description = "The ID of the AI Search service" + value = azurerm_search_service.search.id +} + +output "cosmos_db_id" { + description = "The ID of the Cosmos DB account" + value = azurerm_cosmosdb_account.cosmos.id +} diff --git a/infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/code/providers.tf b/infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/code/providers.tf new file mode 100644 index 000000000..fb4b415dc --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/code/providers.tf @@ -0,0 +1,12 @@ +# Setup providers +provider "azapi" { +} + +provider "azurerm" { + features { + key_vault { + purge_soft_delete_on_destroy = false + } + } + storage_use_azuread = true +} diff --git a/infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/code/variables.tf b/infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/code/variables.tf new file mode 100644 index 000000000..685d84eed --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/code/variables.tf @@ -0,0 +1,35 @@ +variable "location" { + description = "The Azure region where resources will be deployed" + type = string + default = "eastus2" +} + +variable "ai_services_name_prefix" { + description = "Prefix for AI Foundry account name" + type = string + default = "foundry" +} + +variable "project_name" { + description = "The name of the project" + type = string + default = "cmk-standard-agent-project" +} + +variable "model_name" { + description = "The model to deploy" + type = string + default = "gpt-4.1" +} + +variable "model_version" { + description = "The version of the model" + type = string + default = "2025-04-14" +} + +variable "model_capacity" { + description = "The capacity of the model deployment" + type = number + default = 40 +} diff --git a/infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/code/versions.tf b/infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/code/versions.tf new file mode 100644 index 000000000..6eef7e581 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/31-customer-managed-keys-standard-agent/code/versions.tf @@ -0,0 +1,22 @@ +# Configure the AzApi and AzureRM providers +terraform { + 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.12" + } + } + required_version = ">= 1.10.0, < 2.0.0" +} diff --git a/infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/README.md b/infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/README.md new file mode 100644 index 000000000..52c8dcb23 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/README.md @@ -0,0 +1,57 @@ +# Customer Managed Keys with User-Assigned Identity + +This folder provides a Terraform implementation for CMK encryption with User-Assigned Managed Identity (with noted platform limitations). + +## Overview + +Combines: +- Customer-Managed Key (CMK) encryption +- User-Assigned Managed Identity +- Key Vault integration +- Microsoft Foundry and project configuration + +## Status + +**🚧 PARTIALLY IMPLEMENTED** - This scenario requires: + +1. User-Assigned Managed Identity creation +2. Key Vault with encryption key +3. RBAC assignments for UAI to Key Vault +4. Microsoft Foundry with CMK and UAI +5. Project with UAI + +## Important Limitation + +**User-Assigned Managed Identity is NOT supported with Customer Managed Keys** in basic setup. + +For CMK with agents, you must use: +- System-Assigned Identity + Standard Agent Setup (see example 31) + +## Reference + +For guidance, see the Bicep reference: +- `infrastructure-setup-bicep/32-customer-managed-keys-user-assigned-identity` + +This scenario may have limited applicability due to the UAI+CMK restriction. + +## Prerequisites + +- Understanding of Azure Managed Identities +- Key Vault and encryption knowledge +- Awareness of CMK limitations + +## Alternative Approaches + +Consider these alternatives: +- Use System-Assigned Identity with CMK (example 30) +- Use UAI without CMK (example 20) +- Use Standard Agent Setup with CMK (example 31) + +## Documentation + +- [Encrypt data at rest with customer-managed keys](https://learn.microsoft.com/en-us/azure/ai-services/encrypt-data-at-rest) +- [azurerm_key_vault - Terraform](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/key_vault) +- [azurerm_user_assigned_identity - Terraform](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/user_assigned_identity) +- [AzAPI Provider](https://registry.terraform.io/providers/azure/azapi/latest/docs) + +`Tags: Customer Managed Keys, User-Assigned Identity, Encryption` diff --git a/infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/code/example.tfvars b/infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/code/example.tfvars new file mode 100644 index 000000000..9fe051a3c --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/code/example.tfvars @@ -0,0 +1,19 @@ +# Example configuration for customer-managed keys with user-assigned identity + +# Azure region +location = "eastus2" + +# AI Foundry configuration +ai_foundry_name = "foundry-cmk-uai" +ai_project_name = null # Will default to {ai_foundry_name}-proj + +# User-Assigned Identity configuration +create_user_assigned_identity = true +user_assigned_identity_name = "foundry-cmk-uai" +# user_assigned_identity_resource_group = "my-existing-rg" # If using existing UAI + +# Model configuration +model_name = "gpt-4o" +model_version = "2024-08-06" +model_capacity = 40 +# For CMK with agents, use System-Assigned Identity (example 31). diff --git a/infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/code/main.tf b/infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/code/main.tf new file mode 100644 index 000000000..d7c9a9052 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/code/main.tf @@ -0,0 +1,159 @@ +########## Create infrastructure with CMK and User-Assigned Identity +########## + +## NOTE: User-Assigned Identity with CMK has limitations +## This setup demonstrates the pattern but may not be supported for all scenarios + +## Get current client config +data "azurerm_client_config" "current" {} + +## Create a random string for unique naming +resource "random_string" "unique" { + length = 4 + min_numeric = 4 + numeric = true + special = false + lower = true + upper = false +} + +## Create a resource group +resource "azurerm_resource_group" "rg" { + name = "rg-aifoundry${random_string.unique.result}" + location = var.location +} + +## Reference existing User-Assigned Identity (if not creating new) +data "azurerm_user_assigned_identity" "existing" { + count = var.create_user_assigned_identity ? 0 : 1 + name = var.user_assigned_identity_name + resource_group_name = var.user_assigned_identity_resource_group +} + +## Create new User-Assigned Identity (if requested) +resource "azurerm_user_assigned_identity" "uai" { + count = var.create_user_assigned_identity ? 1 : 0 + name = var.user_assigned_identity_name + location = var.location + resource_group_name = azurerm_resource_group.rg.name +} + +locals { + uai_id = var.create_user_assigned_identity ? azurerm_user_assigned_identity.uai[0].id : data.azurerm_user_assigned_identity.existing[0].id + uai_principal_id = var.create_user_assigned_identity ? azurerm_user_assigned_identity.uai[0].principal_id : data.azurerm_user_assigned_identity.existing[0].principal_id + uai_client_id = var.create_user_assigned_identity ? azurerm_user_assigned_identity.uai[0].client_id : data.azurerm_user_assigned_identity.existing[0].client_id +} + +## Create Key Vault with soft delete and purge protection for CMK +resource "azurerm_key_vault" "kv" { + name = "kv-${random_string.unique.result}" + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + tenant_id = data.azurerm_client_config.current.tenant_id + sku_name = "standard" + soft_delete_retention_days = 7 + purge_protection_enabled = true + rbac_authorization_enabled = true +} + +## Grant the deployer Key Vault Administrator to create keys +resource "azurerm_role_assignment" "kv_admin" { + scope = azurerm_key_vault.kv.id + role_definition_name = "Key Vault Administrator" + principal_id = data.azurerm_client_config.current.object_id +} + +## Create encryption key in Key Vault +resource "azurerm_key_vault_key" "cmk" { + name = "cmk-encryption-key" + key_vault_id = azurerm_key_vault.kv.id + key_type = "RSA" + key_size = 2048 + key_opts = ["decrypt", "encrypt", "sign", "unwrapKey", "verify", "wrapKey"] + + depends_on = [azurerm_role_assignment.kv_admin] +} + +## Grant UAI access to Key Vault for encryption +resource "azurerm_role_assignment" "uai_kv_crypto" { + scope = azurerm_key_vault.kv.id + role_definition_name = "Key Vault Crypto User" + principal_id = local.uai_principal_id +} + +## Wait for Key Vault permissions +resource "time_sleep" "wait_for_kv_permissions" { + depends_on = [azurerm_role_assignment.uai_kv_crypto] + create_duration = "30s" +} + +## Create AI Foundry account with User-Assigned Identity (initially without CMK) +resource "azapi_resource" "ai_foundry" { + type = "Microsoft.CognitiveServices/accounts@2025-06-01" + name = var.ai_foundry_name + location = var.location + parent_id = azurerm_resource_group.rg.id + + identity { + type = "UserAssigned" + identity_ids = [local.uai_id] + } + + body = { + kind = "AIServices" + sku = { + name = "S0" + } + properties = { + allowProjectManagement = true + customSubDomainName = var.ai_foundry_name + disableLocalAuth = false + publicNetworkAccess = "Enabled" + } + } + + depends_on = [time_sleep.wait_for_kv_permissions] +} + +## Update AI Foundry with CMK encryption +## NOTE: UAI + CMK may have limitations depending on the service configuration +resource "azapi_update_resource" "ai_foundry_cmk" { + type = "Microsoft.CognitiveServices/accounts@2025-06-01" + resource_id = azapi_resource.ai_foundry.id + + body = { + properties = { + encryption = { + keySource = "Microsoft.KeyVault" + keyVaultProperties = { + identityClientId = local.uai_client_id + keyName = azurerm_key_vault_key.cmk.name + keyVersion = azurerm_key_vault_key.cmk.version + keyVaultUri = azurerm_key_vault.kv.vault_uri + } + } + } + } +} + +## Create AI Foundry project with User-Assigned Identity +resource "azapi_resource" "ai_project" { + type = "Microsoft.CognitiveServices/accounts/projects@2025-06-01" + name = coalesce(var.ai_project_name, "${var.ai_foundry_name}-proj") + location = var.location + parent_id = azapi_resource.ai_foundry.id + + identity { + type = "UserAssigned" + identity_ids = [local.uai_id] + } + + body = { + properties = { + displayName = "CMK UAI Project" + description = "Project with Customer-Managed Keys and User-Assigned Identity" + } + } + + depends_on = [azapi_update_resource.ai_foundry_cmk] +} diff --git a/infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/code/outputs.tf b/infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/code/outputs.tf new file mode 100644 index 000000000..65149a883 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/code/outputs.tf @@ -0,0 +1,35 @@ +output "resource_group_name" { + description = "The name of the resource group" + value = azurerm_resource_group.rg.name +} + +output "user_assigned_identity_id" { + description = "The ID of the User-Assigned Identity" + value = local.uai_id +} + +output "ai_foundry_id" { + description = "The ID of the AI Foundry account" + value = azapi_resource.ai_foundry.id +} + +output "ai_project_id" { + description = "The ID of the AI Foundry project" + value = azapi_resource.ai_project.id +} + +output "key_vault_id" { + description = "The ID of the Key Vault" + value = azurerm_key_vault.kv.id +} + +output "key_vault_name" { + description = "The name of the Key Vault" + value = azurerm_key_vault.kv.name +} + +output "encryption_key_id" { + description = "The ID of the encryption key" + value = azurerm_key_vault_key.cmk.id +} + diff --git a/infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/code/providers.tf b/infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/code/providers.tf new file mode 100644 index 000000000..fb4b415dc --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/code/providers.tf @@ -0,0 +1,12 @@ +# Setup providers +provider "azapi" { +} + +provider "azurerm" { + features { + key_vault { + purge_soft_delete_on_destroy = false + } + } + storage_use_azuread = true +} diff --git a/infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/code/variables.tf b/infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/code/variables.tf new file mode 100644 index 000000000..e96832a93 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/code/variables.tf @@ -0,0 +1,37 @@ +variable "location" { + description = "The Azure region where resources will be deployed" + type = string + default = "eastus2" +} + +variable "ai_foundry_name" { + description = "The name of the AI Foundry account" + type = string + default = "foundry-cmk-uai" +} + +variable "ai_project_name" { + description = "The name of the AI Foundry project" + type = string + default = null # Will default to {ai_foundry_name}-proj +} + +variable "create_user_assigned_identity" { + description = "Whether to create a new User-Assigned Identity or use existing" + type = bool + default = true +} + +variable "user_assigned_identity_name" { + description = "Name of the User-Assigned Identity" + type = string + default = "foundry-cmk-uai" +} + +variable "user_assigned_identity_resource_group" { + description = "Resource group of existing UAI (if not creating new)" + type = string + default = "" +} + + diff --git a/infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/code/versions.tf b/infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/code/versions.tf new file mode 100644 index 000000000..6eef7e581 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/32-customer-managed-keys-user-assigned-identity/code/versions.tf @@ -0,0 +1,22 @@ +# Configure the AzApi and AzureRM providers +terraform { + 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.12" + } + } + required_version = ">= 1.10.0, < 2.0.0" +} diff --git a/infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/README.md b/infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/README.md new file mode 100644 index 000000000..ffcce9af9 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/README.md @@ -0,0 +1,69 @@ +# Microsoft Foundry Agent Service: Standard Agent Setup + +This Terraform template provisions resources for standard agent setup with bring-your-own search and storage (BYOS). + +## Description + +- Creates a Cognitive Services Account +- Deploys a GPT-4.1 model +- Creates a project +- **Creates Azure Storage Account for agent data** +- **Creates Azure AI Search for agent indexing** +- **Creates Cosmos DB for agent threads** +- Connects these resources to the project + +## Standard vs Basic Setup + +**Standard Setup** (this template): +- You manage and control the storage, search, and database resources +- Better for production workloads requiring specific configurations +- Supports customer-managed keys and private networking +- More control over data residency and compliance + +**Basic Setup** (see example 40): +- Microsoft manages storage and search resources +- Simpler setup for development and testing + +## Prerequisites + +- Azure CLI or Terraform installed +- Appropriate Azure permissions (Contributor role) + +## Deployment + +1. Navigate to the code directory: +```bash +cd code +``` + +2. Initialize Terraform: +```bash +terraform init +``` + +3. Customize variables in terraform.tfvars + +4. Deploy: +```bash +terraform plan +terraform apply +``` + +## Resources Created + +- Microsoft Foundry account +- Microsoft Foundry project +- GPT-4.1 model deployment +- Storage Account (for agent file storage) +- Azure AI Search (for agent indexing) +- Cosmos DB Account (for agent threads) +- Connections and RBAC assignments + +## Documentation + +- [Set up your agent environment](https://learn.microsoft.com/en-us/azure/ai-services/agents/environment-setup) +- [azurerm_storage_account - Terraform](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/storage_account) +- [azurerm_search_service - Terraform](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/search_service) +- [AzAPI Provider](https://registry.terraform.io/providers/azure/azapi/latest/docs) + +`Tags: Microsoft.CognitiveServices/accounts/projects, Standard Agent, BYOS` diff --git a/infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/code/example.tfvars b/infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/code/example.tfvars new file mode 100644 index 000000000..b5db0b002 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/code/example.tfvars @@ -0,0 +1,9 @@ +# Example terraform.tfvars file +# Copy this to terraform.tfvars and customize for your deployment + +location = "eastus2" +ai_services_name_prefix = "foundry" +project_name = "standard-agent-project" +model_name = "gpt-4.1" +model_version = "2025-04-14" +model_capacity = 40 diff --git a/infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/code/main.tf b/infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/code/main.tf new file mode 100644 index 000000000..d6f6c3d59 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/code/main.tf @@ -0,0 +1,305 @@ +########## Create infrastructure resources +########## + +## Get subscription data +data "azurerm_client_config" "current" {} + +## Create a random string for unique naming +resource "random_string" "unique" { + length = 4 + min_numeric = 4 + numeric = true + special = false + lower = true + upper = false +} + +locals { + account_name = lower("${var.ai_services_name_prefix}${random_string.unique.result}") +} + +## Create a resource group +resource "azurerm_resource_group" "rg" { + name = "rg-aifoundry${random_string.unique.result}" + location = var.location +} + +## Create Storage Account for agent data +resource "azurerm_storage_account" "storage" { + name = "aifoundry${random_string.unique.result}stor" + resource_group_name = azurerm_resource_group.rg.name + location = var.location + account_kind = "StorageV2" + account_tier = "Standard" + account_replication_type = "ZRS" + + shared_access_key_enabled = false + min_tls_version = "TLS1_2" + allow_nested_items_to_be_public = false + + network_rules { + default_action = "Allow" + bypass = ["AzureServices"] + } +} + +## Create Azure AI Search for agent indexing +resource "azurerm_search_service" "search" { + name = replace("aifoundry-${random_string.unique.result}-search", "_", "-") + resource_group_name = azurerm_resource_group.rg.name + location = var.location + sku = "standard" + + local_authentication_enabled = true + authentication_failure_mode = "http401WithBearerChallenge" + public_network_access_enabled = true +} + +## Create Cosmos DB for agent threads +resource "azurerm_cosmosdb_account" "cosmos" { + name = "aifoundry${random_string.unique.result}cosmos" + location = var.location + resource_group_name = azurerm_resource_group.rg.name + offer_type = "Standard" + kind = "GlobalDocumentDB" + + consistency_policy { + consistency_level = "Session" + } + + geo_location { + location = var.location + failover_priority = 0 + } + + public_network_access_enabled = true +} + +## Create AI Foundry account +resource "azapi_resource" "ai_foundry" { + type = "Microsoft.CognitiveServices/accounts@2025-04-01-preview" + name = local.account_name + location = var.location + parent_id = azurerm_resource_group.rg.id + + identity { + type = "SystemAssigned" + } + + body = { + kind = "AIServices" + sku = { + name = "S0" + } + properties = { + allowProjectManagement = true + customSubDomainName = local.account_name + publicNetworkAccess = "Enabled" + disableLocalAuth = false + networkAcls = { + defaultAction = "Allow" + virtualNetworkRules = [] + ipRules = [] + } + } + } +} + +## Grant project access to Storage +resource "azurerm_role_assignment" "storage_blob_data_contributor" { + scope = azurerm_storage_account.storage.id + role_definition_name = "Storage Blob Data Contributor" + principal_id = azapi_resource.ai_project.identity[0].principal_id +} + +## Grant project access to AI Search +resource "azurerm_role_assignment" "search_index_data_contributor" { + scope = azurerm_search_service.search.id + role_definition_name = "Search Index Data Contributor" + principal_id = azapi_resource.ai_project.identity[0].principal_id +} + +resource "azurerm_role_assignment" "search_service_contributor" { + scope = azurerm_search_service.search.id + role_definition_name = "Search Service Contributor" + principal_id = azapi_resource.ai_project.identity[0].principal_id +} + +## Grant project access to Cosmos DB (ARM-level Operator role) +resource "azurerm_role_assignment" "cosmos_db_operator" { + scope = azurerm_cosmosdb_account.cosmos.id + role_definition_name = "Cosmos DB Operator" + principal_id = azapi_resource.ai_project.identity[0].principal_id +} + +## Wait for role assignments to propagate +resource "time_sleep" "wait_for_rbac" { + depends_on = [ + azurerm_role_assignment.storage_blob_data_contributor, + azurerm_role_assignment.search_index_data_contributor, + azurerm_role_assignment.search_service_contributor, + azurerm_role_assignment.cosmos_db_operator + ] + create_duration = "60s" +} + +## Create AI Foundry project +resource "azapi_resource" "ai_project" { + type = "Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview" + name = var.project_name + location = var.location + parent_id = azapi_resource.ai_foundry.id + + identity { + type = "SystemAssigned" + } + + body = { + properties = { + description = "Standard agent project with BYOS" + displayName = var.project_name + } + } + + depends_on = [azapi_resource.ai_foundry] +} + +## Create connections as project sub-resources (matching bicep) +resource "azapi_resource" "storage_connection" { + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview" + name = azurerm_storage_account.storage.name + parent_id = azapi_resource.ai_project.id + + body = { + properties = { + category = "AzureStorageAccount" + target = azurerm_storage_account.storage.primary_blob_endpoint + authType = "AAD" + metadata = { + ApiType = "Azure" + ResourceId = azurerm_storage_account.storage.id + location = var.location + } + } + } +} + +resource "azapi_resource" "search_connection" { + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview" + name = azurerm_search_service.search.name + parent_id = azapi_resource.ai_project.id + + body = { + properties = { + category = "CognitiveSearch" + target = "https://${azurerm_search_service.search.name}.search.windows.net" + authType = "AAD" + metadata = { + ApiType = "Azure" + ResourceId = azurerm_search_service.search.id + location = var.location + } + } + } +} + +resource "azapi_resource" "cosmos_connection" { + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview" + name = azurerm_cosmosdb_account.cosmos.name + parent_id = azapi_resource.ai_project.id + + body = { + properties = { + category = "CosmosDB" + target = azurerm_cosmosdb_account.cosmos.endpoint + authType = "AAD" + metadata = { + ApiType = "Azure" + ResourceId = azurerm_cosmosdb_account.cosmos.id + location = var.location + } + } + } +} + +## Deploy model +resource "azapi_resource" "model_deployment" { + type = "Microsoft.CognitiveServices/accounts/deployments@2025-04-01-preview" + name = var.model_name + parent_id = azapi_resource.ai_foundry.id + + body = { + sku = { + capacity = var.model_capacity + name = "GlobalStandard" + } + properties = { + model = { + name = var.model_name + format = "OpenAI" + version = var.model_version + } + } + } + + depends_on = [azapi_resource.ai_project] +} + +## Set up capability hosts for agent support +resource "azapi_resource" "account_capability_host" { + type = "Microsoft.CognitiveServices/accounts/capabilityHosts@2025-04-01-preview" + name = "${local.account_name}-capHost" + parent_id = azapi_resource.ai_foundry.id + schema_validation_enabled = false + + body = { + properties = { + capabilityHostKind = "Agents" + } + } + + timeouts { + create = "60m" + } + + depends_on = [ + azapi_resource.ai_project, + azapi_resource.storage_connection, + azapi_resource.search_connection, + azapi_resource.cosmos_connection, + time_sleep.wait_for_rbac + ] +} + +resource "azapi_resource" "project_capability_host" { + type = "Microsoft.CognitiveServices/accounts/projects/capabilityHosts@2025-04-01-preview" + name = "${var.project_name}-capHost" + parent_id = azapi_resource.ai_project.id + schema_validation_enabled = false + + body = { + properties = { + capabilityHostKind = "Agents" + storageConnections = [azurerm_storage_account.storage.name] + vectorStoreConnections = [azurerm_search_service.search.name] + threadStorageConnections = [azurerm_cosmosdb_account.cosmos.name] + } + } + + timeouts { + create = "60m" + } + + depends_on = [azapi_resource.account_capability_host] +} + +## Grant project Cosmos DB data-plane access (after caphost creates enterprise_memory db) +resource "azurerm_cosmosdb_sql_role_assignment" "cosmos_contributor" { + resource_group_name = azurerm_resource_group.rg.name + account_name = azurerm_cosmosdb_account.cosmos.name + role_definition_id = "${azurerm_cosmosdb_account.cosmos.id}/sqlRoleDefinitions/00000000-0000-0000-0000-000000000002" + principal_id = azapi_resource.ai_project.identity[0].principal_id + scope = "${azurerm_cosmosdb_account.cosmos.id}/dbs/enterprise_memory" + + depends_on = [azapi_resource.project_capability_host] +} diff --git a/infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/code/outputs.tf b/infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/code/outputs.tf new file mode 100644 index 000000000..1549f0a71 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/code/outputs.tf @@ -0,0 +1,29 @@ +output "resource_group_name" { + description = "The name of the resource group" + value = azurerm_resource_group.rg.name +} + +output "ai_foundry_id" { + description = "The ID of the AI Foundry account" + value = azapi_resource.ai_foundry.id +} + +output "ai_project_id" { + description = "The ID of the AI Foundry project" + value = azapi_resource.ai_project.id +} + +output "storage_account_name" { + description = "The name of the storage account" + value = azurerm_storage_account.storage.name +} + +output "search_service_name" { + description = "The name of the search service" + value = azurerm_search_service.search.name +} + +output "cosmos_account_name" { + description = "The name of the Cosmos DB account" + value = azurerm_cosmosdb_account.cosmos.name +} diff --git a/infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/code/providers.tf b/infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/code/providers.tf new file mode 100644 index 000000000..9a71e9677 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/code/providers.tf @@ -0,0 +1,8 @@ +# Setup providers +provider "azapi" { +} + +provider "azurerm" { + features {} + storage_use_azuread = true +} diff --git a/infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/code/variables.tf b/infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/code/variables.tf new file mode 100644 index 000000000..4560e2c7d --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/code/variables.tf @@ -0,0 +1,35 @@ +variable "location" { + description = "The Azure region where resources will be deployed" + type = string + default = "eastus2" +} + +variable "ai_services_name_prefix" { + description = "Prefix for AI Foundry account name" + type = string + default = "foundry" +} + +variable "project_name" { + description = "The name of the project" + type = string + default = "standard-agent-project" +} + +variable "model_name" { + description = "The model to deploy" + type = string + default = "gpt-4.1" +} + +variable "model_version" { + description = "The version of the model" + type = string + default = "2025-04-14" +} + +variable "model_capacity" { + description = "The capacity of the model deployment" + type = number + default = 40 +} diff --git a/infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/code/versions.tf b/infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/code/versions.tf new file mode 100644 index 000000000..15755402c --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/41-standard-agent-setup/code/versions.tf @@ -0,0 +1,22 @@ +# Configure the AzApi and AzureRM providers +terraform { + 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" + } + } + required_version = ">= 1.10.0, < 2.0.0" +} diff --git a/infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/README.md b/infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/README.md new file mode 100644 index 000000000..d07f2781b --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/README.md @@ -0,0 +1,62 @@ +# Microsoft Foundry Agent Service: Basic Agent Setup with the Bing Search Tool + +Use this template as a starting point for creating a basic agent project where you know you will want to create agents with the Grounding with Bing Search tool. + +For more information, see [Azure AI Services Agents Environment Setup](https://learn.microsoft.com/en-us/azure/ai-services/agents/environment-setup). + +> **Note:** Deploying the template does not create an agent; it only provisions the necessary resources to get started. + +## Description + +- Creates an Microsoft Foundry account +- Creates a project +- Deploys a GPT-4.1 model +- Creates a Bing Grounding connection with API key authentication + +## Prerequisites + +- Azure CLI or Terraform installed +- A valid Bing Search v7 API key (obtain from Azure Portal) +- Appropriate Azure permissions + +## Deployment + +1. Navigate to the code directory: +```bash +cd code +``` + +2. Initialize Terraform: +```bash +terraform init +``` + +3. Provide your Bing API key via terraform.tfvars or environment variable + +4. Deploy: +```bash +terraform plan +terraform apply +``` + +## Getting Started + +Creating your first agent with Microsoft Foundry Agent Service is a two-step process: + +1. **Set up your agent environment** (this template) +2. **Create and configure your agent** with Bing Search tool using your preferred SDK or the Azure Foundry Portal + +## Resources Created + +- Microsoft Foundry account +- Microsoft Foundry project +- Model deployment (GPT-4.1) +- Bing Grounding connection + +## Documentation + +- [Set up your agent environment](https://learn.microsoft.com/en-us/azure/ai-services/agents/environment-setup) +- [Bing Search API](https://learn.microsoft.com/en-us/bing/search-apis/bing-web-search/overview) +- [AzAPI Provider](https://registry.terraform.io/providers/azure/azapi/latest/docs) + +`Tags: Microsoft.CognitiveServices/accounts/projects, Microsoft.CognitiveServices/accounts/connections, Bing Search` diff --git a/infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/code/example.tfvars b/infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/code/example.tfvars new file mode 100644 index 000000000..865ff04d9 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/code/example.tfvars @@ -0,0 +1,11 @@ +# Example terraform.tfvars file +# Copy this to terraform.tfvars and customize for your deployment + +location = "eastus2" +ai_foundry_name = "foundry-bing" +ai_project_name = "foundry-bing-proj" +bing_api_key = "YOUR_BING_API_KEY_HERE" # Replace with your actual Bing API key +bing_connection_name = "bing-grounding" +model_name = "gpt-4.1" +model_version = "2025-04-14" +model_capacity = 40 diff --git a/infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/code/main.tf b/infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/code/main.tf new file mode 100644 index 000000000..d1b3bef58 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/code/main.tf @@ -0,0 +1,142 @@ +########## Create infrastructure resources +########## + +## Get subscription data +data "azurerm_client_config" "current" {} + +## Create a random string for unique naming +resource "random_string" "unique" { + length = 4 + min_numeric = 4 + numeric = true + special = false + lower = true + upper = false +} + +locals { + account_name = lower("${var.ai_services_name}${random_string.unique.result}") +} + +## Create a resource group +resource "azurerm_resource_group" "rg" { + name = "rg-aifoundry${random_string.unique.result}" + location = var.location +} + +## Step 1: Create AI Foundry account and deploy model +resource "azapi_resource" "ai_foundry" { + type = "Microsoft.CognitiveServices/accounts@2025-04-01-preview" + name = local.account_name + location = var.location + parent_id = azurerm_resource_group.rg.id + + identity { + type = "SystemAssigned" + } + + body = { + kind = "AIServices" + sku = { + name = "S0" + } + properties = { + allowProjectManagement = true + customSubDomainName = local.account_name + disableLocalAuth = false + publicNetworkAccess = "Enabled" + networkAcls = { + defaultAction = "Allow" + virtualNetworkRules = [] + ipRules = [] + } + } + } +} + +resource "azapi_resource" "model_deployment" { + type = "Microsoft.CognitiveServices/accounts/deployments@2025-04-01-preview" + name = var.model_name + parent_id = azapi_resource.ai_foundry.id + + body = { + sku = { + capacity = var.model_capacity + name = var.model_sku_name + } + properties = { + model = { + name = var.model_name + format = var.model_format + version = var.model_version + } + } + } +} + +## Step 2: Create AI Foundry project +resource "azapi_resource" "ai_project" { + type = "Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview" + name = var.project_name + location = var.location + parent_id = azapi_resource.ai_foundry.id + + identity { + type = "SystemAssigned" + } + + body = { + properties = { + description = var.project_description + displayName = var.project_display_name + } + } +} + +## Step 3: Create Bing Search resource +resource "azapi_resource" "bing_search" { + type = "Microsoft.Bing/accounts@2020-06-10" + name = "bingsearch-${local.account_name}" + parent_id = azurerm_resource_group.rg.id + location = "global" + schema_validation_enabled = false + + body = { + sku = { + name = "G1" + } + kind = "Bing.Grounding" + } +} + +## Get Bing Search keys +data "azapi_resource_action" "bing_keys" { + type = "Microsoft.Bing/accounts@2020-06-10" + resource_id = azapi_resource.bing_search.id + action = "listKeys" + response_export_values = ["key1"] +} + +## Step 4: Create Bing Search connection using the Bing resource key +resource "azapi_resource" "bing_connection" { + type = "Microsoft.CognitiveServices/accounts/connections@2025-04-01-preview" + name = "bing-grounding" + parent_id = azapi_resource.ai_foundry.id + + body = { + properties = { + category = "ApiKey" + target = "https://api.bing.microsoft.com/" + authType = "ApiKey" + isSharedToAll = true + credentials = { + key = data.azapi_resource_action.bing_keys.output.key1 + } + metadata = { + ApiType = "Azure" + Location = azapi_resource.bing_search.location + ResourceId = azapi_resource.bing_search.id + } + } + } +} diff --git a/infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/code/outputs.tf b/infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/code/outputs.tf new file mode 100644 index 000000000..af64827fb --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/code/outputs.tf @@ -0,0 +1,24 @@ +output "resource_group_name" { + description = "The name of the resource group" + value = azurerm_resource_group.rg.name +} + +output "ai_foundry_id" { + description = "The ID of the AI Foundry account" + value = azapi_resource.ai_foundry.id +} + +output "ai_project_id" { + description = "The ID of the AI Foundry project" + value = azapi_resource.ai_project.id +} + +output "bing_connection_id" { + description = "The ID of the Bing connection" + value = azapi_resource.bing_connection.id +} + +output "model_deployment_name" { + description = "The name of the model deployment" + value = azapi_resource.model_deployment.name +} diff --git a/infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/code/providers.tf b/infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/code/providers.tf new file mode 100644 index 000000000..9a71e9677 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/code/providers.tf @@ -0,0 +1,8 @@ +# Setup providers +provider "azapi" { +} + +provider "azurerm" { + features {} + storage_use_azuread = true +} diff --git a/infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/code/variables.tf b/infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/code/variables.tf new file mode 100644 index 000000000..50ce0d9e0 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/code/variables.tf @@ -0,0 +1,59 @@ +variable "location" { + description = "The Azure region where resources will be deployed" + type = string + default = "eastus2" +} + +variable "ai_services_name" { + description = "Prefix for AI Foundry account name" + type = string + default = "aiServices" +} + +variable "project_name" { + description = "The name of the project" + type = string + default = "project" +} + +variable "project_description" { + description = "Description for the project" + type = string + default = "some description" +} + +variable "project_display_name" { + description = "Display name for the project" + type = string + default = "project_display_name" +} + +variable "model_name" { + description = "The model to deploy" + type = string + default = "gpt-4.1" +} + +variable "model_format" { + description = "The model format" + type = string + default = "OpenAI" +} + +variable "model_version" { + description = "The version of the model" + type = string + default = "2025-04-14" +} + +variable "model_sku_name" { + description = "The SKU name for the model deployment" + type = string + default = "GlobalStandard" +} + +variable "model_capacity" { + description = "The capacity (quota) for the model deployment" + type = number + default = 30 +} diff --git a/infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/code/versions.tf b/infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/code/versions.tf new file mode 100644 index 000000000..e597a8a69 --- /dev/null +++ b/infrastructure/infrastructure-setup-terraform/45-basic-agent-bing/code/versions.tf @@ -0,0 +1,18 @@ +# Configure the AzApi and AzureRM providers +terraform { + required_providers { + azapi = { + source = "azure/azapi" + version = "~> 2.5" + } + azurerm = { + source = "hashicorp/azurerm" + version = "~> 4.37" + } + random = { + source = "hashicorp/random" + version = "~> 3.7" + } + } + required_version = ">= 1.10.0, < 2.0.0" +}