diff --git a/.github/workflows/terraform.yml b/.github/workflows/terraform.yml index c99e7ec..78d25b5 100644 --- a/.github/workflows/terraform.yml +++ b/.github/workflows/terraform.yml @@ -28,7 +28,7 @@ jobs: name: "Terraform" with: environment: "dev" - terraform_version: "1.10.3" + terraform_version: "1.10.4" node_version: 20 working_directory: "./tests/e2e" tenant_id: "37963dd4-f4e6-40f8-a7d6-24b97919e452" @@ -43,7 +43,7 @@ jobs: if: github.event_name == 'push' || github.event_name == 'release' with: environment: "dev" - terraform_version: "1.10.3" + terraform_version: "1.10.4" node_version: 20 working_directory: "./tests/e2e" tenant_id: "37963dd4-f4e6-40f8-a7d6-24b97919e452" diff --git a/main.tf b/main.tf index 05e1029..8d6bfd6 100644 --- a/main.tf +++ b/main.tf @@ -80,6 +80,7 @@ module "data_application" { providers = { azurerm = azurerm + azapi = azapi azuread = azuread time = time } @@ -94,6 +95,7 @@ module "data_application" { app_name = each.key storage_account_ids = module.core.storage_account_ids databricks_workspace_details = module.core.databricks_workspace_details + ai_services = try(each.value.ai_services, {}) # HA/DR variables zone_redundancy_enabled = var.zone_redundancy_enabled diff --git a/modules/dataapplication/aiservice.tf b/modules/dataapplication/aiservice.tf new file mode 100644 index 0000000..5f9bbf1 --- /dev/null +++ b/modules/dataapplication/aiservice.tf @@ -0,0 +1,28 @@ +module "ai_service" { + source = "github.com/PerfectThymeTech/terraform-azurerm-modules//modules/aiservice?ref=main" + providers = { + azurerm = azurerm + azapi = azapi + time = time + } + + for_each = var.ai_services + + location = var.location + resource_group_name = azurerm_resource_group.resource_group_app.name + tags = var.tags + + cognitive_account_name = "${local.prefix}-${each.key}-kv001" + cognitive_account_kind = each.value.kind + cognitive_account_sku = each.value.sku + cognitive_account_firewall_bypass_azure_services = contains(local.ai_service_kind_firewall_bypass_azure_services_list, each.value.kind) ? true : false + cognitive_account_outbound_network_access_restricted = true + cognitive_account_outbound_network_access_allowed_fqdns = [] + cognitive_account_local_auth_enabled = false + cognitive_account_deployments = {} + diagnostics_configurations = var.diagnostics_configurations + subnet_id = var.subnet_id_app + connectivity_delay_in_seconds = var.connectivity_delay_in_seconds + private_dns_zone_id_cognitive_account = var.private_dns_zone_id_cognitive_account + customer_managed_key = var.customer_managed_key +} diff --git a/modules/dataapplication/locals.tf b/modules/dataapplication/locals.tf index 0398158..bf71cac 100644 --- a/modules/dataapplication/locals.tf +++ b/modules/dataapplication/locals.tf @@ -5,4 +5,43 @@ locals { # Databricks locals databricks_enterprise_application_id = "2ff814a6-3304-4ab8-85cb-cd0e6f879c1d" + + # AI service locals + ai_service_kind_firewall_bypass_azure_services_list = [ + "OpenAI" + ] + ai_service_kind_role_map_write = { + "AnomalyDetector" = "Cognitive Services User" + "ComputerVision" = "Cognitive Services User" + "CognitiveServices" = "Cognitive Services User" + "ContentModerator" = "Cognitive Services User" + "CustomVision.Training" = "Cognitive Services Custom Vision Contributor" + "CustomVision.Prediction" = "Cognitive Services Custom Vision Contributor" + "Face" = "Cognitive Services User" + "FormRecognizer" = "Cognitive Services User" + "ImmersiveReader" = "Cognitive Services User" + "LUIS" = "Cognitive Services Language Owner" + "Personalizer" = "Cognitive Services User" + "SpeechServices" = "Cognitive Services Speech Contributor" + "TextAnalytics" = "Cognitive Services Language Owner" + "TextTranslation" = "Cognitive Services Language Owner" + "OpenAI" = "Cognitive Services OpenAI Contributor" + } + ai_service_kind_role_map_use = { + "AnomalyDetector" = "Cognitive Services User" + "ComputerVision" = "Cognitive Services User" + "CognitiveServices" = "Cognitive Services User" + "ContentModerator" = "Cognitive Services User" + "CustomVision.Training" = "Cognitive Services Custom Vision Reader" + "CustomVision.Prediction" = "Cognitive Services Custom Vision Reader" + "Face" = "Cognitive Services User" + "FormRecognizer" = "Cognitive Services User" + "ImmersiveReader" = "Cognitive Services User" + "LUIS" = "Cognitive Services Language Reader" + "Personalizer" = "Cognitive Services User" + "SpeechServices" = "Cognitive Services Speech User" + "TextAnalytics" = "Cognitive Services Language Reader" + "TextTranslation" = "Cognitive Services Language Reader" + "OpenAI" = "Cognitive Services OpenAI User" + } } diff --git a/modules/dataapplication/roleassignments_admin.tf b/modules/dataapplication/roleassignments_admin.tf index d6ed1cd..cdc07a1 100644 --- a/modules/dataapplication/roleassignments_admin.tf +++ b/modules/dataapplication/roleassignments_admin.tf @@ -41,6 +41,25 @@ resource "azurerm_role_assignment" "role_assignment_databricks_workspace_reader_ principal_type = "Group" } +# AI service role assignments +resource "azurerm_role_assignment" "role_assignment_ai_service_admin" { + for_each = 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 = data.azuread_group.group_admin.object_id + principal_type = "Group" +} + +resource "azurerm_role_assignment" "role_assignment_cognitive_services_usages_reader_admin" { + description = "Cognitive Services Usages Reader to check quota for Azure Open AI models." + scope = data.azurerm_subscription.current.id + role_definition_name = "Cognitive Services Usages Reader" + principal_id = data.azuread_group.group_admin.object_id + principal_type = "Group" +} + # Storage role assignments resource "azurerm_role_assignment" "role_assignment_storage_container_external_blob_data_owner_admin" { description = "Role assignment to the external storage container." diff --git a/modules/dataapplication/roleassignments_developer.tf b/modules/dataapplication/roleassignments_developer.tf index ca75be2..1323ca6 100644 --- a/modules/dataapplication/roleassignments_developer.tf +++ b/modules/dataapplication/roleassignments_developer.tf @@ -51,6 +51,17 @@ resource "azurerm_role_assignment" "role_assignment_databricks_workspace_reader_ principal_type = "Group" } +# AI service role assignments +resource "azurerm_role_assignment" "role_assignment_ai_service_developer" { + for_each = 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(data.azuread_group.group_developer[*].object_id) + principal_type = "Group" +} + # Storage role assignments resource "azurerm_role_assignment" "role_assignment_storage_container_external_blob_data_conributor_developer" { count = var.developer_group_name == "" ? 0 : 1 diff --git a/modules/dataapplication/roleassignments_reader.tf b/modules/dataapplication/roleassignments_reader.tf index 77a2dd7..08b0d9d 100644 --- a/modules/dataapplication/roleassignments_reader.tf +++ b/modules/dataapplication/roleassignments_reader.tf @@ -42,6 +42,8 @@ resource "azurerm_role_assignment" "role_assignment_databricks_workspace_reader_ principal_type = "Group" } +# AI service role assignments + # Storage role assignments resource "azurerm_role_assignment" "role_assignment_storage_container_external_blob_data_reader_reader" { count = var.reader_group_name == "" ? 0 : 1 diff --git a/modules/dataapplication/roleassignments_service_principal.tf b/modules/dataapplication/roleassignments_service_principal.tf index 9abaf31..1325e1c 100644 --- a/modules/dataapplication/roleassignments_service_principal.tf +++ b/modules/dataapplication/roleassignments_service_principal.tf @@ -41,6 +41,25 @@ resource "azurerm_role_assignment" "role_assignment_databricks_workspace_reader_ principal_type = "ServicePrincipal" } +# AI service role assignments +resource "azurerm_role_assignment" "role_assignment_ai_service_service_principal" { + for_each = 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 = data.azuread_service_principal.service_principal.object_id + principal_type = "ServicePrincipal" +} + +resource "azurerm_role_assignment" "role_assignment_cognitive_services_usages_reader_service_principal" { + description = "Cognitive Services Usages Reader to check quota for Azure Open AI models." + scope = data.azurerm_subscription.current.id + role_definition_name = "Cognitive Services Usages Reader" + principal_id = data.azuread_service_principal.service_principal.object_id + principal_type = "ServicePrincipal" +} + # Storage role assignments resource "azurerm_role_assignment" "role_assignment_storage_container_external_blob_data_owner_service_principal" { description = "Role assignment to the external storage container." diff --git a/modules/dataapplication/terraform.tf b/modules/dataapplication/terraform.tf index fb0bb9d..ea79b96 100644 --- a/modules/dataapplication/terraform.tf +++ b/modules/dataapplication/terraform.tf @@ -4,6 +4,10 @@ terraform { source = "hashicorp/azurerm" version = "~> 4.0" } + azapi = { + source = "Azure/azapi" + version = "~> 2.0" + } azuread = { source = "hashicorp/azuread" version = "~> 3.0" diff --git a/modules/dataapplication/variables.tf b/modules/dataapplication/variables.tf index c700c26..d7ca226 100644 --- a/modules/dataapplication/variables.tf +++ b/modules/dataapplication/variables.tf @@ -89,6 +89,25 @@ variable "databricks_workspace_details" { default = {} } +variable "ai_services" { + description = "Specifies the map of ai services to be created for this application." + type = map(object({ + location = optional(string, null) + kind = string + sku = string + })) + sensitive = false + nullable = false + default = {} + validation { + condition = alltrue([ + length([for kind in values(var.ai_services)[*].kind : kind if !contains(["AnomalyDetector", "ComputerVision", "CognitiveServices", "ContentModerator", "CustomVision.Training", "CustomVision.Prediction", "Face", "FormRecognizer", "ImmersiveReader", "LUIS", "Personalizer", "SpeechServices", "TextAnalytics", "TextTranslation", "OpenAI"], kind)]) <= 0, + length([for sku in values(var.ai_services)[*].sku : sku if !startswith(sku, "S") && !startswith(sku, "P") && !startswith(sku, "E") && !startswith(sku, "DC")]) <= 0 + ]) + error_message = "Please specify a valid ai service configuration." + } +} + # HA/DR variables variable "zone_redundancy_enabled" { description = "Specifies whether zone-redundancy should be enabled for all resources." @@ -263,6 +282,17 @@ variable "private_dns_zone_id_vault" { } } +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") || endswith(var.private_dns_zone_id_cognitive_account, "privatelink.openai.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/schemas/app.schema.json b/schemas/app.schema.json index f25503f..500fd0c 100644 --- a/schemas/app.schema.json +++ b/schemas/app.schema.json @@ -198,6 +198,65 @@ }, "required": [], "additionalProperties": false + }, + "ai_services": { + "description": "Specifies the ai services to be deployed for the application.", + "type": "object", + "patternProperties": { + "^.*$": { + "properties": { + "location": { + "description": "Specifies the location of the ai service in case it needs to be deployed in another region for capacity reasons.", + "type": "string" + }, + "kind": { + "description": "Specifies the kind of the ai service.", + "type": "string", + "enum": [ + "AnomalyDetector", + "ComputerVision", + "CognitiveServices", + "ContentModerator", + "CustomVision.Training", + "CustomVision.Prediction", + "Face", + "FormRecognizer", + "ImmersiveReader", + "LUIS", + "Personalizer", + "SpeechServices", + "TextAnalytics", + "TextTranslation", + "OpenAI" + ] + }, + "sku": { + "description": "Specifies the sku of the ai service.", + "type": "string", + "enum": [ + "S", + "S0", + "S1", + "S2", + "S3", + "S4", + "S5", + "S6", + "P0", + "P1", + "P2", + "E0", + "DC0" + ] + } + }, + "required": [ + "kind", + "sku" + ], + "additionalProperties": false + } + } } }, "required": [ diff --git a/tests/e2e/data-applications/app001.yml b/tests/e2e/data-applications/app001.yml index 0d66d09..0054555 100644 --- a/tests/e2e/data-applications/app001.yml +++ b/tests/e2e/data-applications/app001.yml @@ -29,3 +29,9 @@ budget: endpoints: email: email_address: test@microsoft.com + +ai_services: + fr: + location: swedencentral + kind: FormRecognizer + sku: S0 diff --git a/tests/e2e/terraform.tf b/tests/e2e/terraform.tf index 956769f..9e2ab40 100644 --- a/tests/e2e/terraform.tf +++ b/tests/e2e/terraform.tf @@ -4,7 +4,7 @@ terraform { required_providers { azurerm = { source = "hashicorp/azurerm" - version = "4.14.0" + version = "4.15.0" } azapi = { source = "azure/azapi"