From 5171865bc3808f1b822b54d987ec2509b8904415 Mon Sep 17 00:00:00 2001 From: Rodrigo Liberoff Date: Fri, 31 May 2024 13:21:20 +0200 Subject: [PATCH] Solved issue 45: Add optional private endpoints for AI Services and Storage Accounts (#55) * Added new output variable `virtual_network_id` in module `vnet` to return the unique identifier of the created virtual network. * Updated `st` module to create an optional private endpoints infrastructure for Blog Storage and File Shares. * - Now the Azure API Management is deployed with (External) VNet support when the `use_private_endpoints` variable is set to `true`. * Add `use_private_endpoints` variable to APIM module. * - Added support to set private endpoints for AI Services and Storage Account services. This private endpoints are set when the variable `use_private_endpoints` is `true`. Also, the current IP address of the machine executing these templates is used as a allowed IP address to facilitate managing these services. * - Increased the size of the subnet for Private Endpoints, since currently near 9 services requires them. - Increased the size of the subnet for the APIM as recommended by Microsoft's documentation. - Also, added delegation in the subnet for the APIM as specified by Microsoft's documentation. * - Improved names for Private Endpoints and related resources in Storage Account module. * Add support for private endpoints and related variables in Cognitive Services module. * Add support for private endpoints in Document Intelligence (a.k.a. `Forms`) module. * Add support for private endpoints in OpenAI module. * Add support for private endpoints in Azure AI Search module. * Add dependency on `azapi_resource.apim_backend_pool` for `policy` resource in APIM module. * Add support for purging soft delete on destroy for API Management in `providers.tf` * Add support for HTTP provider version `3.4.2` from HashiCorp. * Removed `form_recognizer` module. It is now created in the `cog` module. --- infra/.terraform.lock.hcl | 19 ++++ infra/main.tf | 64 +++++++----- infra/modules/apim/main.tf | 6 +- infra/modules/apim/variables.tf | 1 + infra/modules/cog/main.tf | 163 ++++++++++++++++++++++++++++++ infra/modules/cog/outputs.tf | 4 + infra/modules/cog/variables.tf | 5 + infra/modules/form/main.tf | 9 -- infra/modules/form/outputs.tf | 3 - infra/modules/form/variables.tf | 3 - infra/modules/openai/main.tf | 50 ++++++++- infra/modules/openai/variables.tf | 5 + infra/modules/search/main.tf | 37 +++++++ infra/modules/search/variables.tf | 6 +- infra/modules/st/main.tf | 95 +++++++++++++++-- infra/modules/st/variables.tf | 6 +- infra/modules/vnet/main.tf | 14 ++- infra/modules/vnet/outputs.tf | 4 + infra/providers.tf | 3 + infra/variables.tf | 10 ++ 20 files changed, 451 insertions(+), 56 deletions(-) delete mode 100644 infra/modules/form/main.tf delete mode 100644 infra/modules/form/outputs.tf delete mode 100644 infra/modules/form/variables.tf diff --git a/infra/.terraform.lock.hcl b/infra/.terraform.lock.hcl index 495af38..39dc5c3 100644 --- a/infra/.terraform.lock.hcl +++ b/infra/.terraform.lock.hcl @@ -60,6 +60,25 @@ provider "registry.terraform.io/hashicorp/azurerm" { ] } +provider "registry.terraform.io/hashicorp/http" { + version = "3.4.2" + hashes = [ + "h1:YxJewcIIT5sF2h8N+F7eZMsdEimpDpveAOzq/RUiUEo=", + "zh:0ba051c9c8659ce0fec94a3d50926745f11759509c4d6de0ad5f5eb289f0edd9", + "zh:23e6760e8406fef645913bf47bfab1ca984c1c5805d2bb0ef8310b16913d29cd", + "zh:3c69fde4548bfe65b968534c4df8d699648c921d6a065b97fec5faece73a442b", + "zh:41c7f9a8c117704b7a8fa96a57ebfb92b72129d9625128eeb0dee7d5a09d1110", + "zh:59d09d2e00727df10565cc82a33250b44201fcd353eb2b1579507a5a0adcce18", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:c95b2f63d4357b3068531b90d9dca62a32551d7693defb7ab14b650b5d139c57", + "zh:cc0a3bbd3026191b35f417d3a8f26bdfad376d15be9e8d99a8803487ca5b0105", + "zh:d1185c6abb3ba25123fb7df1ad7dbe2b9cd8f43962628da551040fbe1934656f", + "zh:dfb26fccab7ecdc150f67415e6cfe19d699dc43e8bf5722f36032b17b46a0fbe", + "zh:eb1fcc00073bc0463f64e49600a73d925b1a0c0ae5b94dd7b67d3ebac248a113", + "zh:ec9b9ad69cf790cb0603a1036d758063bbbc35c0c75f72dd04a1eddaf46ad010", + ] +} + provider "registry.terraform.io/hashicorp/random" { version = "3.6.1" constraints = "3.6.1" diff --git a/infra/main.tf b/infra/main.tf index 40c4b93..15edbef 100644 --- a/infra/main.tf +++ b/infra/main.tf @@ -1,10 +1,16 @@ data "azurerm_subscription" "current" {} +data "http" "current_ip" { + url = "http://ipv4.icanhazip.com" + count = var.use_private_endpoints ? 1 : 0 +} + resource "random_id" "random" { byte_length = 8 } locals { + allowed_ips = var.use_private_endpoints ? concat(var.allowed_ips, ["${chomp(data.http.current_ip[0].response_body)}"]) : var.allowed_ips sufix = var.use_random_suffix ? substr(lower(random_id.random.hex), 1, 5) : "" name_sufix = var.use_random_suffix ? "-${local.sufix}" : "" resource_group_name = "${var.resource_group_name}${local.name_sufix}" @@ -66,7 +72,8 @@ module "apim" { openai_service_name = module.openai.openai_service_name openai_service_endpoint = module.openai.openai_endpoint tenant_id = data.azurerm_subscription.current.tenant_id - + use_private_endpoints = var.use_private_endpoints + depends_on = [module.nsg] } @@ -84,18 +91,15 @@ resource "azurerm_role_assignment" "id_reader" { } module "search" { - source = "./modules/search" - location = azurerm_resource_group.rg.location - resource_group_name = azurerm_resource_group.rg.name - search_name = local.search_name - principal_id = module.mi.principal_id -} - -module "form_recognizer" { - source = "./modules/form" - location = azurerm_resource_group.rg.location - resource_group_name = azurerm_resource_group.rg.name - form_recognizer_name = local.form_recognizer_name + source = "./modules/search" + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + search_name = local.search_name + principal_id = module.mi.principal_id + allowed_ips = local.allowed_ips + vnet_id = module.vnet.virtual_network_id + private_endpoints_subnet_id = module.vnet.pe_subnet_id + use_private_endpoints = var.use_private_endpoints } module "log" { @@ -114,19 +118,28 @@ module "appi" { } module "st" { - source = "./modules/st" - location = azurerm_resource_group.rg.location - resource_group_name = azurerm_resource_group.rg.name - storage_account_name = local.storage_account_name - principal_id = module.mi.principal_id + source = "./modules/st" + location = azurerm_resource_group.rg.location + resource_group_name = azurerm_resource_group.rg.name + storage_account_name = local.storage_account_name + principal_id = module.mi.principal_id + vnet_id = module.vnet.virtual_network_id + private_endpoints_subnet_id = module.vnet.pe_subnet_id + use_private_endpoints = var.use_private_endpoints + allowed_ips = local.allowed_ips } module "openai" { - source = "./modules/openai" - location = var.location_azopenai - resource_group_name = azurerm_resource_group.rg.name - azopenai_name = local.azopenai_name - principal_id = module.mi.principal_id + source = "./modules/openai" + location = var.location_azopenai + resource_group_name = azurerm_resource_group.rg.name + azopenai_name = local.azopenai_name + principal_id = module.mi.principal_id + allowed_ips = local.allowed_ips + vnet_id = module.vnet.virtual_network_id + vnet_location = azurerm_resource_group.rg.location + private_endpoints_subnet_id = module.vnet.pe_subnet_id + use_private_endpoints = var.use_private_endpoints } module "cog" { @@ -137,12 +150,17 @@ module "cog" { bing_name = local.bing_name cognitive_services_name = local.cognitive_services_name content_safety_name = local.content_safety_name + form_recognizer_name = local.form_recognizer_name speech_name = local.speech_name vision_name = local.vision_name vision_location = var.location_azopenai content_safety_storage_resource_id = module.st.storage_account_id deploy_bing = var.deploy_bing content_safety_location = var.location_content_safety + allowed_ips = local.allowed_ips + vnet_id = module.vnet.virtual_network_id + private_endpoints_subnet_id = module.vnet.pe_subnet_id + use_private_endpoints = var.use_private_endpoints } module "cae" { diff --git a/infra/modules/apim/main.tf b/infra/modules/apim/main.tf index cb70eb5..bf57ed8 100644 --- a/infra/modules/apim/main.tf +++ b/infra/modules/apim/main.tf @@ -26,6 +26,10 @@ resource "azapi_resource" "apim" { publisherName = var.publisher_name apiVersionConstraint = {} developerPortalStatus = "Disabled" + virtualNetworkType = var.use_private_endpoints ? "External" : "None" + virtualNetworkConfiguration = var.use_private_endpoints ? { + subnetResourceId = var.apim_subnet_id + } : null } }) response_export_values = [ @@ -150,7 +154,7 @@ resource "azurerm_api_management_api_policy" "policy" { XML - depends_on = [azurerm_api_management_backend.openai] + depends_on = [azurerm_api_management_backend.openai, azapi_resource.apim_backend_pool] } # https://github.com/aavetis/azure-openai-logger/blob/main/README.md diff --git a/infra/modules/apim/variables.tf b/infra/modules/apim/variables.tf index 8de67ee..9f3426f 100644 --- a/infra/modules/apim/variables.tf +++ b/infra/modules/apim/variables.tf @@ -9,3 +9,4 @@ variable "appi_instrumentation_key" {} variable "openai_service_name" {} variable "openai_service_endpoint" {} variable "tenant_id" {} +variable "use_private_endpoints" {} diff --git a/infra/modules/cog/main.tf b/infra/modules/cog/main.tf index 2f3803c..7074820 100644 --- a/infra/modules/cog/main.tf +++ b/infra/modules/cog/main.tf @@ -1,3 +1,20 @@ +resource "azurerm_cognitive_account" "form" { + name = var.form_recognizer_name + location = var.location + resource_group_name = var.resource_group_name + kind = "FormRecognizer" + sku_name = "S0" + public_network_access_enabled = true + custom_subdomain_name = var.form_recognizer_name + dynamic "network_acls" { # Only set network rules if private endpoints are used, adding allowed IPs to access the service + for_each = var.use_private_endpoints ? [1] : [] + content { + default_action = "Deny" + ip_rules = var.allowed_ips + } + } +} + resource "azurerm_cognitive_account" "content_safety" { name = var.content_safety_name kind = "ContentSafety" @@ -9,6 +26,13 @@ resource "azurerm_cognitive_account" "content_safety" { identity { type = "SystemAssigned" } + dynamic "network_acls" { # Only set network rules if private endpoints are used, adding allowed IPs to access the service + for_each = var.use_private_endpoints ? [1] : [] + content { + default_action = "Deny" + ip_rules = var.allowed_ips + } + } } resource "azurerm_cognitive_account" "cognitive" { @@ -19,6 +43,13 @@ resource "azurerm_cognitive_account" "cognitive" { resource_group_name = var.resource_group_name public_network_access_enabled = true custom_subdomain_name = var.cognitive_services_name + dynamic "network_acls" { # Only set network rules if private endpoints are used, adding allowed IPs to access the service + for_each = var.use_private_endpoints ? [1] : [] + content { + default_action = "Deny" + ip_rules = var.allowed_ips + } + } } resource "azurerm_cognitive_account" "speech" { @@ -29,6 +60,13 @@ resource "azurerm_cognitive_account" "speech" { resource_group_name = var.resource_group_name public_network_access_enabled = true custom_subdomain_name = var.speech_name + dynamic "network_acls" { # Only set network rules if private endpoints are used, adding allowed IPs to access the service + for_each = var.use_private_endpoints ? [1] : [] + content { + default_action = "Deny" + ip_rules = var.allowed_ips + } + } } resource "azurerm_cognitive_account" "vision" { @@ -39,6 +77,13 @@ resource "azurerm_cognitive_account" "vision" { resource_group_name = var.resource_group_name public_network_access_enabled = true custom_subdomain_name = var.vision_name + dynamic "network_acls" { # Only set network rules if private endpoints are used, adding allowed IPs to access the service + for_each = var.use_private_endpoints ? [1] : [] + content { + default_action = "Deny" + ip_rules = var.allowed_ips + } + } } resource "azurerm_resource_group_template_deployment" "main" { @@ -105,8 +150,126 @@ resource "azurerm_resource_group_template_deployment" "main" { TEMPLATE } +# Assign Cognitive Services identity to reader role on the storage account + resource "azurerm_role_assignment" "reader" { scope = var.content_safety_storage_resource_id role_definition_name = "Storage Blob Data Reader" principal_id = azurerm_cognitive_account.content_safety.identity[0].principal_id } + +## Private endpoints + +resource "azurerm_private_dns_zone" "private_dns_zone_cognitive" { + count = var.use_private_endpoints ? 1 : 0 + name = "privatelink.cognitiveservices.azure.com" + resource_group_name = var.resource_group_name +} + +resource "azurerm_private_dns_zone_virtual_network_link" "private_dns_zone_link_cognitive" { + count = var.use_private_endpoints ? 1 : 0 + name = var.content_safety_name + resource_group_name = var.resource_group_name + private_dns_zone_name = azurerm_private_dns_zone.private_dns_zone_cognitive[0].name + virtual_network_id = var.vnet_id +} + +resource "azurerm_private_endpoint" "pep_content_safety" { + count = var.use_private_endpoints ? 1 : 0 + name = "pep-${var.content_safety_name}" + location = var.location + resource_group_name = var.resource_group_name + subnet_id = var.private_endpoints_subnet_id + + private_service_connection { + name = "${var.content_safety_name}-safety-privateserviceconnection" + private_connection_resource_id = azurerm_cognitive_account.content_safety.id + is_manual_connection = false + subresource_names = ["account"] + } + + private_dns_zone_group { + name = "${var.content_safety_name}-privatelink" + private_dns_zone_ids = [azurerm_private_dns_zone.private_dns_zone_cognitive[0].id] + } +} + +resource "azurerm_private_endpoint" "pep_cognitive_services" { + count = var.use_private_endpoints ? 1 : 0 + name = "pep-${var.cognitive_services_name}" + location = var.location + resource_group_name = var.resource_group_name + subnet_id = var.private_endpoints_subnet_id + + private_service_connection { + name = "${var.cognitive_services_name}-privateserviceconnection" + private_connection_resource_id = azurerm_cognitive_account.cognitive.id + is_manual_connection = false + subresource_names = ["account"] + } + + private_dns_zone_group { + name = "${var.cognitive_services_name}-privatelink" + private_dns_zone_ids = [azurerm_private_dns_zone.private_dns_zone_cognitive[0].id] + } +} + +resource "azurerm_private_endpoint" "pep_speech" { + count = var.use_private_endpoints ? 1 : 0 + name = "pep-${var.speech_name}" + location = var.location + resource_group_name = var.resource_group_name + subnet_id = var.private_endpoints_subnet_id + + private_service_connection { + name = "${var.speech_name}-privateserviceconnection" + private_connection_resource_id = azurerm_cognitive_account.speech.id + is_manual_connection = false + subresource_names = ["account"] + } + + private_dns_zone_group { + name = "${var.speech_name}-privatelink" + private_dns_zone_ids = [azurerm_private_dns_zone.private_dns_zone_cognitive[0].id] + } +} + +resource "azurerm_private_endpoint" "pep_vision" { + count = var.use_private_endpoints ? 1 : 0 + name = "pep-${var.vision_name}" + location = var.location + resource_group_name = var.resource_group_name + subnet_id = var.private_endpoints_subnet_id + + private_service_connection { + name = "${var.vision_name}-privateserviceconnection" + private_connection_resource_id = azurerm_cognitive_account.vision.id + is_manual_connection = false + subresource_names = ["account"] + } + + private_dns_zone_group { + name = "${var.vision_name}-privatelink" + private_dns_zone_ids = [azurerm_private_dns_zone.private_dns_zone_cognitive[0].id] + } +} + +resource "azurerm_private_endpoint" "pep_form" { + count = var.use_private_endpoints ? 1 : 0 + name = "pep-${var.form_recognizer_name}" + location = var.location + resource_group_name = var.resource_group_name + subnet_id = var.private_endpoints_subnet_id + + private_service_connection { + name = "${var.form_recognizer_name}-privateserviceconnection" + private_connection_resource_id = azurerm_cognitive_account.form.id + is_manual_connection = false + subresource_names = ["account"] + } + + private_dns_zone_group { + name = "${var.form_recognizer_name}-privatelink" + private_dns_zone_ids = [azurerm_private_dns_zone.private_dns_zone_cognitive[0].id] + } +} diff --git a/infra/modules/cog/outputs.tf b/infra/modules/cog/outputs.tf index 7774379..4ce9e6e 100644 --- a/infra/modules/cog/outputs.tf +++ b/infra/modules/cog/outputs.tf @@ -29,3 +29,7 @@ output "vision_endpoint" { output "vision_key" { value = azurerm_cognitive_account.vision.primary_access_key } + +output "form_recognizer_name" { + value = azurerm_cognitive_account.form.name +} diff --git a/infra/modules/cog/variables.tf b/infra/modules/cog/variables.tf index 93e34f4..8c3d965 100644 --- a/infra/modules/cog/variables.tf +++ b/infra/modules/cog/variables.tf @@ -3,6 +3,7 @@ variable "resource_group_id" {} variable "location" {} variable "content_safety_name" {} variable "cognitive_services_name" {} +variable "form_recognizer_name" {} variable "speech_name" {} variable "vision_name" {} variable "vision_location" {} @@ -10,3 +11,7 @@ variable "bing_name" {} variable "deploy_bing" {} variable "content_safety_storage_resource_id" {} variable "content_safety_location" {} +variable "allowed_ips" {} +variable "vnet_id" {} +variable "private_endpoints_subnet_id" {} +variable "use_private_endpoints" {} diff --git a/infra/modules/form/main.tf b/infra/modules/form/main.tf deleted file mode 100644 index d4957ba..0000000 --- a/infra/modules/form/main.tf +++ /dev/null @@ -1,9 +0,0 @@ -resource "azurerm_cognitive_account" "form" { - name = var.form_recognizer_name - location = var.location - resource_group_name = var.resource_group_name - kind = "FormRecognizer" - sku_name = "S0" - public_network_access_enabled = true - custom_subdomain_name = var.form_recognizer_name -} diff --git a/infra/modules/form/outputs.tf b/infra/modules/form/outputs.tf deleted file mode 100644 index 5fc5ffb..0000000 --- a/infra/modules/form/outputs.tf +++ /dev/null @@ -1,3 +0,0 @@ -output "form_recognizer_name" { - value = azurerm_cognitive_account.form.name -} diff --git a/infra/modules/form/variables.tf b/infra/modules/form/variables.tf deleted file mode 100644 index 5ae7b9a..0000000 --- a/infra/modules/form/variables.tf +++ /dev/null @@ -1,3 +0,0 @@ -variable "resource_group_name" {} -variable "location" {} -variable "form_recognizer_name" {} diff --git a/infra/modules/openai/main.tf b/infra/modules/openai/main.tf index 5b55425..adcb227 100644 --- a/infra/modules/openai/main.tf +++ b/infra/modules/openai/main.tf @@ -6,8 +6,17 @@ resource "azurerm_cognitive_account" "openai" { resource_group_name = var.resource_group_name public_network_access_enabled = true custom_subdomain_name = var.azopenai_name + dynamic "network_acls" { # Only set network rules if private endpoints are used, adding allowed IPs to access the service + for_each = var.use_private_endpoints ? [1] : [] + content { + default_action = "Deny" + ip_rules = var.allowed_ips + } + } } +# Deploy models into Azure OpenAI + resource "azurerm_cognitive_deployment" "gpt_35_turbo" { name = "gpt-35-turbo" cognitive_account_id = azurerm_cognitive_account.openai.id @@ -17,7 +26,6 @@ resource "azurerm_cognitive_deployment" "gpt_35_turbo" { name = "gpt-35-turbo" version = "0613" } - scale { type = "Standard" capacity = 40 @@ -33,7 +41,6 @@ resource "azurerm_cognitive_deployment" "embedding" { name = "text-embedding-ada-002" version = "2" } - scale { type = "Standard" capacity = 40 @@ -49,7 +56,6 @@ resource "azurerm_cognitive_deployment" "gpt_4" { name = "gpt-4" version = "1106-Preview" } - scale { type = "Standard" capacity = 20 @@ -65,16 +71,52 @@ resource "azurerm_cognitive_deployment" "gpt4_vision" { name = "gpt-4" version = "vision-preview" } - scale { type = "Standard" capacity = 20 } } +# Set role assignment for OpenAI + resource "azurerm_role_assignment" "openai_user" { scope = azurerm_cognitive_account.openai.id role_definition_name = "Cognitive Services OpenAI User" principal_id = var.principal_id } +# Private endpoint + +resource "azurerm_private_dns_zone" "private_dns_zone_openai" { + count = var.use_private_endpoints ? 1 : 0 + name = "privatelink.openai.azure.com" + resource_group_name = var.resource_group_name +} + +resource "azurerm_private_endpoint" "pep_openai" { + count = var.use_private_endpoints ? 1 : 0 + name = "pep-${var.azopenai_name}" + location = var.vnet_location + resource_group_name = var.resource_group_name + subnet_id = var.private_endpoints_subnet_id + + private_service_connection { + name = "${var.azopenai_name}-privateserviceconnection" + private_connection_resource_id = azurerm_cognitive_account.openai.id + is_manual_connection = false + subresource_names = ["account"] + } + + private_dns_zone_group { + name = "${var.azopenai_name}-privatelink" + private_dns_zone_ids = [azurerm_private_dns_zone.private_dns_zone_openai[0].id] + } +} + +resource "azurerm_private_dns_zone_virtual_network_link" "private_dns_zone_link_openai" { + count = var.use_private_endpoints ? 1 : 0 + name = var.azopenai_name + resource_group_name = var.resource_group_name + private_dns_zone_name = azurerm_private_dns_zone.private_dns_zone_openai[0].name + virtual_network_id = var.vnet_id +} \ No newline at end of file diff --git a/infra/modules/openai/variables.tf b/infra/modules/openai/variables.tf index 5cac0a8..10c6937 100644 --- a/infra/modules/openai/variables.tf +++ b/infra/modules/openai/variables.tf @@ -2,3 +2,8 @@ variable "resource_group_name" {} variable "location" {} variable "azopenai_name" {} variable "principal_id" {} +variable "allowed_ips" {} +variable "vnet_id" {} +variable "vnet_location" {} +variable "private_endpoints_subnet_id" {} +variable "use_private_endpoints" {} diff --git a/infra/modules/search/main.tf b/infra/modules/search/main.tf index 47b3735..3afc3fa 100644 --- a/infra/modules/search/main.tf +++ b/infra/modules/search/main.tf @@ -4,6 +4,7 @@ resource "azurerm_search_service" "search" { resource_group_name = var.resource_group_name sku = "standard" semantic_search_sku = "free" + allowed_ips = var.use_private_endpoints ? var.allowed_ips : null local_authentication_enabled = false } @@ -25,3 +26,39 @@ resource "azurerm_role_assignment" "search_service_contributor" { role_definition_name = "Search Service Contributor" principal_id = var.principal_id } + +# Private endpoint + +resource "azurerm_private_dns_zone" "private_dns_zone_search" { + count = var.use_private_endpoints ? 1 : 0 + name = "privatelink.search.windows.net" + resource_group_name = var.resource_group_name +} + +resource "azurerm_private_endpoint" "pep_search" { + count = var.use_private_endpoints ? 1 : 0 + name = "pep-${var.search_name}" + location = var.location + resource_group_name = var.resource_group_name + subnet_id = var.private_endpoints_subnet_id + + private_service_connection { + name = "${var.search_name}-privateserviceconnection" + private_connection_resource_id = azurerm_search_service.search.id + is_manual_connection = false + subresource_names = ["searchService"] + } + + private_dns_zone_group { + name = "${var.search_name}-privatelink" + private_dns_zone_ids = [azurerm_private_dns_zone.private_dns_zone_search[0].id] + } +} + +resource "azurerm_private_dns_zone_virtual_network_link" "private_dns_zone_link_search" { + count = var.use_private_endpoints ? 1 : 0 + name = var.search_name + resource_group_name = var.resource_group_name + private_dns_zone_name = azurerm_private_dns_zone.private_dns_zone_search[0].name + virtual_network_id = var.vnet_id +} diff --git a/infra/modules/search/variables.tf b/infra/modules/search/variables.tf index b59623c..09ba0aa 100644 --- a/infra/modules/search/variables.tf +++ b/infra/modules/search/variables.tf @@ -1,4 +1,8 @@ variable "resource_group_name" {} variable "location" {} variable "search_name" {} -variable "principal_id" {} \ No newline at end of file +variable "principal_id" {} +variable "allowed_ips" {} +variable "vnet_id" {} +variable "private_endpoints_subnet_id" {} +variable "use_private_endpoints" {} diff --git a/infra/modules/st/main.tf b/infra/modules/st/main.tf index 293d671..e3fe734 100644 --- a/infra/modules/st/main.tf +++ b/infra/modules/st/main.tf @@ -1,3 +1,7 @@ +locals { + network_rules_bypass = var.use_private_endpoints ? [ "None" ] : [ "AzureServices" ] +} + resource "azurerm_storage_account" "sa" { name = var.storage_account_name location = var.location @@ -6,17 +10,19 @@ resource "azurerm_storage_account" "sa" { account_replication_type = "LRS" enable_https_traffic_only = true allow_nested_items_to_be_public = false - # We are enabling the firewall only allowing traffic from our PC's public IP. - # network_rules { - # default_action = "Deny" - # virtual_network_subnet_ids = [] - # ip_rules = [ - # jsondecode(data.http.current_public_ip.body).ip - # ] - # } } -# Create data container +resource "azurerm_storage_account_network_rules" "sa_network_rules" { + count = var.use_private_endpoints ? 1 : 0 + storage_account_id = azurerm_storage_account.sa.id + default_action = "Deny" + virtual_network_subnet_ids = [] + ip_rules = var.allowed_ips + bypass = local.network_rules_bypass +} + +# Create containers and file shares, then populate them as required. + resource "azurerm_storage_container" "content" { name = "content" container_access_type = "private" @@ -93,3 +99,74 @@ resource "azurerm_role_assignment" "storage_contributor" { principal_id = var.principal_id } +# Private endpoint for the Blob Storage + +resource "azurerm_private_dns_zone" "private_dns_zone_blob" { + count = var.use_private_endpoints ? 1 : 0 + name = "privatelink.blob.core.windows.net" + resource_group_name = var.resource_group_name +} + +resource "azurerm_private_endpoint" "pep_blob" { + count = var.use_private_endpoints ? 1 : 0 + name = "pep-${var.storage_account_name}-blob" + location = var.location + resource_group_name = var.resource_group_name + subnet_id = var.private_endpoints_subnet_id + + private_service_connection { + name = "${var.storage_account_name}-blob-privateserviceconnection" + private_connection_resource_id = azurerm_storage_account.sa.id + is_manual_connection = false + subresource_names = ["blob"] + } + + private_dns_zone_group { + name = "${var.storage_account_name}-blob-privatelink" + private_dns_zone_ids = [azurerm_private_dns_zone.private_dns_zone_blob[0].id] + } +} + +resource "azurerm_private_dns_zone_virtual_network_link" "private_dns_zone_link_blob" { + count = var.use_private_endpoints ? 1 : 0 + name = "${var.storage_account_name}-blob" + resource_group_name = var.resource_group_name + private_dns_zone_name = azurerm_private_dns_zone.private_dns_zone_blob[0].name + virtual_network_id = var.vnet_id +} + +# Private endpoint for the File Share + +resource "azurerm_private_dns_zone" "private_dns_zone_file" { + count = var.use_private_endpoints ? 1 : 0 + name = "privatelink.file.core.windows.net" + resource_group_name = var.resource_group_name +} + +resource "azurerm_private_endpoint" "pep_file" { + count = var.use_private_endpoints ? 1 : 0 + name = "pep-${var.storage_account_name}-file" + location = var.location + resource_group_name = var.resource_group_name + subnet_id = var.private_endpoints_subnet_id + + private_service_connection { + name = "${var.storage_account_name}-file-privateserviceconnection" + private_connection_resource_id = azurerm_storage_account.sa.id + is_manual_connection = false + subresource_names = ["file"] + } + + private_dns_zone_group { + name = "${var.storage_account_name}-file-privatelink" + private_dns_zone_ids = [azurerm_private_dns_zone.private_dns_zone_file[0].id] + } +} + +resource "azurerm_private_dns_zone_virtual_network_link" "private_dns_zone_link_file" { + count = var.use_private_endpoints ? 1 : 0 + name = "file" + resource_group_name = var.resource_group_name + private_dns_zone_name = azurerm_private_dns_zone.private_dns_zone_file[0].name + virtual_network_id = var.vnet_id +} diff --git a/infra/modules/st/variables.tf b/infra/modules/st/variables.tf index 64fe18b..17ee0e1 100644 --- a/infra/modules/st/variables.tf +++ b/infra/modules/st/variables.tf @@ -1,4 +1,8 @@ variable "resource_group_name" {} variable "location" {} variable "storage_account_name" {} -variable "principal_id" {} \ No newline at end of file +variable "principal_id" {} +variable "allowed_ips" {} +variable "vnet_id" {} +variable "private_endpoints_subnet_id" {} +variable "use_private_endpoints" {} diff --git a/infra/modules/vnet/main.tf b/infra/modules/vnet/main.tf index 5cd94bc..a44679b 100644 --- a/infra/modules/vnet/main.tf +++ b/infra/modules/vnet/main.tf @@ -9,14 +9,24 @@ resource "azurerm_subnet" "apim" { name = "snet-apim" resource_group_name = var.resource_group_name virtual_network_name = azurerm_virtual_network.vnet.name - address_prefixes = ["10.5.0.0/29"] + address_prefixes = ["10.5.0.0/27"] # A minimum subnet size of /26 or /27 is recommended when creating a new subnet for APIM. Reference: https://learn.microsoft.com/en-us/azure/api-management/integrate-vnet-outbound#prerequisites + # Delegate the subnet to "Microsoft.Web/serverFarms". Reference: https://learn.microsoft.com/en-us/azure/api-management/integrate-vnet-outbound#delegate-the-subnet + delegation { + name = "apim-delegation" + service_delegation { + name = "Microsoft.Web/serverFarms" + actions = [ + "Microsoft.Network/virtualNetworks/subnets/join/action", + ] + } + } } resource "azurerm_subnet" "private_endpoints" { name = "snet-pe" resource_group_name = var.resource_group_name virtual_network_name = azurerm_virtual_network.vnet.name - address_prefixes = ["10.5.1.0/29"] + address_prefixes = ["10.5.1.0/28"] # Currently, this deployment creates around 9 services that requires private endpoints. } resource "azurerm_subnet" "cae" { diff --git a/infra/modules/vnet/outputs.tf b/infra/modules/vnet/outputs.tf index 0528762..756f638 100644 --- a/infra/modules/vnet/outputs.tf +++ b/infra/modules/vnet/outputs.tf @@ -2,6 +2,10 @@ output "virtual_network_name" { value = azurerm_virtual_network.vnet.name } +output "virtual_network_id" { + value = azurerm_virtual_network.vnet.id +} + output "apim_subnet_id" { value = azurerm_subnet.apim.id } diff --git a/infra/providers.tf b/infra/providers.tf index 0f30658..7150716 100644 --- a/infra/providers.tf +++ b/infra/providers.tf @@ -29,6 +29,9 @@ provider "azurerm" { cognitive_account { purge_soft_delete_on_destroy = true } + api_management { + purge_soft_delete_on_destroy = true + } } } diff --git a/infra/variables.tf b/infra/variables.tf index d44481d..edd7646 100644 --- a/infra/variables.tf +++ b/infra/variables.tf @@ -145,3 +145,13 @@ variable "enable_openai_plugin_call_transcript" { variable "enable_openai_plugin_compare_financial_products" { default = false } + +variable "use_private_endpoints" { + type = bool + default = false +} + +variable "allowed_ips" { + type = list(string) + default = [] +}