diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml index 7c1eedb6..b99c06a3 100644 --- a/.github/workflows/terraform.yml +++ b/.github/workflows/terraform.yml @@ -36,7 +36,7 @@ jobs: tenant_id: "37963dd4-f4e6-40f8-a7d6-24b97919e452" subscription_id: "9842be63-c8c0-4647-a5d1-0c5e7f8bbb25" secrets: - CLIENT_ID_PLAN: ${{ secrets.CLIENT_ID_PLAN }} + CLIENT_ID_PLAN: ${{ secrets.CLIENT_ID }} CLIENT_ID_APPLY: ${{ secrets.CLIENT_ID }} terraform_destroy: diff --git a/README.md b/README.md index 6ccd02f5..01d57e31 100644 --- a/README.md +++ b/README.md @@ -242,6 +242,7 @@ object( storage_subnet = string consumption_subnet = string fabric_subnet = string + aifoundry_subnet = optional(string, "") databricks_engineering_private_subnet = string databricks_engineering_public_subnet = string databricks_consumption_private_subnet = optional(string, "") @@ -260,6 +261,29 @@ Type: `string` The following input variables are optional (have default values): +### [ai\_foundry\_account\_details](#input\_ai\_foundry\_account\_details) + +Description: Specifies the ai foundry configuration. + +Type: + +```hcl +object({ + enabled = optional(bool, false) + search_service = optional(object({ + sku = optional(string, "basic") + semantic_search_sku = optional(string, "standard") + partition_count = optional(number, 1) + replica_count = optional(number, 1) + }), {}) + cosmos_db = optional(object({ + consistency_level = optional(string, "Session") + }), {}) + }) +``` + +Default: `{}` + ### [customer\_managed\_key](#input\_customer\_managed\_key) Description: Specifies the customer managed key configurations. @@ -411,6 +435,14 @@ Type: `string` Default: `""` +### [private\_dns\_zone\_id\_ai\_services](#input\_private\_dns\_zone\_id\_ai\_services) + +Description: Specifies the resource ID of the private DNS zone for Azure Foundry (AI Services). Not required if DNS A-records get created via Azure Policy. + +Type: `string` + +Default: `""` + ### [private\_dns\_zone\_id\_blob](#input\_private\_dns\_zone\_id\_blob) Description: Specifies the resource ID of the private DNS zone for Azure Storage blob endpoints. Not required if DNS A-records get created via Azue Policy. @@ -427,6 +459,14 @@ Type: `string` Default: `""` +### [private\_dns\_zone\_id\_cosmos\_sql](#input\_private\_dns\_zone\_id\_cosmos\_sql) + +Description: Specifies the resource ID of the private DNS zone for cosmos db sql. Not required if DNS A-records get created via Azure Policy. + +Type: `string` + +Default: `""` + ### [private\_dns\_zone\_id\_data\_factory](#input\_private\_dns\_zone\_id\_data\_factory) Description: Specifies the resource ID of the private DNS zone for Azure Data Factory. Not required if DNS A-records get created via Azure Policy. diff --git a/databricks.tf b/databricks.tf index ad197682..e81a7b6b 100644 --- a/databricks.tf +++ b/databricks.tf @@ -23,6 +23,7 @@ module "databricks_core" { databricks_ip_access_list_deny = [] databricks_network_connectivity_config_name = var.databricks_network_connectivity_config_name databricks_compliance_security_profile_standards = var.databricks_compliance_security_profile_standards + # databricks_network_policy_details = var.databricks_network_policy_details # Identity variables service_principal_name_terraform_plan = var.service_principal_name_terraform_plan diff --git a/main.tf b/main.tf index af35356d..2dda0b21 100644 --- a/main.tf +++ b/main.tf @@ -19,6 +19,7 @@ module "platform" { subnet_cidr_range_storage = var.subnet_cidr_ranges.storage_subnet subnet_cidr_range_consumption = var.subnet_cidr_ranges.consumption_subnet subnet_cidr_range_fabric = var.subnet_cidr_ranges.fabric_subnet + subnet_cidr_range_aifoundry = var.subnet_cidr_ranges.aifoundry_subnet subnet_cidr_range_engineering_private = var.subnet_cidr_ranges.databricks_engineering_private_subnet subnet_cidr_range_engineering_public = var.subnet_cidr_ranges.databricks_engineering_public_subnet subnet_cidr_range_consumption_private = var.subnet_cidr_ranges.databricks_consumption_private_subnet @@ -30,6 +31,7 @@ module "platform" { } } databricks_workspace_consumption_enabled = var.databricks_workspace_consumption_enabled + aifoundry_enabled = var.ai_foundry_account_details.enabled } module "core" { @@ -53,6 +55,7 @@ module "core" { databricks_compliance_security_profile_standards = var.databricks_compliance_security_profile_standards databricks_workspace_consumption_enabled = var.databricks_workspace_consumption_enabled fabric_capacity_details = var.fabric_capacity_details + ai_foundry_account_details = var.ai_foundry_account_details # HA/DR variables zone_redundancy_enabled = var.zone_redundancy_enabled @@ -68,6 +71,7 @@ module "core" { vnet_id = var.vnet_id subnet_id_storage = module.platform.subnet_id_storage subnet_id_consumption = module.platform.subnet_id_consumption + subnet_id_aifoundry = module.platform.subnet_id_aifoundry subnet_id_engineering_private = module.platform.subnet_id_engineering_private subnet_id_engineering_public = module.platform.subnet_id_engineering_public subnet_id_consumption_private = module.platform.subnet_id_consumption_private @@ -75,10 +79,15 @@ module "core" { connectivity_delay_in_seconds = local.connectivity_delay_in_seconds # DNS variables - private_dns_zone_id_blob = var.private_dns_zone_id_blob - private_dns_zone_id_dfs = var.private_dns_zone_id_dfs - private_dns_zone_id_queue = var.private_dns_zone_id_queue - private_dns_zone_id_databricks = var.private_dns_zone_id_databricks + private_dns_zone_id_blob = var.private_dns_zone_id_blob + private_dns_zone_id_dfs = var.private_dns_zone_id_dfs + private_dns_zone_id_queue = var.private_dns_zone_id_queue + private_dns_zone_id_databricks = var.private_dns_zone_id_databricks + private_dns_zone_id_ai_services = var.private_dns_zone_id_ai_services + private_dns_zone_id_cognitive_account = var.private_dns_zone_id_cognitive_account + private_dns_zone_id_open_ai = var.private_dns_zone_id_open_ai + private_dns_zone_id_search_service = var.private_dns_zone_id_search_service + private_dns_zone_id_cosmos_sql = var.private_dns_zone_id_cosmos_sql # Customer-managed key variables customer_managed_key = var.customer_managed_key @@ -135,12 +144,17 @@ module "data_application" { root_folder = try(each.value.repository.github.fabric_root_folder, "") } } + ai_foundry_account_details = module.core.ai_foundry_account_details + ai_foundry_project_details = { + enabled = try(each.value.ai_foundry_project.enabled, false) + } storage_dependencies = module.core.storage_dependencies # HA/DR variables zone_redundancy_enabled = var.zone_redundancy_enabled # Logging and monitoring variables + log_analytics_workspace_id = var.log_analytics_workspace_id diagnostics_configurations = local.diagnostics_configurations alerting = try(each.value.alerting, {}) diff --git a/modules/core/aifoundry.tf b/modules/core/aifoundry.tf new file mode 100644 index 00000000..f9351bcc --- /dev/null +++ b/modules/core/aifoundry.tf @@ -0,0 +1,34 @@ +module "ai_foundry_account" { + source = "github.com/PerfectThymeTech/terraform-azurerm-modules//modules/aifoundry?ref=marvinbuss/ai_foundry" + providers = { + azurerm = azurerm + time = time + } + + count = var.ai_foundry_account_details.enabled ? 1 : 0 + + location = var.location + resource_group_name = one(azurerm_resource_group.resource_group_ai[*].name) + tags = var.tags + ai_services_name = "${local.prefix}-aif001" + ai_services_sku = "S0" + ai_services_firewall_bypass_azure_services = true + ai_services_outbound_network_access_restricted = true + ai_services_outbound_network_access_allowed_fqdns = [] + ai_services_local_auth_enabled = false + ai_services_projects = {} + ai_services_cosmosdb_accounts = {} + ai_services_storage_accounts = {} + ai_services_aisearch_accounts = {} + ai_services_openai_accounts = {} + ai_services_connections_account = {} + ai_services_deployments = {} + diagnostics_configurations = var.diagnostics_configurations + subnet_id = var.subnet_id_consumption + subnet_id_capability_hosts = var.subnet_id_aifoundry + connectivity_delay_in_seconds = var.connectivity_delay_in_seconds + private_dns_zone_id_ai_services = var.private_dns_zone_id_ai_services + private_dns_zone_id_cognitive_account = var.private_dns_zone_id_cognitive_account + private_dns_zone_id_open_ai = var.private_dns_zone_id_open_ai + customer_managed_key = var.customer_managed_key +} diff --git a/modules/core/aisearch.tf b/modules/core/aisearch.tf new file mode 100644 index 00000000..3faee5a4 --- /dev/null +++ b/modules/core/aisearch.tf @@ -0,0 +1,27 @@ +module "ai_search" { + source = "github.com/PerfectThymeTech/terraform-azurerm-modules//modules/aisearch?ref=main" + providers = { + azurerm = azurerm + time = time + } + + count = var.ai_foundry_account_details.enabled ? 1 : 0 + + location = var.location + resource_group_name = one(azurerm_resource_group.resource_group_ai[*].name) + tags = var.tags + search_service_name = "${local.prefix}-srch001" + search_service_sku = var.ai_foundry_account_details.search_service.sku + search_service_semantic_search_sku = var.ai_foundry_account_details.search_service.semantic_search_sku + search_service_local_authentication_enabled = false + search_service_authentication_failure_mode = null + search_service_hosting_mode = "default" + search_service_partition_count = var.ai_foundry_account_details.search_service.partition_count + search_service_replica_count = var.ai_foundry_account_details.search_service.replica_count + search_service_shared_private_links = local.search_service_shared_private_links + diagnostics_configurations = var.diagnostics_configurations + subnet_id = var.subnet_id_consumption + connectivity_delay_in_seconds = var.connectivity_delay_in_seconds + private_dns_zone_id_search_service = var.private_dns_zone_id_search_service + customer_managed_key = var.customer_managed_key +} diff --git a/modules/core/cosmosdb.tf b/modules/core/cosmosdb.tf new file mode 100644 index 00000000..69201a8f --- /dev/null +++ b/modules/core/cosmosdb.tf @@ -0,0 +1,56 @@ +module "cosmos_db" { + source = "github.com/PerfectThymeTech/terraform-azurerm-modules//modules/cosmosdb?ref=main" + providers = { + azurerm = azurerm + time = time + } + + count = var.ai_foundry_account_details.enabled ? 1 : 0 + + location = var.location + resource_group_name = one(azurerm_resource_group.resource_group_ai[*].name) + tags = var.tags + cosmosdb_account_name = "${local.prefix}-csms001" + cosmosdb_account_access_key_metadata_writes_enabled = false + cosmosdb_account_analytical_storage_enabled = false + cosmosdb_account_automatic_failover_enabled = false + cosmosdb_account_backup = { + type = "Continuous" + tier = "Continuous7Days" + storage_redundancy = null + retention_in_hours = null + interval_in_minutes = null + } + cosmosdb_account_capabilities = [] + cosmosdb_account_capacity_total_throughput_limit = -1 + cosmosdb_account_consistency_policy = { + consistency_level = var.ai_foundry_account_details.cosmos_db.consistency_level + max_interval_in_seconds = null + max_staleness_prefix = null + } + cosmosdb_account_cors_rules = {} + cosmosdb_account_default_identity_type = null + cosmosdb_account_geo_location = [ + { + location = var.location + failover_priority = 0 + zone_redundant = false + } + ] + cosmosdb_account_kind = "GlobalDocumentDB" + cosmosdb_account_mongo_server_version = null + cosmosdb_account_local_authentication_disabled = true + cosmosdb_account_partition_merge_enabled = false + diagnostics_configurations = var.diagnostics_configurations + subnet_id = var.subnet_id_consumption + connectivity_delay_in_seconds = var.connectivity_delay_in_seconds + private_endpoint_subresource_names = ["Sql"] + private_dns_zone_id_cosmos_sql = var.private_dns_zone_id_cosmos_sql + private_dns_zone_id_cosmos_mongodb = "" + private_dns_zone_id_cosmos_cassandra = "" + private_dns_zone_id_cosmos_gremlin = "" + private_dns_zone_id_cosmos_table = "" + private_dns_zone_id_cosmos_analytical = "" + private_dns_zone_id_cosmos_coordinator = "" + customer_managed_key = var.customer_managed_key +} diff --git a/modules/core/locals.tf b/modules/core/locals.tf index 5e0e11a1..518beec9 100644 --- a/modules/core/locals.tf +++ b/modules/core/locals.tf @@ -103,4 +103,7 @@ locals { group_id = "dfs" } } + + # Search service locals + search_service_shared_private_links = {} } diff --git a/modules/core/main.tf b/modules/core/main.tf index ac55b992..652f1f27 100644 --- a/modules/core/main.tf +++ b/modules/core/main.tf @@ -18,6 +18,14 @@ resource "azurerm_resource_group" "resource_group_consumption" { tags = var.tags } +resource "azurerm_resource_group" "resource_group_ai" { + count = var.ai_foundry_account_details.enabled ? 1 : 0 + + name = "${local.prefix}-ai-rg" + location = var.location + tags = var.tags +} + resource "azurerm_resource_group" "resource_group_fabric" { name = "${local.prefix}-fbrc-rg" location = var.location diff --git a/modules/core/outputs.tf b/modules/core/outputs.tf index 97c56de4..0e906a2f 100644 --- a/modules/core/outputs.tf +++ b/modules/core/outputs.tf @@ -52,3 +52,30 @@ output "fabric_capacity_name" { description = "Specifies the name of the Fabric capacity." value = try(reverse(split("/", one(module.fabric_capacity[*].fabric_capacity_id), "/"))[0], "") } + +# AI Foundry details +output "ai_foundry_account_details" { + description = "Specifies the ai foundry details of the account." + value = { + enabled = var.ai_foundry_account_details.enabled + ai_foundry_account = { + id = one(module.ai_foundry_account[*].ai_services_id) + } + search_service = { + id = one(module.ai_search[*].search_service_id) + target = "https://${one(module.ai_search[*].search_service_name)}.search.windows.net" + } + cosmos_db = { + id = one(module.cosmos_db[*].cosmosdb_account_id) + target = one(module.cosmos_db[*].cosmosdb_account_endpoint) + } + storage_account = { + id = one(module.storage_account_aifoundry[*].storage_account_id) + target = one(module.storage_account_aifoundry[*].storage_account_primary_blob_endpoint) + } + } + sensitive = false + depends_on = [ + # one(module.ai_foundry_account[*].ai_services_setup_completed), + ] +} diff --git a/modules/core/storage.tf b/modules/core/storage.tf index 93a591d3..3275e239 100644 --- a/modules/core/storage.tf +++ b/modules/core/storage.tf @@ -217,3 +217,49 @@ module "storage_account_workspace" { private_dns_zone_id_dfs = var.private_dns_zone_id_dfs customer_managed_key = var.customer_managed_key } + +module "storage_account_aifoundry" { + source = "github.com/PerfectThymeTech/terraform-azurerm-modules//modules/storage?ref=main" + providers = { + azurerm = azurerm + time = time + } + + count = var.ai_foundry_account_details.enabled ? 1 : 0 + + location = var.location + resource_group_name = one(azurerm_resource_group.resource_group_ai[*].name) + tags = var.tags + + storage_account_name = replace("${local.prefix}-stg-aif", "-", "") + storage_access_tier = "Hot" + storage_account_type = "StorageV2" + storage_account_tier = "Standard" + storage_account_replication_type = var.zone_redundancy_enabled ? "ZRS" : "LRS" + storage_blob_change_feed_enabled = false + storage_blob_container_delete_retention_in_days = 30 + storage_blob_delete_retention_in_days = 30 + storage_blob_cors_rules = {} + storage_blob_last_access_time_enabled = false + storage_blob_versioning_enabled = false + storage_is_hns_enabled = false + storage_network_bypass = ["AzureServices"] + storage_network_private_link_access = local.storage_network_private_link_access + storage_public_network_access_enabled = true + storage_nfsv3_enabled = false + storage_sftp_enabled = false + storage_shared_access_key_enabled = false + storage_container_names = [] + storage_static_website = [] + diagnostics_configurations = var.diagnostics_configurations + subnet_id = var.subnet_id_storage + connectivity_delay_in_seconds = var.connectivity_delay_in_seconds + private_endpoint_subresource_names = ["blob", ] + private_dns_zone_id_blob = var.private_dns_zone_id_blob + private_dns_zone_id_file = "" + private_dns_zone_id_table = "" + private_dns_zone_id_queue = "" + private_dns_zone_id_web = "" + private_dns_zone_id_dfs = "" + customer_managed_key = var.customer_managed_key +} diff --git a/modules/core/variables.tf b/modules/core/variables.tf index bc015352..595e2492 100644 --- a/modules/core/variables.tf +++ b/modules/core/variables.tf @@ -86,6 +86,25 @@ variable "fabric_capacity_details" { } } +variable "ai_foundry_account_details" { + description = "Specifies the ai foundry configuration." + type = object({ + enabled = optional(bool, false) + search_service = optional(object({ + sku = optional(string, "basic") + semantic_search_sku = optional(string, "standard") + partition_count = optional(number, 1) + replica_count = optional(number, 1) + }), {}) + cosmos_db = optional(object({ + consistency_level = optional(string, "Session") + }), {}) + }) + sensitive = false + nullable = false + default = {} +} + # HA/DR variables variable "zone_redundancy_enabled" { description = "Specifies whether zone-redundancy should be enabled for all resources." @@ -169,6 +188,16 @@ variable "subnet_id_consumption" { } } +variable "subnet_id_aifoundry" { + description = "Specifies the id of the ai foundry subnet used for the agent service." + type = string + sensitive = false + validation { + condition = var.subnet_id_aifoundry == "" || length(split("/", var.subnet_id_aifoundry)) == 11 + error_message = "Please specify a valid resource ID." + } +} + variable "subnet_id_engineering_private" { description = "Specifies the id of the private subnet used for the databricks workspace for engineering." type = string @@ -268,6 +297,61 @@ variable "private_dns_zone_id_databricks" { } } +variable "private_dns_zone_id_cognitive_account" { + description = "Specifies the resource ID of the private DNS zone for Azure Cognitive Services. Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_cognitive_account == "" || (length(split("/", var.private_dns_zone_id_cognitive_account)) == 9 && (endswith(var.private_dns_zone_id_cognitive_account, "privatelink.cognitiveservices.azure.com"))) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} + +variable "private_dns_zone_id_open_ai" { + description = "Specifies the resource ID of the private DNS zone for Azure Open AI. Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_open_ai == "" || (length(split("/", var.private_dns_zone_id_open_ai)) == 9 && (endswith(var.private_dns_zone_id_open_ai, "privatelink.openai.azure.com"))) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} + +variable "private_dns_zone_id_ai_services" { + description = "Specifies the resource ID of the private DNS zone for Azure Foundry (AI Services). Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_ai_services == "" || (length(split("/", var.private_dns_zone_id_ai_services)) == 9 && endswith(var.private_dns_zone_id_ai_services, "privatelink.services.ai.azure.com")) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} + +variable "private_dns_zone_id_search_service" { + description = "Specifies the resource ID of the private DNS zone for Azure Cognitive Search endpoints. Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_search_service == "" || (length(split("/", var.private_dns_zone_id_search_service)) == 9 && endswith(var.private_dns_zone_id_search_service, "privatelink.search.windows.net")) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} + +variable "private_dns_zone_id_cosmos_sql" { + description = "Specifies the resource ID of the private DNS zone for cosmos db sql. Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_cosmos_sql == "" || (length(split("/", var.private_dns_zone_id_cosmos_sql)) == 9 && endswith(var.private_dns_zone_id_cosmos_sql, "privatelink.documents.azure.com")) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} + # Customer-managed key variables variable "customer_managed_key" { description = "Specifies the customer managed key configurations." diff --git a/modules/dataapplication/aifoundry.tf b/modules/dataapplication/aifoundry.tf new file mode 100644 index 00000000..a26a95b0 --- /dev/null +++ b/modules/dataapplication/aifoundry.tf @@ -0,0 +1,24 @@ +resource "azapi_resource" "ai_foundry_project" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + type = "Microsoft.CognitiveServices/accounts/projects@2025-04-01-preview" + name = local.prefix + location = var.location + parent_id = var.ai_foundry_account_details.ai_foundry_account.id + identity { + type = "SystemAssigned" + } + + body = { + properties = { + description = "Azure AI Foundry Project - ${local.prefix}" + displayName = "${local.prefix} Project" + } + } + + response_export_values = ["properties.internalId"] + schema_validation_enabled = true + locks = [] + ignore_casing = false + ignore_missing_property = true +} diff --git a/modules/dataapplication/aifoundry_capabilityhost.tf b/modules/dataapplication/aifoundry_capabilityhost.tf new file mode 100644 index 00000000..08dad602 --- /dev/null +++ b/modules/dataapplication/aifoundry_capabilityhost.tf @@ -0,0 +1,32 @@ +resource "azapi_resource" "ai_foundry_project_capability_hosts" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + type = "Microsoft.CognitiveServices/accounts/projects/capabilityHosts@2025-04-01-preview" + name = "default-project-${local.prefix}" + parent_id = one(azapi_resource.ai_foundry_project[*].id) + + body = { + properties = { + aiServicesConnections = slice(local.ai_foundry_project_connection_openai_names, 0, 1) + capabilityHostKind = "Agents" + storageConnections = [one(azapi_resource.ai_foundry_project_connection_storage_account[*].name)] + threadStorageConnections = [one(azapi_resource.ai_foundry_project_connection_cosmosdb_account[*].name)] + vectorStoreConnections = [one(azapi_resource.ai_foundry_project_connection_search_service_account[*].name)] + } + } + + response_export_values = [] + schema_validation_enabled = false + locks = [] + ignore_casing = false + ignore_missing_property = true + + depends_on = [ + azurerm_role_assignment.role_assignment_storage_account_aifoundry_blob_data_contributor_ai_foundry_project, + azurerm_role_assignment.role_assignment_storage_account_aifoundry_blob_data_owner_ai_foundry_project, + azurerm_role_assignment.role_assignment_cosmosdb_account_operator_ai_foundry_project, + azurerm_role_assignment.role_assignment_search_service_account_search_index_data_contributor_ai_foundry_project, + azurerm_role_assignment.role_assignment_search_service_account_search_service_contributor_ai_foundry_project, + azurerm_role_assignment.role_assignment_ai_service_ai_foundry_project, + ] +} diff --git a/modules/dataapplication/aifoundry_connections.tf b/modules/dataapplication/aifoundry_connections.tf new file mode 100644 index 00000000..d9d38911 --- /dev/null +++ b/modules/dataapplication/aifoundry_connections.tf @@ -0,0 +1,359 @@ +# Cosmos DB connections +resource "azapi_resource" "ai_foundry_project_connection_cosmosdb_account" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview" + name = "cosmosdb-account-${local.prefix}" + parent_id = one(azapi_resource.ai_foundry_project[*].id) + + body = { + properties = { + authType = "AAD" + category = "CosmosDb" + error = null + expiryTime = null + isSharedToAll = false + metadata = { + ApiType = "Azure" + ResourceId = var.ai_foundry_account_details.cosmos_db.id + location = var.location + } + peRequirement = "NotRequired" + target = var.ai_foundry_account_details.cosmos_db.target + useWorkspaceManagedIdentity = true + } + } + + response_export_values = [] + schema_validation_enabled = false + locks = [] + ignore_casing = false + ignore_missing_property = true +} + +# Search connections +resource "azapi_resource" "ai_foundry_project_connection_search_service_account" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview" + name = "cognitivesearch-account-${local.prefix}" + parent_id = one(azapi_resource.ai_foundry_project[*].id) + + body = { + properties = { + authType = "AAD" + category = "CognitiveSearch" + error = null + expiryTime = null + isSharedToAll = false + metadata = { + ApiType = "Azure" + ResourceId = var.ai_foundry_account_details.search_service.id + location = var.location + } + peRequirement = "NotRequired" + target = var.ai_foundry_account_details.search_service.target + useWorkspaceManagedIdentity = true + } + } + + response_export_values = [] + schema_validation_enabled = false + locks = [] + ignore_casing = false + ignore_missing_property = true +} + +resource "azapi_resource" "ai_foundry_project_connection_search_service" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled && var.search_service_details.enabled ? 1 : 0 + + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview" + name = "cognitivesearch-${local.prefix}" + parent_id = one(azapi_resource.ai_foundry_project[*].id) + + body = { + properties = { + authType = "AAD" + category = "CognitiveSearch" + error = null + expiryTime = null + isSharedToAll = false + metadata = { + ApiType = "Azure" + ResourceId = one(module.ai_search[*].search_service_id) + location = var.location + } + peRequirement = "NotRequired" + target = "https://${one(module.ai_search[*].search_service_name)}.search.windows.net" + useWorkspaceManagedIdentity = true + } + } + + response_export_values = [] + schema_validation_enabled = false + locks = [] + ignore_casing = false + ignore_missing_property = true +} + +# Open AI connections +resource "azapi_resource" "ai_foundry_project_connection_openai" { + for_each = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? local.ai_foundry_project_connection_openai : {} + + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview" + name = "openai-${local.prefix}-${each.key}" + parent_id = one(azapi_resource.ai_foundry_project[*].id) + + body = { + properties = { + authType = "AAD" + category = "AzureOpenAI" + error = null + expiryTime = null + isSharedToAll = false + metadata = { + ApiType = "Azure" + ResourceId = each.value.id + location = each.value.location + } + peRequirement = "NotRequired" + target = each.value.target + useWorkspaceManagedIdentity = true + } + } + + response_export_values = [] + schema_validation_enabled = false + locks = [] + ignore_casing = false + ignore_missing_property = true +} + +# Application Insights connections +resource "azapi_resource" "ai_foundry_project_connection_appinsights" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview" + name = "appinsights-${local.prefix}" + parent_id = one(azapi_resource.ai_foundry_project[*].id) + + body = { + properties = { + authType = "ApiKey" + category = "AppInsights" + credentials = { + key = module.application_insights.application_insights_connection_string + } + error = null + expiryTime = null + isSharedToAll = false + metadata = { + ApiType = "Azure" + ResourceId = module.application_insights.application_insights_id + location = var.location + } + peRequirement = "NotRequired" + target = module.application_insights.application_insights_id + useWorkspaceManagedIdentity = true + } + } + + response_export_values = [] + schema_validation_enabled = false + locks = [] + ignore_casing = false + ignore_missing_property = true +} + +# Storage connections +resource "azapi_resource" "ai_foundry_project_connection_storage_account" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview" + name = "azurestorageaccount-account-${local.prefix}" + parent_id = one(azapi_resource.ai_foundry_project[*].id) + + body = { + properties = { + authType = "AAD" + category = "AzureStorageAccount" + error = null + expiryTime = null + isSharedToAll = false + metadata = { + ApiType = "Azure" + ResourceId = var.ai_foundry_account_details.storage_account.id + location = var.location + } + peRequirement = "NotRequired" + target = var.ai_foundry_account_details.storage_account.target + useWorkspaceManagedIdentity = true + } + } + + response_export_values = [] + schema_validation_enabled = false + locks = [] + ignore_casing = false + ignore_missing_property = true +} + +resource "azapi_resource" "ai_foundry_project_connection_storage_provider" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview" + name = "azurestorageaccount-provider-${local.prefix}" + parent_id = one(azapi_resource.ai_foundry_project[*].id) + + body = { + properties = { + authType = "AAD" + category = "AzureStorageAccount" + error = null + expiryTime = null + isSharedToAll = false + metadata = { + ApiType = "Azure" + ResourceId = var.storage_account_ids.provider + location = var.location + } + peRequirement = "NotRequired" + target = "https://${reverse(split("/", var.storage_account_ids.provider))[0]}.blob.core.windows.net/" + useWorkspaceManagedIdentity = true + } + } + + response_export_values = [] + schema_validation_enabled = false + locks = [] + ignore_casing = false + ignore_missing_property = true +} + +resource "azapi_resource" "ai_foundry_project_connection_storage_raw" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview" + name = "azurestorageaccount-raw-${local.prefix}" + parent_id = one(azapi_resource.ai_foundry_project[*].id) + + body = { + properties = { + authType = "AAD" + category = "AzureStorageAccount" + error = null + expiryTime = null + isSharedToAll = false + metadata = { + ApiType = "Azure" + ResourceId = var.storage_account_ids.raw + location = var.location + } + peRequirement = "NotRequired" + target = "https://${reverse(split("/", var.storage_account_ids.raw))[0]}.blob.core.windows.net/" + useWorkspaceManagedIdentity = true + } + } + + response_export_values = [] + schema_validation_enabled = false + locks = [] + ignore_casing = false + ignore_missing_property = true +} + +resource "azapi_resource" "ai_foundry_project_connection_storage_enriched" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview" + name = "azurestorageaccount-enriched-${local.prefix}" + parent_id = one(azapi_resource.ai_foundry_project[*].id) + + body = { + properties = { + authType = "AAD" + category = "AzureStorageAccount" + error = null + expiryTime = null + isSharedToAll = false + metadata = { + ApiType = "Azure" + ResourceId = var.storage_account_ids.enriched + location = var.location + } + peRequirement = "NotRequired" + target = "https://${reverse(split("/", var.storage_account_ids.enriched))[0]}.blob.core.windows.net/" + useWorkspaceManagedIdentity = true + } + } + + response_export_values = [] + schema_validation_enabled = false + locks = [] + ignore_casing = false + ignore_missing_property = true +} + +resource "azapi_resource" "ai_foundry_project_connection_storage_curated" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview" + name = "azurestorageaccount-curated-${local.prefix}" + parent_id = one(azapi_resource.ai_foundry_project[*].id) + + body = { + properties = { + authType = "AAD" + category = "AzureStorageAccount" + error = null + expiryTime = null + isSharedToAll = false + metadata = { + ApiType = "Azure" + ResourceId = var.storage_account_ids.curated + location = var.location + } + peRequirement = "NotRequired" + target = "https://${reverse(split("/", var.storage_account_ids.curated))[0]}.blob.core.windows.net/" + useWorkspaceManagedIdentity = true + } + } + + response_export_values = [] + schema_validation_enabled = false + locks = [] + ignore_casing = false + ignore_missing_property = true +} + +resource "azapi_resource" "ai_foundry_project_connection_storage_workspace" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + type = "Microsoft.CognitiveServices/accounts/projects/connections@2025-04-01-preview" + name = "azurestorageaccount-workspace-${local.prefix}" + parent_id = one(azapi_resource.ai_foundry_project[*].id) + + body = { + properties = { + authType = "AAD" + category = "AzureStorageAccount" + error = null + expiryTime = null + isSharedToAll = false + metadata = { + ApiType = "Azure" + ResourceId = var.storage_account_ids.workspace + location = var.location + } + peRequirement = "NotRequired" + target = "https://${reverse(split("/", var.storage_account_ids.workspace))[0]}.blob.core.windows.net/" + useWorkspaceManagedIdentity = true + } + } + + response_export_values = [] + schema_validation_enabled = false + locks = [] + ignore_casing = false + ignore_missing_property = true +} diff --git a/modules/dataapplication/applicationinsights.tf b/modules/dataapplication/applicationinsights.tf new file mode 100644 index 00000000..dacdba4a --- /dev/null +++ b/modules/dataapplication/applicationinsights.tf @@ -0,0 +1,14 @@ +module "application_insights" { + source = "github.com/PerfectThymeTech/terraform-azurerm-modules//modules/applicationinsights?ref=main" + providers = { + azurerm = azurerm + } + + location = var.location + resource_group_name = azurerm_resource_group.resource_group_app_monitoring.name + tags = local.tags + application_insights_name = "${local.prefix}-ai001" + application_insights_application_type = "web" + application_insights_log_analytics_workspace_id = var.log_analytics_workspace_id + diagnostics_configurations = [] +} diff --git a/modules/dataapplication/data.tf b/modules/dataapplication/data.tf index 4f014d3d..41a74e92 100644 --- a/modules/dataapplication/data.tf +++ b/modules/dataapplication/data.tf @@ -74,3 +74,11 @@ data "azuread_service_principal" "service_principal_data_provider" { display_name = each.value.service_principal_name } + +data "azurerm_cosmosdb_sql_role_definition" "cosmosdb_sql_role_definition" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + resource_group_name = split("/", var.ai_foundry_account_details.cosmos_db.id)[4] + account_name = reverse(split("/", var.ai_foundry_account_details.cosmos_db.id))[0] + role_definition_id = "00000000-0000-0000-0000-000000000002" +} diff --git a/modules/dataapplication/locals.tf b/modules/dataapplication/locals.tf index c98b56ec..3b6ab822 100644 --- a/modules/dataapplication/locals.tf +++ b/modules/dataapplication/locals.tf @@ -174,4 +174,24 @@ locals { local.search_service_shared_default_private_links, local.search_service_shared_ai_service_private_links ) + + # AI Foundry locals + ai_foundry_project_internal_id = one(azapi_resource.ai_foundry_project[*].output.properties.internalId) + ai_foundry_project_workspace_id = "${substr(local.ai_foundry_project_internal_id, 0, 8)}-${substr(local.ai_foundry_project_internal_id, 8, 4)}-${substr(local.ai_foundry_project_internal_id, 12, 4)}-${substr(local.ai_foundry_project_internal_id, 16, 4)}-${substr(local.ai_foundry_project_internal_id, 20, 12)}" + ai_foundry_project_connection_openai = { + for key, value in var.ai_services : + key => { + id = module.ai_service[key].cognitive_account_id + target = module.ai_service[key].cognitive_account_endpoint + location = value.location + } if value.kind == "OpenAI" + } + ai_foundry_project_connection_openai_names = [ + for key, value in local.ai_foundry_project_connection_openai : + azapi_resource.ai_foundry_project_connection_openai[key].name + ] + cosmosdb_account_database_name = "enterprise_memory" + cosmosdb_account_database_container_thread_message_name = "thread-message-store" + cosmosdb_account_database_container_system_thread_message_name = "system-thread-message-store" + cosmosdb_account_database_container_agent_entity_store_name = "agent-entity-store" } diff --git a/modules/dataapplication/roleassignments_admin.tf b/modules/dataapplication/roleassignments_admin.tf index cfe5d6a4..2421543c 100644 --- a/modules/dataapplication/roleassignments_admin.tf +++ b/modules/dataapplication/roleassignments_admin.tf @@ -90,6 +90,27 @@ resource "azurerm_role_assignment" "role_assignment_search_service_contributor_a principal_type = "Group" } +# AI Foundry role assignments +resource "azurerm_role_assignment" "role_assignment_ai_foundry_account_reader_admin" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + description = "Role assignment to ai foundry account." + scope = var.ai_foundry_account_details.ai_foundry_account.id + role_definition_name = "Reader" + principal_id = data.azuread_group.group_admin.object_id + principal_type = "Group" +} + +resource "azurerm_role_assignment" "role_assignment_ai_foundry_project_manager_admin" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + description = "Role assignment to ai foundry project." + scope = one(azapi_resource.ai_foundry_project[*].id) + role_definition_name = "Azure AI Project Manager" + principal_id = data.azuread_group.group_admin.object_id + principal_type = "Group" +} + # Data factory role assignments resource "azurerm_role_assignment" "role_assignment_data_factory_data_factory_contributor_admin" { count = var.data_factory_details.enabled ? 1 : 0 diff --git a/modules/dataapplication/roleassignments_developer.tf b/modules/dataapplication/roleassignments_developer.tf index ca40455e..c682a018 100644 --- a/modules/dataapplication/roleassignments_developer.tf +++ b/modules/dataapplication/roleassignments_developer.tf @@ -94,6 +94,27 @@ resource "azurerm_role_assignment" "role_assignment_search_service_contributor_d principal_type = "Group" } +# AI Foundry role assignments +resource "azurerm_role_assignment" "role_assignment_ai_foundry_account_reader_developer" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled && var.developer_group_name != "" ? 1 : 0 + + description = "Role assignment to ai foundry account." + scope = var.ai_foundry_account_details.ai_foundry_account.id + role_definition_name = "Reader" + principal_id = one(data.azuread_group.group_developer[*].object_id) + principal_type = "Group" +} + +resource "azurerm_role_assignment" "role_assignment_ai_foundry_project_manager_developer" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled && var.developer_group_name != "" ? 1 : 0 + + description = "Role assignment to ai foundry project." + scope = one(azapi_resource.ai_foundry_project[*].id) + role_definition_name = "Azure AI Project Manager" + principal_id = one(data.azuread_group.group_developer[*].object_id) + principal_type = "Group" +} + # Data factory role assignments resource "azurerm_role_assignment" "role_assignment_data_factory_data_factory_contributor_developer" { count = var.developer_group_name != "" && var.data_factory_details.enabled ? 1 : 0 diff --git a/modules/dataapplication/roleassignments_msi_aifoundry.tf b/modules/dataapplication/roleassignments_msi_aifoundry.tf new file mode 100644 index 00000000..a1fd1f12 --- /dev/null +++ b/modules/dataapplication/roleassignments_msi_aifoundry.tf @@ -0,0 +1,199 @@ +# Capability Host Role Assignments +resource "azurerm_role_assignment" "role_assignment_storage_account_aifoundry_blob_data_contributor_ai_foundry_project" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + description = "Role assignment for storage write operations." + scope = var.ai_foundry_account_details.storage_account.id + role_definition_name = "Storage Blob Data Contributor" + principal_id = one(azapi_resource.ai_foundry_project[*].identity[0].principal_id) + principal_type = "ServicePrincipal" +} + +resource "azurerm_role_assignment" "role_assignment_storage_account_aifoundry_blob_data_owner_ai_foundry_project" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + description = "Role assignment for storage write operations." + scope = var.ai_foundry_account_details.storage_account.id + role_definition_name = "Storage Blob Data Owner" + principal_id = one(azapi_resource.ai_foundry_project[*].identity[0].principal_id) + principal_type = "ServicePrincipal" + condition_version = "2.0" + condition = <<-EOT + ( + ( + !(ActionMatches{'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/tags/read'}) + AND + !(ActionMatches{'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/filter/action'}) + AND + !(ActionMatches{'Microsoft.Storage/storageAccounts/blobServices/containers/blobs/tags/write'}) + ) + OR + ( + @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringStartsWithIgnoreCase '${local.ai_foundry_project_workspace_id}' + AND + @Resource[Microsoft.Storage/storageAccounts/blobServices/containers:name] StringLikeIgnoreCase '*-azureml-agent' + ) + ) + EOT +} + +resource "azurerm_role_assignment" "role_assignment_cosmosdb_account_operator_ai_foundry_project" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + description = "Role assignment for cosmos write operations." + scope = var.ai_foundry_account_details.cosmos_db.id + role_definition_name = "Cosmos DB Operator" + principal_id = one(azapi_resource.ai_foundry_project[*].identity[0].principal_id) + principal_type = "ServicePrincipal" +} + +resource "azurerm_cosmosdb_sql_role_assignment" "cosmosdb_sql_role_assignment_thread_message_store_ai_foundry_project" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + resource_group_name = split("/", var.ai_foundry_account_details.cosmos_db.id)[4] + account_name = reverse(split("/", var.ai_foundry_account_details.cosmos_db.id))[0] + role_definition_id = one(data.azurerm_cosmosdb_sql_role_definition.cosmosdb_sql_role_definition[*].id) + principal_id = one(azapi_resource.ai_foundry_project[*].identity[0].principal_id) + scope = "${var.ai_foundry_account_details.cosmos_db.id}/dbs/${local.cosmosdb_account_database_name}/colls/${local.ai_foundry_project_workspace_id}-${local.cosmosdb_account_database_container_thread_message_name}" + + depends_on = [ + azapi_resource.ai_foundry_project_capability_hosts, + ] +} + +resource "azurerm_cosmosdb_sql_role_assignment" "cosmosdb_sql_role_assignment_system_thread_message_store_ai_foundry_project" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + resource_group_name = split("/", var.ai_foundry_account_details.cosmos_db.id)[4] + account_name = reverse(split("/", var.ai_foundry_account_details.cosmos_db.id))[0] + role_definition_id = one(data.azurerm_cosmosdb_sql_role_definition.cosmosdb_sql_role_definition[*].id) + principal_id = one(azapi_resource.ai_foundry_project[*].identity[0].principal_id) + scope = "${var.ai_foundry_account_details.cosmos_db.id}/dbs/${local.cosmosdb_account_database_name}/colls/${local.ai_foundry_project_workspace_id}-${local.cosmosdb_account_database_container_system_thread_message_name}" + + depends_on = [ + azapi_resource.ai_foundry_project_capability_hosts, + ] +} + +resource "azurerm_cosmosdb_sql_role_assignment" "cosmosdb_sql_role_assignment_agent_entity_store_ai_foundry_project" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + resource_group_name = split("/", var.ai_foundry_account_details.cosmos_db.id)[4] + account_name = reverse(split("/", var.ai_foundry_account_details.cosmos_db.id))[0] + role_definition_id = one(data.azurerm_cosmosdb_sql_role_definition.cosmosdb_sql_role_definition[*].id) + principal_id = one(azapi_resource.ai_foundry_project[*].identity[0].principal_id) + scope = "${var.ai_foundry_account_details.cosmos_db.id}/dbs/${local.cosmosdb_account_database_name}/colls/${local.ai_foundry_project_workspace_id}-${local.cosmosdb_account_database_container_agent_entity_store_name}" + + depends_on = [ + azapi_resource.ai_foundry_project_capability_hosts, + ] +} + +resource "azurerm_role_assignment" "role_assignment_search_service_account_search_index_data_contributor_ai_foundry_project" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + description = "Role assignment for ai search write operations." + scope = var.ai_foundry_account_details.search_service.id + role_definition_name = "Search Index Data Contributor" + principal_id = one(azapi_resource.ai_foundry_project[*].identity[0].principal_id) + principal_type = "ServicePrincipal" +} + +resource "azurerm_role_assignment" "role_assignment_search_service_account_search_service_contributor_ai_foundry_project" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + description = "Role assignment for ai search write operations." + scope = var.ai_foundry_account_details.search_service.id + role_definition_name = "Search Service Contributor" + principal_id = one(azapi_resource.ai_foundry_project[*].identity[0].principal_id) + principal_type = "ServicePrincipal" +} + +# Resource group role assignments + +# Key vault role assignments + +# Databricks role assignments + +# AI service role assignments +resource "azurerm_role_assignment" "role_assignment_ai_service_ai_foundry_project" { + for_each = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? var.ai_services : {} + + description = "Role assignment to the ai services." + scope = module.ai_service[each.key].cognitive_account_id + role_definition_name = local.ai_service_kind_role_map_write[each.value.kind] + principal_id = one(azapi_resource.ai_foundry_project[*].identity[0].principal_id) + principal_type = "ServicePrincipal" +} + +# AI search service role assignment +resource "azurerm_role_assignment" "role_assignment_search_service_index_data_contributor_ai_foundry_project" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled && var.search_service_details.enabled ? 1 : 0 + + description = "Role assignment to create or manage objects in AI Search." + scope = one(module.ai_search[*].search_service_id) + role_definition_name = "Search Index Data Contributor" + principal_id = one(azapi_resource.ai_foundry_project[*].identity[0].principal_id) + principal_type = "ServicePrincipal" +} + +resource "azurerm_role_assignment" "role_assignment_search_service_contributor_ai_foundry_project" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled && var.search_service_details.enabled ? 1 : 0 + + description = "Role assignment to load documents and run indexing jobs in AI Search." + scope = one(module.ai_search[*].search_service_id) + role_definition_name = "Search Service Contributor" + principal_id = one(azapi_resource.ai_foundry_project[*].identity[0].principal_id) + principal_type = "ServicePrincipal" +} + +# Storage Role Assignments +resource "azurerm_role_assignment" "role_assignment_storage_container_provider_blob_data_contributor_ai_foundry_project" { + for_each = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? var.data_provider_details : {} + + description = "Role assignment to provider storage account container to read and write data." + scope = azurerm_storage_container.storage_container_provider[each.key].id + role_definition_name = "Storage Blob Data Contributor" + principal_id = one(azapi_resource.ai_foundry_project[*].identity[0].principal_id) + principal_type = "ServicePrincipal" +} + +resource "azurerm_role_assignment" "role_assignment_storage_container_raw_blob_data_contributor_ai_foundry_project" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + description = "Role assignment to raw storage account container to read and write data." + scope = azurerm_storage_container.storage_container_raw.id + role_definition_name = "Storage Blob Data Contributor" + principal_id = one(azapi_resource.ai_foundry_project[*].identity[0].principal_id) + principal_type = "ServicePrincipal" +} + +resource "azurerm_role_assignment" "role_assignment_storage_container_enriched_blob_data_contributor_ai_foundry_project" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + description = "Role assignment to enriched storage account container to read and write data." + scope = azurerm_storage_container.storage_container_enriched.id + role_definition_name = "Storage Blob Data Contributor" + principal_id = one(azapi_resource.ai_foundry_project[*].identity[0].principal_id) + principal_type = "ServicePrincipal" +} + +resource "azurerm_role_assignment" "role_assignment_storage_container_curated_blob_data_contributor_ai_foundry_project" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + description = "Role assignment to curated storage account container to read and write data." + scope = azurerm_storage_container.storage_container_curated.id + role_definition_name = "Storage Blob Data Contributor" + principal_id = one(azapi_resource.ai_foundry_project[*].identity[0].principal_id) + principal_type = "ServicePrincipal" +} + +resource "azurerm_role_assignment" "role_assignment_storage_container_workspace_blob_data_contributor_ai_foundry_project" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + description = "Role assignment to workspace storage account container to read and write data." + scope = azurerm_storage_container.storage_container_workspace.id + role_definition_name = "Storage Blob Data Contributor" + principal_id = one(azapi_resource.ai_foundry_project[*].identity[0].principal_id) + principal_type = "ServicePrincipal" +} diff --git a/modules/dataapplication/roleassignments_msi_databricksaccessconnector.tf b/modules/dataapplication/roleassignments_msi_databricksaccessconnector.tf index 70bb0d6d..d975b8f9 100644 --- a/modules/dataapplication/roleassignments_msi_databricksaccessconnector.tf +++ b/modules/dataapplication/roleassignments_msi_databricksaccessconnector.tf @@ -81,6 +81,27 @@ resource "azurerm_role_assignment" "role_assignment_search_service_contributor_a principal_type = "ServicePrincipal" } +# AI Foundry role assignments +resource "azurerm_role_assignment" "role_assignment_ai_foundry_account_reader_accessconnector" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + description = "Role assignment to ai foundry account." + scope = var.ai_foundry_account_details.ai_foundry_account.id + role_definition_name = "Reader" + principal_id = module.databricks_access_connector.databricks_access_connector_principal_id + principal_type = "ServicePrincipal" +} + +resource "azurerm_role_assignment" "role_assignment_ai_foundry_project_manager_accessconnector" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + description = "Role assignment to ai foundry project." + scope = one(azapi_resource.ai_foundry_project[*].id) + role_definition_name = "Azure AI Project Manager" + principal_id = module.databricks_access_connector.databricks_access_connector_principal_id + principal_type = "ServicePrincipal" +} + # Storage Role Assignments resource "azurerm_role_assignment" "role_assignment_storage_account_provider_event_subscription_contributor_accessconnector" { description = "Role assignment to provider storage account to create event triggers." diff --git a/modules/dataapplication/roleassignments_msi_datafactory.tf b/modules/dataapplication/roleassignments_msi_datafactory.tf index 3b393cf9..813fa86a 100644 --- a/modules/dataapplication/roleassignments_msi_datafactory.tf +++ b/modules/dataapplication/roleassignments_msi_datafactory.tf @@ -93,6 +93,27 @@ resource "azurerm_role_assignment" "role_assignment_search_service_contributor_d principal_type = "ServicePrincipal" } +# AI Foundry role assignments +resource "azurerm_role_assignment" "role_assignment_ai_foundry_account_reader_datafactory" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled && var.data_factory_details.enabled ? 1 : 0 + + description = "Role assignment to ai foundry account." + scope = var.ai_foundry_account_details.ai_foundry_account.id + role_definition_name = "Reader" + principal_id = one(module.data_factory[*].data_factory_principal_id) + principal_type = "ServicePrincipal" +} + +resource "azurerm_role_assignment" "role_assignment_ai_foundry_project_manager_datafactory" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled && var.data_factory_details.enabled ? 1 : 0 + + description = "Role assignment to ai foundry project." + scope = one(azapi_resource.ai_foundry_project[*].id) + role_definition_name = "Azure AI Project Manager" + principal_id = one(module.data_factory[*].data_factory_principal_id) + principal_type = "ServicePrincipal" +} + # Storage Role Assignments resource "azurerm_role_assignment" "role_assignment_storage_account_provider_event_subscription_contributor_datafactory" { count = var.data_factory_details.enabled ? 1 : 0 diff --git a/modules/dataapplication/roleassignments_reader.tf b/modules/dataapplication/roleassignments_reader.tf index dc1ea6ec..7646c60e 100644 --- a/modules/dataapplication/roleassignments_reader.tf +++ b/modules/dataapplication/roleassignments_reader.tf @@ -57,6 +57,27 @@ resource "azurerm_role_assignment" "role_assignment_databricks_workspace_reader_ # AI search service role assignment +# AI Foundry role assignments +resource "azurerm_role_assignment" "role_assignment_ai_foundry_account_reader_reader" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled && var.reader_group_name != "" ? 1 : 0 + + description = "Role assignment to ai foundry account." + scope = var.ai_foundry_account_details.ai_foundry_account.id + role_definition_name = "Reader" + principal_id = one(data.azuread_group.group_reader[*].object_id) + principal_type = "Group" +} + +resource "azurerm_role_assignment" "role_assignment_ai_foundry_project_reader_reader" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled && var.reader_group_name != "" ? 1 : 0 + + description = "Role assignment to ai foundry project." + scope = one(azapi_resource.ai_foundry_project[*].id) + role_definition_name = "Reader" + principal_id = one(data.azuread_group.group_reader[*].object_id) + principal_type = "Group" +} + # Data factory role assignments # Fabric role assignments diff --git a/modules/dataapplication/roleassignments_service_principal.tf b/modules/dataapplication/roleassignments_service_principal.tf index 9ca3c194..b0014da2 100644 --- a/modules/dataapplication/roleassignments_service_principal.tf +++ b/modules/dataapplication/roleassignments_service_principal.tf @@ -93,6 +93,27 @@ resource "azurerm_role_assignment" "role_assignment_search_service_contributor_s principal_type = "ServicePrincipal" } +# AI Foundry role assignments +resource "azurerm_role_assignment" "role_assignment_ai_foundry_account_reader_service_principal" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled && var.service_principal_name != "" ? 1 : 0 + + description = "Role assignment to ai foundry account." + scope = var.ai_foundry_account_details.ai_foundry_account.id + role_definition_name = "Reader" + principal_id = one(data.azuread_service_principal.service_principal[*].object_id) + principal_type = "ServicePrincipal" +} + +resource "azurerm_role_assignment" "role_assignment_ai_foundry_project_manager_service_principal" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled && var.service_principal_name != "" ? 1 : 0 + + description = "Role assignment to ai foundry project." + scope = one(azapi_resource.ai_foundry_project[*].id) + role_definition_name = "Azure AI Project Manager" + principal_id = one(data.azuread_service_principal.service_principal[*].object_id) + principal_type = "ServicePrincipal" +} + # Data factory role assignments resource "azurerm_role_assignment" "role_assignment_data_factory_data_factory_contributor_service_principal" { count = var.service_principal_name != "" && var.data_factory_details.enabled ? 1 : 0 diff --git a/modules/dataapplication/roleassignments_uai.tf b/modules/dataapplication/roleassignments_uai.tf index e73c760e..d4aa2b36 100644 --- a/modules/dataapplication/roleassignments_uai.tf +++ b/modules/dataapplication/roleassignments_uai.tf @@ -81,6 +81,27 @@ resource "azurerm_role_assignment" "role_assignment_search_service_contributor_u principal_type = "ServicePrincipal" } +# AI Foundry role assignments +resource "azurerm_role_assignment" "role_assignment_ai_foundry_account_reader_uai" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + description = "Role assignment to ai foundry account." + scope = var.ai_foundry_account_details.ai_foundry_account.id + role_definition_name = "Reader" + principal_id = module.user_assigned_identity.user_assigned_identity_principal_id + principal_type = "ServicePrincipal" +} + +resource "azurerm_role_assignment" "role_assignment_ai_foundry_project_manager_uai" { + count = var.ai_foundry_project_details.enabled && var.ai_foundry_account_details.enabled ? 1 : 0 + + description = "Role assignment to ai foundry project." + scope = one(azapi_resource.ai_foundry_project[*].id) + role_definition_name = "Azure AI Project Manager" + principal_id = module.user_assigned_identity.user_assigned_identity_principal_id + principal_type = "ServicePrincipal" +} + # Data factory role assignments resource "azurerm_role_assignment" "role_assignment_data_factory_data_factory_contributor_uai" { count = var.data_factory_details.enabled ? 1 : 0 diff --git a/modules/dataapplication/variables.tf b/modules/dataapplication/variables.tf index 0610fc4a..b84b4d29 100644 --- a/modules/dataapplication/variables.tf +++ b/modules/dataapplication/variables.tf @@ -164,7 +164,7 @@ variable "search_service_details" { sku = optional(string, "standard") semantic_search_sku = optional(string, "standard") partition_count = optional(number, 1) - replica_count = optional(string, 1) + replica_count = optional(number, 1) }) sensitive = false nullable = false @@ -207,6 +207,42 @@ variable "fabric_workspace_details" { default = {} } +variable "ai_foundry_account_details" { + description = "Specifies the ai foundry account details." + type = object({ + enabled = optional(bool, false) + ai_foundry_account = optional(object({ + id = optional(string, "") + }), {}) + search_service = optional(object({ + id = optional(string, "") + target = optional(string, "") + }), {}) + cosmos_db = optional(object({ + id = optional(string, "") + target = optional(string, "") + }), {}) + storage_account = optional(object({ + id = optional(string, "") + target = optional(string, "") + }), {}) + }) + sensitive = false + nullable = false + default = {} +} + +variable "ai_foundry_project_details" { + description = "Specifies the ai foundry project details." + type = object({ + enabled = optional(bool, true) + # tbd + }) + sensitive = false + nullable = false + default = {} +} + variable "storage_dependencies" { description = "Specifies a list of dependencies for storage resources." type = list(bool) @@ -224,6 +260,17 @@ variable "zone_redundancy_enabled" { } # Logging and monitoring variables +variable "log_analytics_workspace_id" { + description = "Specifies the resource ID of a log analytics workspace for all diagnostic logs." + type = string + sensitive = false + default = "" + validation { + condition = length(split("/", var.log_analytics_workspace_id)) == 9 || var.log_analytics_workspace_id == "" + error_message = "Please specify a valid resource ID." + } +} + variable "diagnostics_configurations" { description = "Specifies the diagnostic configuration for the service." type = list(object({ diff --git a/modules/databrickscore/locals.tf b/modules/databrickscore/locals.tf index 6b54f9a8..ebfe5425 100644 --- a/modules/databrickscore/locals.tf +++ b/modules/databrickscore/locals.tf @@ -2,15 +2,15 @@ locals { # General locals prefix = "${lower(var.prefix)}-${var.environment}-core" system_schema_names = [ - "access", - # "billing", # billing system schema can only be enabled by Databricks - "compute", - "lakeflow", + # "access", # access system schema is automatically enabled by Databricks + # "billing", # billing system schema is automatically enabled by Databricks + # "compute", # compute system schema is automatically enabled by Databricks + # "lakeflow", # lakeflow system schema is automatically enabled by Databricks # "lineage", # lineage system schema can only be enabled by Databricks - "marketplace", + # "marketplace", # marketplace system schema can only be enabled by Databricks # "query", # query system schema can only be enabled by Databricks - "serving", - "storage", + # "serving", # serving system schema is automatically enabled by Databricks + # "storage", # storage system schema is automatically enabled by Databricks ] # Databricks locals diff --git a/modules/databricksdataapplication/externallocation.tf b/modules/databricksdataapplication/externallocation.tf index d392ad43..683bce60 100644 --- a/modules/databricksdataapplication/externallocation.tf +++ b/modules/databricksdataapplication/externallocation.tf @@ -69,3 +69,20 @@ resource "databricks_external_location" "external_location_workspace" { skip_validation = false url = "abfss://${local.storage_container_workspace.storage_container_name}@${local.storage_container_workspace.storage_account_name}.dfs.core.windows.net/" } + +resource "databricks_workspace_binding" "workspace_binding_external_location_provider" { + for_each = merge([ + for key, value in var.data_provider_details : { + for item in value.databricks_catalog.workspace_binding_catalog : + "${key}-${item}" => { + key = key + workspace_binding_catalog_workspace_id = item + } if value.databricks_catalog.enabled + } + ]...) + + binding_type = "BINDING_TYPE_READ_WRITE" + securable_name = databricks_external_location.external_location_provider[each.value.key].name + securable_type = "external_location" + workspace_id = each.value.workspace_binding_catalog_workspace_id +} diff --git a/modules/databricksdataapplication/roleassignment_admin.tf b/modules/databricksdataapplication/roleassignment_admin.tf index 1032b694..6e9c03e6 100644 --- a/modules/databricksdataapplication/roleassignment_admin.tf +++ b/modules/databricksdataapplication/roleassignment_admin.tf @@ -33,7 +33,7 @@ resource "databricks_grant" "grant_catalog_provider_admin" { # Metadata "BROWSE", - # "APPLY_TAG", # Only allow system assigned tags at catalog level + "APPLY_TAG", # Read "EXECUTE", @@ -74,7 +74,7 @@ resource "databricks_grant" "grant_catalog_internal_admin" { # Metadata "BROWSE", - # "APPLY_TAG", # Only allow system assigned tags at catalog level + "APPLY_TAG", # Read "EXECUTE", @@ -115,7 +115,7 @@ resource "databricks_grant" "grant_catalog_published_admin" { # Metadata "BROWSE", - # "APPLY_TAG", # Only allow system assigned tags at catalog level + "APPLY_TAG", # Read "EXECUTE", diff --git a/modules/databricksdataapplication/roleassignment_developer.tf b/modules/databricksdataapplication/roleassignment_developer.tf index 35b45390..7d5939c7 100644 --- a/modules/databricksdataapplication/roleassignment_developer.tf +++ b/modules/databricksdataapplication/roleassignment_developer.tf @@ -37,7 +37,7 @@ resource "databricks_grant" "grant_catalog_provider_developer" { # Metadata "BROWSE", - # "APPLY_TAG", # Only allow system assigned tags at catalog level + "APPLY_TAG", # Read "EXECUTE", @@ -80,7 +80,7 @@ resource "databricks_grant" "grant_catalog_internal_developer" { # Metadata "BROWSE", - # "APPLY_TAG", # Only allow system assigned tags at catalog level + "APPLY_TAG", # Read "EXECUTE", @@ -123,7 +123,7 @@ resource "databricks_grant" "grant_catalog_published_developer" { # Metadata "BROWSE", - # "APPLY_TAG", # Only allow system assigned tags at catalog level + "APPLY_TAG", # Read "EXECUTE", diff --git a/modules/databricksdataapplication/roleassignment_service_principal.tf b/modules/databricksdataapplication/roleassignment_service_principal.tf index b798edfb..eb8e57ed 100644 --- a/modules/databricksdataapplication/roleassignment_service_principal.tf +++ b/modules/databricksdataapplication/roleassignment_service_principal.tf @@ -37,7 +37,7 @@ resource "databricks_grant" "grant_catalog_provider_service_principal" { # Metadata "BROWSE", - # "APPLY_TAG", # Only allow system assigned tags at catalog level + "APPLY_TAG", # Read "EXECUTE", @@ -80,7 +80,7 @@ resource "databricks_grant" "grant_catalog_internal_service_principal" { # Metadata "BROWSE", - # "APPLY_TAG", # Only allow system assigned tags at catalog level + "APPLY_TAG", # Read "EXECUTE", @@ -123,7 +123,7 @@ resource "databricks_grant" "grant_catalog_published_service_principal" { # Metadata "BROWSE", - # "APPLY_TAG", # Only allow system assigned tags at catalog level + "APPLY_TAG", # Read "EXECUTE", diff --git a/modules/databricksdataapplication/roleassignment_service_principal_datafactory.tf b/modules/databricksdataapplication/roleassignment_service_principal_datafactory.tf index faa85ed7..cb0d9da7 100644 --- a/modules/databricksdataapplication/roleassignment_service_principal_datafactory.tf +++ b/modules/databricksdataapplication/roleassignment_service_principal_datafactory.tf @@ -37,7 +37,7 @@ resource "databricks_grant" "grant_catalog_provider_service_principal_data_facto # Metadata "BROWSE", - # "APPLY_TAG", # Only allow system assigned tags at catalog level + "APPLY_TAG", # Read "EXECUTE", @@ -80,7 +80,7 @@ resource "databricks_grant" "grant_catalog_internal_service_principal_data_facto # Metadata "BROWSE", - # "APPLY_TAG", # Only allow system assigned tags at catalog level + "APPLY_TAG", # Read "EXECUTE", @@ -123,7 +123,7 @@ resource "databricks_grant" "grant_catalog_published_service_principal_data_fact # Metadata "BROWSE", - # "APPLY_TAG", # Only allow system assigned tags at catalog level + "APPLY_TAG", # Read "EXECUTE", diff --git a/modules/databricksdataapplication/roleassignment_service_principal_terraform_plan.tf b/modules/databricksdataapplication/roleassignment_service_principal_terraform_plan.tf index 8cc751ac..e257ec37 100644 --- a/modules/databricksdataapplication/roleassignment_service_principal_terraform_plan.tf +++ b/modules/databricksdataapplication/roleassignment_service_principal_terraform_plan.tf @@ -26,7 +26,7 @@ resource "databricks_grant" "grant_catalog_provider_service_principal_terraform_ # Metadata "BROWSE", - # "APPLY_TAG", # Only allow system assigned tags at catalog level + "APPLY_TAG", # Read # "EXECUTE", @@ -65,7 +65,7 @@ resource "databricks_grant" "grant_catalog_internal_service_principal_terraform_ # Metadata "BROWSE", - # "APPLY_TAG", # Only allow system assigned tags at catalog level + "APPLY_TAG", # Read # "EXECUTE", @@ -104,7 +104,7 @@ resource "databricks_grant" "grant_catalog_published_service_principal_terraform # Metadata "BROWSE", - # "APPLY_TAG", # Only allow system assigned tags at catalog level + "APPLY_TAG", # Read # "EXECUTE", @@ -134,7 +134,7 @@ resource "databricks_grant" "grant_external_location_provider_service_principal_ privileges = [ # General # "ALL_PRIVILIGES", # Use specific permissions instead of allowing all permissions by default - # "MANAGE", # Only allow system assigned permissions at catalog level and enforce permissions at lower levels + "MANAGE", # Required to read workspace binding # Metadata "BROWSE", diff --git a/modules/databricksdataapplication/roleassignment_uai.tf b/modules/databricksdataapplication/roleassignment_uai.tf index fc799748..62801dae 100644 --- a/modules/databricksdataapplication/roleassignment_uai.tf +++ b/modules/databricksdataapplication/roleassignment_uai.tf @@ -33,7 +33,7 @@ resource "databricks_grant" "grant_catalog_provider_uai" { # Metadata "BROWSE", - # "APPLY_TAG", # Only allow system assigned tags at catalog level + "APPLY_TAG", # Read "EXECUTE", @@ -74,7 +74,7 @@ resource "databricks_grant" "grant_catalog_internal_uai" { # Metadata "BROWSE", - # "APPLY_TAG", # Only allow system assigned tags at catalog level + "APPLY_TAG", # Read "EXECUTE", @@ -115,7 +115,7 @@ resource "databricks_grant" "grant_catalog_published_uai" { # Metadata "BROWSE", - # "APPLY_TAG", # Only allow system assigned tags at catalog level + "APPLY_TAG", # Read "EXECUTE", diff --git a/modules/databricksdataapplication/roleassignments_data_provider.tf b/modules/databricksdataapplication/roleassignments_data_provider.tf index d8d4c4eb..e0f0486b 100644 --- a/modules/databricksdataapplication/roleassignments_data_provider.tf +++ b/modules/databricksdataapplication/roleassignments_data_provider.tf @@ -10,7 +10,7 @@ resource "databricks_grant" "grant_catalog_provider_data_provider_service_princi ]...) catalog = databricks_catalog.catalog_provider[each.value.key].id - principal = data.databricks_service_principal.service_principal_data_provider[each.key].application_id # each.value.service_principal_name + principal = data.databricks_service_principal.service_principal_data_provider[each.key].application_id privileges = [ # General # "ALL_PRIVILIGES", # Use specific permissions instead of allowing all permissions by default @@ -23,7 +23,7 @@ resource "databricks_grant" "grant_catalog_provider_data_provider_service_princi # Metadata "BROWSE", - # "APPLY_TAG", # Only allow system assigned tags at catalog level + "APPLY_TAG", # Read "EXECUTE", @@ -45,6 +45,41 @@ resource "databricks_grant" "grant_catalog_provider_data_provider_service_princi ] } +resource "databricks_grant" "grant_external_location_provider_data_provider_service_principal" { + for_each = merge([ + for key, value in var.data_provider_details : { + for service_principal_name in value.service_principal_names : + "${key}-${service_principal_name}" => { + key = key + service_principal_name = service_principal_name + } if value.databricks_catalog.enabled + } + ]...) + + external_location = databricks_external_location.external_location_provider[each.value.key].id + principal = data.databricks_service_principal.service_principal_data_provider[each.key].application_id + privileges = [ + # General + # "ALL_PRIVILIGES", # Use specific permissions instead of allowing all permissions by default + # "MANAGE", # Only allow system assigned permissions at catalog level and enforce permissions at lower levels + + # Metadata + "BROWSE", + + # Read + "READ_FILES", + + # Edit + "WRITE_FILES", + + # Create + "CREATE_EXTERNAL_TABLE", + "CREATE_EXTERNAL_VOLUME", + "CREATE_FOREIGN_SECURABLE", + "CREATE_MANAGED_STORAGE", + ] +} + resource "databricks_grant" "grant_catalog_provider_data_provider_group" { for_each = merge([ for key, value in var.data_provider_details : { @@ -57,7 +92,7 @@ resource "databricks_grant" "grant_catalog_provider_data_provider_group" { ]...) catalog = databricks_catalog.catalog_provider[each.value.key].id - principal = data.databricks_group.group_data_provider[each.key].display_name # each.value.group_name + principal = data.databricks_group.group_data_provider[each.key].display_name privileges = [ # General # "ALL_PRIVILIGES", # Use specific permissions instead of allowing all permissions by default @@ -70,7 +105,7 @@ resource "databricks_grant" "grant_catalog_provider_data_provider_group" { # Metadata "BROWSE", - # "APPLY_TAG", # Only allow system assigned tags at catalog level + "APPLY_TAG", # Read "EXECUTE", @@ -91,3 +126,38 @@ resource "databricks_grant" "grant_catalog_provider_data_provider_group" { "CREATE_VOLUME", ] } + +resource "databricks_grant" "grant_external_location_provider_data_provider_group" { + for_each = merge([ + for key, value in var.data_provider_details : { + for group_name in value.group_names : + "${key}-${group_name}" => { + key = key + group_name = group_name + } if value.databricks_catalog.enabled + } + ]...) + + external_location = databricks_external_location.external_location_provider[each.value.key].id + principal = data.databricks_group.group_data_provider[each.key].display_name + privileges = [ + # General + # "ALL_PRIVILIGES", # Use specific permissions instead of allowing all permissions by default + # "MANAGE", # Only allow system assigned permissions at catalog level and enforce permissions at lower levels + + # Metadata + "BROWSE", + + # Read + "READ_FILES", + + # Edit + "WRITE_FILES", + + # Create + "CREATE_EXTERNAL_TABLE", + "CREATE_EXTERNAL_VOLUME", + "CREATE_FOREIGN_SECURABLE", + "CREATE_MANAGED_STORAGE", + ] +} diff --git a/modules/platform/locals.tf b/modules/platform/locals.tf index 261b2e29..107f5beb 100644 --- a/modules/platform/locals.tf +++ b/modules/platform/locals.tf @@ -81,6 +81,32 @@ locals { serviceEndpoints = [] } } + subnet_aifoundry = { + name = "AiFoundrySubnet" + properties = { + addressPrefix = var.subnet_cidr_range_aifoundry + defaultOutboundAccess = false + delegations = [ + { + name = "AppDelegation" + properties = { + serviceName = "Microsoft.App/environments" + } + } + ] + ipAllocations = [] + networkSecurityGroup = { + id = data.azurerm_network_security_group.network_security_group.id + } + privateEndpointNetworkPolicies = "Enabled" + privateLinkServiceNetworkPolicies = "Enabled" + routeTable = { + id = data.azurerm_route_table.route_table.id + } + serviceEndpointPolicies = [] + serviceEndpoints = [] + } + } subnet_engineering_private = { name = "EngineeringPrivateSubnet" properties = { @@ -208,3 +234,40 @@ locals { } ] } + +locals { + # Calculate subnet list + # Databricks Consumption enabled? + subnets_databricks_consumption = var.databricks_workspace_consumption_enabled ? flatten([ + [ + local.subnet_storage, + local.subnet_consumption, + local.subnet_fabric, + local.subnet_engineering_private, + local.subnet_engineering_public, + local.subnet_consumption_private, + local.subnet_consumption_public, + ], + local.subnets_private_endpoint_applications, + ]) : flatten([ + [ + local.subnet_storage, + local.subnet_consumption, + local.subnet_fabric, + local.subnet_engineering_private, + local.subnet_engineering_public, + ], + local.subnets_private_endpoint_applications, + ]) + + # AI Foundry enabled? + subnets_aifoundry = var.aifoundry_enabled ? concat( + local.subnets_databricks_consumption, + [ + local.subnet_aifoundry + ] + ) : local.subnets_databricks_consumption + + # Final subnet list + subnets = local.subnets_aifoundry +} diff --git a/modules/platform/network.tf b/modules/platform/network.tf index 2baa894b..19f34403 100644 --- a/modules/platform/network.tf +++ b/modules/platform/network.tf @@ -4,27 +4,7 @@ resource "azapi_update_resource" "virtual_network" { body = { properties = { - subnets = var.databricks_workspace_consumption_enabled ? flatten([ - [ - local.subnet_storage, - local.subnet_consumption, - local.subnet_fabric, - local.subnet_engineering_private, - local.subnet_engineering_public, - local.subnet_consumption_private, - local.subnet_consumption_public, - ], - local.subnets_private_endpoint_applications, - ]) : flatten([ - [ - local.subnet_storage, - local.subnet_consumption, - local.subnet_fabric, - local.subnet_engineering_private, - local.subnet_engineering_public, - ], - local.subnets_private_endpoint_applications, - ]) + subnets = local.subnets } } diff --git a/modules/platform/outputs.tf b/modules/platform/outputs.tf index 91cd9483..6465e969 100644 --- a/modules/platform/outputs.tf +++ b/modules/platform/outputs.tf @@ -16,6 +16,12 @@ output "subnet_id_fabric" { value = "${azapi_update_resource.virtual_network.id}/subnets/${local.subnet_fabric.name}" } +output "subnet_id_aifoundry" { + description = "Specifies the ai foundry subnet id." + sensitive = false + value = "${azapi_update_resource.virtual_network.id}/subnets/${local.subnet_aifoundry.name}" +} + output "subnet_id_engineering_private" { description = "Specifies the private consumption subnet id." sensitive = false diff --git a/modules/platform/variables.tf b/modules/platform/variables.tf index 19b1ef08..5d268713 100644 --- a/modules/platform/variables.tf +++ b/modules/platform/variables.tf @@ -94,6 +94,17 @@ variable "subnet_cidr_range_fabric" { } } +variable "subnet_cidr_range_aifoundry" { + description = "Specifies the cidr ranges of the ai foundry subnet used for the Data Landing Zone." + type = string + sensitive = false + default = "" + validation { + condition = var.subnet_cidr_range_aifoundry == "" || try(cidrnetmask(var.subnet_cidr_range_aifoundry), "invalid") != "invalid" + error_message = "Please specify a valid CIDR range for the ai foundry subnet." + } +} + variable "subnet_cidr_range_engineering_private" { description = "Specifies the cidr ranges of the engineering private subnet used for the Data Landing Zone." type = string @@ -157,3 +168,11 @@ variable "databricks_workspace_consumption_enabled" { nullable = false default = false } + +variable "aifoundry_enabled" { + description = "Specifies whether the ai foundry deployment is enabled." + type = bool + sensitive = false + nullable = false + default = false +} diff --git a/schemas/app.schema.json b/schemas/app.schema.json index 95e998ba..898e2e14 100644 --- a/schemas/app.schema.json +++ b/schemas/app.schema.json @@ -531,6 +531,18 @@ "required": [], "additionalProperties": false }, + "ai_foundry_project": { + "description": "Specifies the ai foundry project configuration for the app.", + "type": "object", + "properties": { + "enabled": { + "description": "Specifies whether ai foundry project should be enabled.", + "type": "boolean" + } + }, + "required": [], + "additionalProperties": false + }, "data_factory": { "description": "Specifies the data factory section of the application.", "type": "object", diff --git a/tests/e2e/data-applications/app001.yml b/tests/e2e/data-applications/app001.yml index 5c7b91a4..c16a6393 100644 --- a/tests/e2e/data-applications/app001.yml +++ b/tests/e2e/data-applications/app001.yml @@ -11,7 +11,7 @@ identity: network: private_endpoint_subnet: - cidr_range: "10.2.1.64/27" + cidr_range: "192.168.1.96/27" repository: type: github @@ -55,7 +55,7 @@ ai_services: kind: FormRecognizer sku: S0 aoai: - location: swedencentral + location: eastus2 kind: OpenAI sku: S0 @@ -66,9 +66,12 @@ ai_search: partition_count: 1 replica_count: 1 +ai_foundry_project: + enabled: true + databricks: sql_endpoints: - dw01: + se01: auto_stop_mins: 60 enable_serverless_compute: false cluster_size: "2X-Small" diff --git a/tests/e2e/local.tf b/tests/e2e/local.tf index b9e28f55..edffb1d7 100644 --- a/tests/e2e/local.tf +++ b/tests/e2e/local.tf @@ -2,9 +2,12 @@ locals { # General locals resource_providers_to_register = [ "Microsoft.Authorization", + "Microsoft.App", "Microsoft.CognitiveServices", + "Microsoft.ContainerService", "Microsoft.Databricks", "Microsoft.DataFactory", + "Microsoft.DocumentDB", "Microsoft.Insights", "Microsoft.KeyVault", "Microsoft.ManagedIdentity", diff --git a/tests/e2e/main.tf b/tests/e2e/main.tf index 0557a459..374e7c22 100644 --- a/tests/e2e/main.tf +++ b/tests/e2e/main.tf @@ -31,6 +31,7 @@ module "data_landing_zone" { databricks_compliance_security_profile_standards = var.databricks_compliance_security_profile_standards databricks_workspace_binding_catalog = var.databricks_workspace_binding_catalog fabric_capacity_details = var.fabric_capacity_details + ai_foundry_account_details = var.ai_foundry_account_details # HA/DR variables zone_redundancy_enabled = var.zone_redundancy_enabled @@ -56,8 +57,10 @@ module "data_landing_zone" { private_dns_zone_id_databricks = var.private_dns_zone_id_databricks private_dns_zone_id_cognitive_account = var.private_dns_zone_id_cognitive_account private_dns_zone_id_open_ai = var.private_dns_zone_id_open_ai + private_dns_zone_id_ai_services = var.private_dns_zone_id_ai_services private_dns_zone_id_data_factory = var.private_dns_zone_id_data_factory private_dns_zone_id_search_service = var.private_dns_zone_id_search_service + private_dns_zone_id_cosmos_sql = var.private_dns_zone_id_cosmos_sql # Customer-managed key variables customer_managed_key = var.customer_managed_key diff --git a/tests/e2e/providers.tf b/tests/e2e/providers.tf index 911c10ca..81e145e4 100644 --- a/tests/e2e/providers.tf +++ b/tests/e2e/providers.tf @@ -30,7 +30,7 @@ provider "azurerm" { permanently_delete_on_destroy = true } resource_group { - prevent_deletion_if_contains_resources = true + prevent_deletion_if_contains_resources = false } } } diff --git a/tests/e2e/terraform.tf b/tests/e2e/terraform.tf index 8826da04..1acb3662 100644 --- a/tests/e2e/terraform.tf +++ b/tests/e2e/terraform.tf @@ -16,11 +16,11 @@ terraform { } fabric = { source = "microsoft/fabric" - version = "1.2.0" + version = "1.3.0" } databricks = { source = "databricks/databricks" - version = "1.83.0" + version = "1.84.0" } time = { source = "hashicorp/time" diff --git a/tests/e2e/variables.tf b/tests/e2e/variables.tf index 72578287..29983d5e 100644 --- a/tests/e2e/variables.tf +++ b/tests/e2e/variables.tf @@ -169,6 +169,25 @@ variable "fabric_capacity_details" { } } +variable "ai_foundry_account_details" { + description = "Specifies the ai foundry configuration." + type = object({ + enabled = optional(bool, false) + search_service = optional(object({ + sku = optional(string, "basic") + semantic_search_sku = optional(string, "standard") + partition_count = optional(number, 1) + replica_count = optional(number, 1) + }), {}) + cosmos_db = optional(object({ + consistency_level = optional(string, "Session") + }), {}) + }) + sensitive = false + nullable = false + default = {} +} + # HA/DR variables variable "zone_redundancy_enabled" { description = "Specifies whether zone-redundancy should be enabled for all resources." @@ -254,6 +273,7 @@ variable "subnet_cidr_ranges" { storage_subnet = string consumption_subnet = string fabric_subnet = string + aifoundry_subnet = optional(string, "") databricks_engineering_private_subnet = string databricks_engineering_public_subnet = string databricks_consumption_private_subnet = optional(string, "") @@ -273,6 +293,10 @@ variable "subnet_cidr_ranges" { condition = try(cidrnetmask(var.subnet_cidr_ranges.fabric_subnet), "invalid") != "invalid" error_message = "Please specify a valid CIDR range for the fabric subnet." } + validation { + condition = var.subnet_cidr_ranges.fabric_subnet == "" || try(cidrnetmask(var.subnet_cidr_ranges.fabric_subnet), "invalid") != "invalid" + error_message = "Please specify a valid CIDR range for the ai foundry subnet." + } validation { condition = try(cidrnetmask(var.subnet_cidr_ranges.databricks_engineering_private_subnet), "invalid") != "invalid" error_message = "Please specify a valid CIDR range for the databricks engineering subnet." @@ -373,6 +397,17 @@ variable "private_dns_zone_id_open_ai" { } } +variable "private_dns_zone_id_ai_services" { + description = "Specifies the resource ID of the private DNS zone for Azure Foundry (AI Services). Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_ai_services == "" || (length(split("/", var.private_dns_zone_id_ai_services)) == 9 && endswith(var.private_dns_zone_id_ai_services, "privatelink.services.ai.azure.com")) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} + variable "private_dns_zone_id_data_factory" { description = "Specifies the resource ID of the private DNS zone for Azure Data Factory. Not required if DNS A-records get created via Azure Policy." type = string @@ -395,6 +430,17 @@ variable "private_dns_zone_id_search_service" { } } +variable "private_dns_zone_id_cosmos_sql" { + description = "Specifies the resource ID of the private DNS zone for cosmos db sql. Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_cosmos_sql == "" || (length(split("/", var.private_dns_zone_id_cosmos_sql)) == 9 && endswith(var.private_dns_zone_id_cosmos_sql, "privatelink.documents.azure.com")) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} + # Customer-managed key variables variable "customer_managed_key" { description = "Specifies the customer managed key configurations." diff --git a/tests/e2e/vars.tfvars b/tests/e2e/vars.tfvars index 60cf05bc..25cfa3ba 100644 --- a/tests/e2e/vars.tfvars +++ b/tests/e2e/vars.tfvars @@ -1,7 +1,7 @@ # General variables -location = "northeurope" +location = "eastus2" environment = "int" -prefix = "mydlz01" +prefix = "mydz01" tags = {} # Service @@ -12,7 +12,7 @@ data_application_file_variables = {} databricks_cluster_policy_library_path = "./databricks-cluster-policies" databricks_cluster_policy_file_variables = {} databricks_account_id = "515f13c1-53bb-48fb-a2c9-75e3f5d943f5" -databricks_network_connectivity_config_name = "ncc-northeurope-test" +databricks_network_connectivity_config_name = "ncc-eastus2-test" databricks_network_policy_details = { allowed_internet_destinations = [ { @@ -35,28 +35,32 @@ fabric_capacity_details = { admin_emails = [] sku = "F2" } +ai_foundry_account_details = { + enabled = true +} # HA/DR variables zone_redundancy_enabled = false # Logging variables -log_analytics_workspace_id = "" +log_analytics_workspace_id = "/subscriptions/e82c5267-9dc4-4f45-ac13-abdd5e130d27/resourceGroups/ptt-dev-logging-rg/providers/Microsoft.OperationalInsights/workspaces/ptt-dev-log001" # Identity variables service_principal_name_terraform_plan = "ptt-dev-uai001-dlz-tfplan" # Network variables -vnet_id = "/subscriptions/9842be63-c8c0-4647-a5d1-0c5e7f8bbb25/resourceGroups/ptt-dev-networking-rg/providers/Microsoft.Network/virtualNetworks/spoke-ptt-dev-vnet001" -nsg_id = "/subscriptions/9842be63-c8c0-4647-a5d1-0c5e7f8bbb25/resourceGroups/ptt-dev-networking-rg/providers/Microsoft.Network/networkSecurityGroups/ptt-dev-default-nsg001" -route_table_id = "/subscriptions/9842be63-c8c0-4647-a5d1-0c5e7f8bbb25/resourceGroups/ptt-dev-networking-rg/providers/Microsoft.Network/routeTables/ptt-dev-default-rt001" +vnet_id = "/subscriptions/9842be63-c8c0-4647-a5d1-0c5e7f8bbb25/resourceGroups/ptt-dev-networking-eus2-rg/providers/Microsoft.Network/virtualNetworks/spoke-ptt-dev-eus2-vnet001" +nsg_id = "/subscriptions/9842be63-c8c0-4647-a5d1-0c5e7f8bbb25/resourceGroups/ptt-dev-networking-eus2-rg/providers/Microsoft.Network/networkSecurityGroups/ptt-dev-defaul-eus2-nsg001" +route_table_id = "/subscriptions/9842be63-c8c0-4647-a5d1-0c5e7f8bbb25/resourceGroups/ptt-dev-networking-eus2-rg/providers/Microsoft.Network/routeTables/ptt-dev-eus2-default-rt001" subnet_cidr_ranges = { - storage_subnet = "10.2.0.0/27" - consumption_subnet = "10.2.0.32/28" - fabric_subnet = "10.2.0.48/28" - databricks_engineering_private_subnet = "10.2.0.64/26" - databricks_engineering_public_subnet = "10.2.0.128/26" - # databricks_consumption_private_subnet = "10.2.0.192/26" - # databricks_consumption_public_subnet = "10.2.1.0/26" + storage_subnet = "192.168.0.0/27" + consumption_subnet = "192.168.0.32/28" + fabric_subnet = "192.168.0.48/28" + databricks_engineering_private_subnet = "192.168.0.64/26" + databricks_engineering_public_subnet = "192.168.0.128/26" + # databricks_consumption_private_subnet = "192.168.0.192/26" + # databricks_consumption_public_subnet = "192.168.1.0/26" + aifoundry_subnet = "192.168.1.64/27" } # DNS variables @@ -67,8 +71,10 @@ private_dns_zone_id_vault = "/subscriptions/e82c5267-9dc4-4f45-ac13- private_dns_zone_id_databricks = "/subscriptions/e82c5267-9dc4-4f45-ac13-abdd5e130d27/resourceGroups/ptt-dev-privatedns-rg/providers/Microsoft.Network/privateDnsZones/privatelink.azuredatabricks.net" private_dns_zone_id_cognitive_account = "/subscriptions/e82c5267-9dc4-4f45-ac13-abdd5e130d27/resourceGroups/ptt-dev-privatedns-rg/providers/Microsoft.Network/privateDnsZones/privatelink.cognitiveservices.azure.com" private_dns_zone_id_open_ai = "/subscriptions/e82c5267-9dc4-4f45-ac13-abdd5e130d27/resourceGroups/ptt-dev-privatedns-rg/providers/Microsoft.Network/privateDnsZones/privatelink.openai.azure.com" +private_dns_zone_id_ai_services = "/subscriptions/e82c5267-9dc4-4f45-ac13-abdd5e130d27/resourceGroups/ptt-dev-privatedns-rg/providers/Microsoft.Network/privateDnsZones/privatelink.services.ai.azure.com" private_dns_zone_id_data_factory = "/subscriptions/e82c5267-9dc4-4f45-ac13-abdd5e130d27/resourceGroups/ptt-dev-privatedns-rg/providers/Microsoft.Network/privateDnsZones/privatelink.datafactory.azure.net" private_dns_zone_id_search_service = "/subscriptions/e82c5267-9dc4-4f45-ac13-abdd5e130d27/resourceGroups/ptt-dev-privatedns-rg/providers/Microsoft.Network/privateDnsZones/privatelink.search.windows.net" +private_dns_zone_id_cosmos_sql = "/subscriptions/e82c5267-9dc4-4f45-ac13-abdd5e130d27/resourceGroups/ptt-dev-privatedns-rg/providers/Microsoft.Network/privateDnsZones/privatelink.documents.azure.com" # Customer-managed key variables customer_managed_key = null diff --git a/variables.tf b/variables.tf index 1e9daad9..1885ad7d 100644 --- a/variables.tf +++ b/variables.tf @@ -169,6 +169,25 @@ variable "fabric_capacity_details" { } } +variable "ai_foundry_account_details" { + description = "Specifies the ai foundry configuration." + type = object({ + enabled = optional(bool, false) + search_service = optional(object({ + sku = optional(string, "basic") + semantic_search_sku = optional(string, "standard") + partition_count = optional(number, 1) + replica_count = optional(number, 1) + }), {}) + cosmos_db = optional(object({ + consistency_level = optional(string, "Session") + }), {}) + }) + sensitive = false + nullable = false + default = {} +} + # HA/DR variables variable "zone_redundancy_enabled" { description = "Specifies whether zone-redundancy should be enabled for all resources." @@ -254,6 +273,7 @@ variable "subnet_cidr_ranges" { storage_subnet = string consumption_subnet = string fabric_subnet = string + aifoundry_subnet = optional(string, "") databricks_engineering_private_subnet = string databricks_engineering_public_subnet = string databricks_consumption_private_subnet = optional(string, "") @@ -273,6 +293,10 @@ variable "subnet_cidr_ranges" { condition = try(cidrnetmask(var.subnet_cidr_ranges.fabric_subnet), "invalid") != "invalid" error_message = "Please specify a valid CIDR range for the fabric subnet." } + validation { + condition = var.subnet_cidr_ranges.fabric_subnet == "" || try(cidrnetmask(var.subnet_cidr_ranges.fabric_subnet), "invalid") != "invalid" + error_message = "Please specify a valid CIDR range for the ai foundry subnet." + } validation { condition = try(cidrnetmask(var.subnet_cidr_ranges.databricks_engineering_private_subnet), "invalid") != "invalid" error_message = "Please specify a valid CIDR range for the databricks engineering subnet." @@ -373,6 +397,17 @@ variable "private_dns_zone_id_open_ai" { } } +variable "private_dns_zone_id_ai_services" { + description = "Specifies the resource ID of the private DNS zone for Azure Foundry (AI Services). Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_ai_services == "" || (length(split("/", var.private_dns_zone_id_ai_services)) == 9 && endswith(var.private_dns_zone_id_ai_services, "privatelink.services.ai.azure.com")) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} + variable "private_dns_zone_id_data_factory" { description = "Specifies the resource ID of the private DNS zone for Azure Data Factory. Not required if DNS A-records get created via Azure Policy." type = string @@ -395,6 +430,17 @@ variable "private_dns_zone_id_search_service" { } } +variable "private_dns_zone_id_cosmos_sql" { + description = "Specifies the resource ID of the private DNS zone for cosmos db sql. Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_cosmos_sql == "" || (length(split("/", var.private_dns_zone_id_cosmos_sql)) == 9 && endswith(var.private_dns_zone_id_cosmos_sql, "privatelink.documents.azure.com")) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} + # Customer-managed key variables variable "customer_managed_key" { description = "Specifies the customer managed key configurations."