From 09ce97253545b09553c53a3a422160f14faeebc3 Mon Sep 17 00:00:00 2001 From: Marvin Buss Date: Fri, 12 Jul 2024 13:17:18 +0200 Subject: [PATCH 1/3] Add AI Studio Secure E2E --- quickstart/301-ai-studio-secure-e2e/data.tf | 1 + quickstart/301-ai-studio-secure-e2e/locals.tf | 3 + quickstart/301-ai-studio-secure-e2e/main.tf | 148 +++++++ .../modules/aistudiohub/README.md | 203 ++++++++++ .../modules/aistudiohub/README_footer.md | 0 .../modules/aistudiohub/README_header.md | 11 + .../modules/aistudiohub/connections.tf | 26 ++ .../modules/aistudiohub/connectivity.tf | 39 ++ .../modules/aistudiohub/data.tf | 5 + .../modules/aistudiohub/diagnostics.tf | 29 ++ .../modules/aistudiohub/locals.tf | 14 + .../modules/aistudiohub/main.tf | 64 +++ .../modules/aistudiohub/outputs.tf | 27 ++ .../modules/aistudiohub/terraform.tf | 18 + .../modules/aistudiohub/variables.tf | 237 +++++++++++ .../modules/aistudiooutboundrules/README.md | 112 +++++ .../aistudiooutboundrules/README_footer.md | 0 .../aistudiooutboundrules/README_header.md | 11 + .../modules/aistudiooutboundrules/locals.tf | 129 ++++++ .../modules/aistudiooutboundrules/main.tf | 56 +++ .../Approve-ManagedPrivateEndpoint.ps1 | 108 +++++ .../aistudiooutboundrules/terraform.tf | 10 + .../aistudiooutboundrules/variables.tf | 91 +++++ .../modules/aistudioproject/README.md | 114 ++++++ .../modules/aistudioproject/README_footer.md | 0 .../modules/aistudioproject/README_header.md | 1 + .../modules/aistudioproject/connections.tf | 26 ++ .../modules/aistudioproject/data.tf | 5 + .../modules/aistudioproject/diagnostics.tf | 29 ++ .../modules/aistudioproject/main.tf | 26 ++ .../modules/aistudioproject/outputs.tf | 17 + .../modules/aistudioproject/terraform.tf | 14 + .../modules/aistudioproject/variables.tf | 87 ++++ .../modules/applicationinsights/README.md | 102 +++++ .../applicationinsights/README_footer.md | 0 .../applicationinsights/README_header.md | 1 + .../modules/applicationinsights/data.tf | 3 + .../applicationinsights/diagnostics.tf | 29 ++ .../modules/applicationinsights/main.tf | 17 + .../modules/applicationinsights/outputs.tf | 17 + .../modules/applicationinsights/terraform.tf | 10 + .../modules/applicationinsights/variables.tf | 72 ++++ .../modules/containerregistry/README.md | 197 +++++++++ .../containerregistry/README_footer.md | 0 .../containerregistry/README_header.md | 1 + .../modules/containerregistry/connectivity.tf | 38 ++ .../modules/containerregistry/data.tf | 3 + .../modules/containerregistry/main.tf | 57 +++ .../modules/containerregistry/outputs.tf | 27 ++ .../modules/containerregistry/terraform.tf | 14 + .../modules/containerregistry/variables.tf | 177 ++++++++ .../modules/keyvault/README.md | 132 ++++++ .../modules/keyvault/README_footer.md | 0 .../modules/keyvault/README_header.md | 1 + .../modules/keyvault/connectivity.tf | 38 ++ .../modules/keyvault/data.tf | 5 + .../modules/keyvault/diagnostics.tf | 29 ++ .../modules/keyvault/main.tf | 23 ++ .../modules/keyvault/outputs.tf | 28 ++ .../modules/keyvault/roleassignments.tf | 5 + .../modules/keyvault/terraform.tf | 14 + .../modules/keyvault/variables.tf | 108 +++++ .../modules/loganalytics/README.md | 92 +++++ .../modules/loganalytics/README_footer.md | 0 .../modules/loganalytics/README_header.md | 1 + .../modules/loganalytics/data.tf | 3 + .../modules/loganalytics/diagnostics.tf | 29 ++ .../modules/loganalytics/main.tf | 17 + .../modules/loganalytics/outputs.tf | 11 + .../modules/loganalytics/terraform.tf | 10 + .../modules/loganalytics/variables.tf | 64 +++ .../modules/prerequisites/prerequisites.tf | 170 ++++++++ .../modules/storage/README.md | 382 ++++++++++++++++++ .../modules/storage/README_footer.md | 0 .../modules/storage/README_header.md | 1 + .../modules/storage/connectivity.tf | 40 ++ .../modules/storage/data.tf | 5 + .../modules/storage/diagnostics.tf | 29 ++ .../modules/storage/locals.tf | 10 + .../modules/storage/main.tf | 113 ++++++ .../modules/storage/outputs.tf | 58 +++ .../modules/storage/roleassignments.tf | 5 + .../modules/storage/terraform.tf | 14 + .../modules/storage/variables.tf | 378 +++++++++++++++++ .../301-ai-studio-secure-e2e/providers.tf | 34 ++ .../301-ai-studio-secure-e2e/resourcegroup.tf | 5 + .../301-ai-studio-secure-e2e/terraform.tf | 18 + .../301-ai-studio-secure-e2e/variables.tf | 159 ++++++++ .../301-ai-studio-secure-e2e/vars.tfvars | 0 89 files changed, 4457 insertions(+) create mode 100644 quickstart/301-ai-studio-secure-e2e/data.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/locals.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/main.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/README.md create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/README_footer.md create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/README_header.md create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/connections.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/connectivity.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/data.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/diagnostics.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/locals.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/main.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/outputs.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/terraform.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/variables.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/README.md create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/README_footer.md create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/README_header.md create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/locals.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/main.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/scripts/Approve-ManagedPrivateEndpoint.ps1 create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/terraform.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/variables.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/README.md create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/README_footer.md create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/README_header.md create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/connections.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/data.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/diagnostics.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/main.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/outputs.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/terraform.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/variables.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/README.md create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/README_footer.md create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/README_header.md create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/data.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/diagnostics.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/main.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/outputs.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/terraform.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/variables.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/containerregistry/README.md create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/containerregistry/README_footer.md create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/containerregistry/README_header.md create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/containerregistry/connectivity.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/containerregistry/data.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/containerregistry/main.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/containerregistry/outputs.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/containerregistry/terraform.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/containerregistry/variables.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/keyvault/README.md create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/keyvault/README_footer.md create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/keyvault/README_header.md create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/keyvault/connectivity.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/keyvault/data.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/keyvault/diagnostics.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/keyvault/main.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/keyvault/outputs.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/keyvault/roleassignments.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/keyvault/terraform.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/keyvault/variables.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/loganalytics/README.md create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/loganalytics/README_footer.md create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/loganalytics/README_header.md create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/loganalytics/data.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/loganalytics/diagnostics.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/loganalytics/main.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/loganalytics/outputs.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/loganalytics/terraform.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/loganalytics/variables.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/prerequisites/prerequisites.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/storage/README.md create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/storage/README_footer.md create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/storage/README_header.md create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/storage/connectivity.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/storage/data.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/storage/diagnostics.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/storage/locals.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/storage/main.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/storage/outputs.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/storage/roleassignments.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/storage/terraform.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/storage/variables.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/providers.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/resourcegroup.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/terraform.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/variables.tf create mode 100644 quickstart/301-ai-studio-secure-e2e/vars.tfvars diff --git a/quickstart/301-ai-studio-secure-e2e/data.tf b/quickstart/301-ai-studio-secure-e2e/data.tf new file mode 100644 index 000000000..cee07df25 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/data.tf @@ -0,0 +1 @@ +data "azurerm_client_config" "current" {} diff --git a/quickstart/301-ai-studio-secure-e2e/locals.tf b/quickstart/301-ai-studio-secure-e2e/locals.tf new file mode 100644 index 000000000..8ad57889b --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/locals.tf @@ -0,0 +1,3 @@ +locals { + prefix = "${lower(var.prefix)}-${var.environment}" +} diff --git a/quickstart/301-ai-studio-secure-e2e/main.tf b/quickstart/301-ai-studio-secure-e2e/main.tf new file mode 100644 index 000000000..bfcc542db --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/main.tf @@ -0,0 +1,148 @@ +module "storage_account" { + source = "./modules/storage" + + location = var.location + resource_group_name = azurerm_resource_group.resource_group.name + tags = var.tags + storage_account_name = replace("stg-${local.prefix}", "-", "") + storage_access_tier = "Hot" + storage_account_type = "StorageV2" + storage_account_tier = "Standard" + storage_account_replication_type = "LRS" + storage_blob_change_feed_enabled = false + storage_blob_container_delete_retention_in_days = 7 + storage_blob_delete_retention_in_days = 7 + storage_blob_cors_rules = { + azureml = { # Docs: https://learn.microsoft.com/en-us/azure/machine-learning/prompt-flow/troubleshoot-guidance?view=azureml-api-2#flow-is-missing + allowed_headers = ["*"] + allowed_methods = ["GET", "HEAD", "PUT", "DELETE", "OPTIONS", "POST", "PATCH"] + allowed_origins = ["https://mlworkspace.azure.ai", "https://ml.azure.com", "https://*.ml.azure.com", "https://ai.azure.com", "https://*.ai.azure.com", "https://mlworkspacecanary.azure.ai", "https://mlworkspace.azureml-test.net"] + exposed_headers = ["*"] + max_age_in_seconds = 1800 + } + } + 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 = [ + "/subscriptions/${data.azurerm_client_config.current.subscription_id}/resourceGroups/*/providers/Microsoft.MachineLearningServices/workspaces/*", + ] + storage_public_network_access_enabled = true + storage_nfsv3_enabled = false + storage_sftp_enabled = false + storage_shared_access_key_enabled = true + storage_container_names = [] + storage_static_website = [] + diagnostics_configurations = [] + subnet_id = var.subnet_id + private_endpoint_subresource_names = ["blob", "file", "queue", "table"] + private_dns_zone_id_blob = var.private_dns_zone_id_blob + private_dns_zone_id_file = var.private_dns_zone_id_file + private_dns_zone_id_table = var.private_dns_zone_id_table + private_dns_zone_id_queue = var.private_dns_zone_id_queue + private_dns_zone_id_web = "" + private_dns_zone_id_dfs = "" + customer_managed_key = var.customer_managed_key +} + +module "key_vault" { + source = "./modules/keyvault" + + location = var.location + resource_group_name = azurerm_resource_group.resource_group.name + tags = var.tags + key_vault_name = "kv-${local.prefix}" + key_vault_sku_name = "standard" + key_vault_soft_delete_retention_days = 7 + diagnostics_configurations = [] + subnet_id = var.subnet_id + private_dns_zone_id_vault = var.private_dns_zone_id_vault +} + +module "log_analytics_workspace" { + source = "./modules/loganalytics" + + location = var.location + resource_group_name = azurerm_resource_group.resource_group.name + tags = var.tags + log_analytics_workspace_name = "log-${local.prefix}" + log_analytics_workspace_retention_in_days = 30 + diagnostics_configurations = [] +} + +module "application_insights" { + source = "./modules/applicationinsights" + + location = var.location + resource_group_name = azurerm_resource_group.resource_group.name + tags = var.tags + application_insights_name = "ai-${local.prefix}" + application_insights_application_type = "web" + application_insights_log_analytics_workspace_id = module.log_analytics_workspace.log_analytics_workspace_id + diagnostics_configurations = [] +} + +module "container_registry" { + source = "./modules/containerregistry" + + location = var.location + resource_group_name = azurerm_resource_group.resource_group.name + tags = var.tags + container_registry_name = "acr-${local.prefix}" + container_registry_admin_enabled = false + container_registry_anonymous_pull_enabled = false + container_registry_data_endpoint_enabled = false + container_registry_export_policy_enabled = false + container_registry_quarantine_policy_enabled = false + container_registry_retention_policy_in_days = 7 + container_registry_trust_policy_enabled = false + container_registry_zone_redundancy_enabled = false + diagnostics_configurations = [] + subnet_id = var.subnet_id + private_dns_zone_id_container_registry = var.private_dns_zone_id_container_registry + customer_managed_key = var.customer_managed_key +} + +module "ai_studio_hub" { + source = "./modules/aistudiohub" + + location = var.location + resource_group_name = azurerm_resource_group.resource_group.name + tags = var.tags + ai_studio_hub_name = "aih-${local.prefix}" + application_insights_id = module.application_insights.application_insights_id + container_registry_id = module.container_registry.container_registry_id + key_vault_id = module.key_vault.key_vault_id + storage_account_id = module.storage_account.storage_account_id + ai_studio_hub_provision_managed_network = false + ai_studio_hub_connections = {} + diagnostics_configurations = [] + subnet_id = var.subnet_id + private_dns_zone_id_machine_learning_api = var.private_dns_zone_id_machine_learning_api + private_dns_zone_id_machine_learning_notebooks = var.private_dns_zone_id_machine_learning_notebooks + customer_managed_key = var.customer_managed_key +} + +module "ai_studio_project" { + source = "./modules/aistudioproject" + + location = var.location + resource_group_name = azurerm_resource_group.resource_group.name + tags = var.tags + ai_studio_project_name = "aip-${local.prefix}" + ai_studio_hub_id = module.ai_studio_hub.ai_studio_hub_id + ai_studio_project_connections = {} +} + +module "ai_studio_outbound_connection_rules" { + source = "./modules/aistudiooutboundrules" + + ai_studio_hub_id = module.ai_studio_hub.ai_studio_hub_id + ai_studio_hub_storage_account_id = module.storage_account.storage_account_id + ai_studio_hub_provision_managed_network = false + ai_studio_hub_approve_private_endpoints = false + ai_studio_hub_outbound_rules_fqdns = {} + ai_studio_hub_outbound_rules_service_endpoints = {} + ai_studio_hub_outbound_rules_private_endpoints = {} +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/README.md b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/README.md new file mode 100644 index 000000000..7978195f5 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/README.md @@ -0,0 +1,203 @@ + +# Azure AI Studio Hub Terraform Module + +## Info for CMK Deployments + +CMK deployments are currently not supported because the service-side encryption of metadata is not supported for hub workspaces today. Once this is supported, we can update the module and enable CMK deployments. More details: https://learn.microsoft.com/en-us/azure/machine-learning/concept-customer-managed-keys?view=azureml-api-2#preview-service-side-encryption-of-metadata + +The standard deployment is currently blocked because of a managed Cosmos DB that gets created inside the customer environment. The workspace requires the asignment of a contributor role on that managed resource group in which the cosmos DB will reside. This assignment is currently being blocked by Azure Policies. + +## Info for user-defined outbound rule management + +We have decided to manage the outbund rules using a separate Terraform module. Reasons for this decision and the module can be found here: [Azure AI Studio Outbound Rules](/modules/aistudiooutboundrules/) + +## Documentation + + +## Requirements + +The following requirements are needed by this module: + +- [terraform](#requirement\_terraform) (>=0.12) + +- [azapi](#requirement\_azapi) (>= 1.14.0) + +- [azurerm](#requirement\_azurerm) (>= 3.100.0) + +- [time](#requirement\_time) (>= 0.9.1) + +## Modules + +No modules. + + + +## Required Inputs + +The following input variables are required: + +### [ai\_studio\_hub\_name](#input\_ai\_studio\_hub\_name) + +Description: Specifies the name of the ai studio hub. + +Type: `string` + +### [application\_insights\_id](#input\_application\_insights\_id) + +Description: Specifies the id of application insights that will be connected to the ai studio hub. + +Type: `string` + +### [container\_registry\_id](#input\_container\_registry\_id) + +Description: Specifies the id of the container registry that will be connected to the ai studio hub. + +Type: `string` + +### [key\_vault\_id](#input\_key\_vault\_id) + +Description: Specifies the id of the key vaul that will be connected to the ai studio hub. + +Type: `string` + +### [location](#input\_location) + +Description: Specifies the location of all resources. + +Type: `string` + +### [resource\_group\_name](#input\_resource\_group\_name) + +Description: Specifies the resource group name in which all resources will get deployed. + +Type: `string` + +### [storage\_account\_id](#input\_storage\_account\_id) + +Description: Specifies the id of the storage account that will be connected to the ai studio hub. + +Type: `string` + +### [subnet\_id](#input\_subnet\_id) + +Description: Specifies the resource id of a subnet in which the private endpoints get created. + +Type: `string` + +## Optional Inputs + +The following input variables are optional (have default values): + +### [ai\_studio\_hub\_connections](#input\_ai\_studio\_hub\_connections) + +Description: Specifies the connections that should be added to the AI Studio Hub. Only provide connections to be shared with all projects at the hub level. + +Type: + +```hcl +map(object({ + auth_type = optional(string, "AAD") + category = string + credentials = optional(any, null) + target = string + metadata = any + })) +``` + +Default: `{}` + +### [ai\_studio\_hub\_provision\_managed\_network](#input\_ai\_studio\_hub\_provision\_managed\_network) + +Description: Specifies whether the managed vnet should be providioned as part of the ai studio hub deployment. + +Type: `bool` + +Default: `false` + +### [connectivity\_delay\_in\_seconds](#input\_connectivity\_delay\_in\_seconds) + +Description: Specifies the delay in seconds after the private endpoint deployment (required for the DNS automation via Policies). + +Type: `number` + +Default: `120` + +### [customer\_managed\_key](#input\_customer\_managed\_key) + +Description: Specifies the customer managed key configurations. + +Type: + +```hcl +object({ + key_vault_id = string, + key_vault_key_versionless_id = string, + user_assigned_identity_id = string, + user_assigned_identity_client_id = string, + }) +``` + +Default: `null` + +### [diagnostics\_configurations](#input\_diagnostics\_configurations) + +Description: Specifies the diagnostic configuration for the service. + +Type: + +```hcl +list(object({ + log_analytics_workspace_id = optional(string, ""), + storage_account_id = optional(string, "") + })) +``` + +Default: `[]` + +### [private\_dns\_zone\_id\_machine\_learning\_api](#input\_private\_dns\_zone\_id\_machine\_learning\_api) + +Description: Specifies the resource ID of the private DNS zone for the Purview account. Not required if DNS A-records get created via Azure Policy. + +Type: `string` + +Default: `""` + +### [private\_dns\_zone\_id\_machine\_learning\_notebooks](#input\_private\_dns\_zone\_id\_machine\_learning\_notebooks) + +Description: Specifies the resource ID of the private DNS zone for the Purview account. Not required if DNS A-records get created via Azure Policy. + +Type: `string` + +Default: `""` + +### [tags](#input\_tags) + +Description: Specifies a key value map of tags to set on every taggable resources. + +Type: `map(string)` + +Default: `{}` + +## Outputs + +The following outputs are exported: + +### [ai\_studio\_hub\_id](#output\_ai\_studio\_hub\_id) + +Description: Specifies the resource id of the ai studio hub. + +### [ai\_studio\_hub\_name](#output\_ai\_studio\_hub\_name) + +Description: Specifies the name of the ai studio hub. + +### [ai\_studio\_hub\_principal\_id](#output\_ai\_studio\_hub\_principal\_id) + +Description: Specifies the principal id of the ai studio hub. + +### [ai\_studio\_hub\_setup\_completed](#output\_ai\_studio\_hub\_setup\_completed) + +Description: Specifies whether the connectivity and identity has been successfully configured. + + + + \ No newline at end of file diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/README_footer.md b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/README_footer.md new file mode 100644 index 000000000..e69de29bb diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/README_header.md b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/README_header.md new file mode 100644 index 000000000..29911bb4c --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/README_header.md @@ -0,0 +1,11 @@ +# Azure AI Studio Hub Terraform Module + +## Info for CMK Deployments + +CMK deployments are currently not supported because the service-side encryption of metadata is not supported for hub workspaces today. Once this is supported, we can update the module and enable CMK deployments. More details: https://learn.microsoft.com/en-us/azure/machine-learning/concept-customer-managed-keys?view=azureml-api-2#preview-service-side-encryption-of-metadata + +The standard deployment is currently blocked because of a managed Cosmos DB that gets created inside the customer environment. The workspace requires the asignment of a contributor role on that managed resource group in which the cosmos DB will reside. This assignment is currently being blocked by Azure Policies. + +## Info for user-defined outbound rule management + +We have decided to manage the outbund rules using a separate Terraform module. Reasons for this decision and the module can be found here: [Azure AI Studio Outbound Rules](/modules/aistudiooutboundrules/) diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/connections.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/connections.tf new file mode 100644 index 000000000..7b77ae22f --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/connections.tf @@ -0,0 +1,26 @@ +resource "azapi_resource" "ai_studio_hub_connections" { + for_each = var.ai_studio_hub_connections + + type = "Microsoft.MachineLearningServices/workspaces/connections@2024-04-01" + name = each.key + parent_id = azapi_resource.ai_studio_hub.id + + body = jsonencode({ + properties = { + authType = each.value.auth_type + category = each.value.category + credentials = each.value.credentials + isSharedToAll = true + sharedUserList = [] + target = each.value.target + metadata = each.value.metadata + } + + }) + + response_export_values = [] + schema_validation_enabled = false # Can be reverted once this is closed: https://github.com/Azure/terraform-provider-azapi/issues/524 + locks = [] + ignore_casing = false + ignore_missing_property = true +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/connectivity.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/connectivity.tf new file mode 100644 index 000000000..0cc24fcbb --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/connectivity.tf @@ -0,0 +1,39 @@ +resource "azurerm_private_endpoint" "private_endpoint_ai_studio_hub" { + name = "${azapi_resource.ai_studio_hub.name}-amlworkspace-pe" + location = var.location + resource_group_name = reverse(split("/", azapi_resource.ai_studio_hub.parent_id))[0] + tags = var.tags + + custom_network_interface_name = "${azapi_resource.ai_studio_hub.name}-amlworkspace-nic" + private_service_connection { + name = "${azapi_resource.ai_studio_hub.name}-amlworkspace-pe" + is_manual_connection = false + private_connection_resource_id = azapi_resource.ai_studio_hub.id + subresource_names = ["amlworkspace"] + } + subnet_id = var.subnet_id + dynamic "private_dns_zone_group" { + for_each = var.private_dns_zone_id_machine_learning_api == "" || var.private_dns_zone_id_machine_learning_notebooks == "" ? [] : [1] + content { + name = "${azapi_resource.ai_studio_hub.name}-arecord" + private_dns_zone_ids = [ + var.private_dns_zone_id_machine_learning_api, + var.private_dns_zone_id_machine_learning_notebooks + ] + } + } + + lifecycle { + ignore_changes = [ + private_dns_zone_group + ] + } +} + +resource "time_sleep" "sleep_connectivity" { + create_duration = "${var.connectivity_delay_in_seconds}s" + + depends_on = [ + azurerm_private_endpoint.private_endpoint_ai_studio_hub + ] +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/data.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/data.tf new file mode 100644 index 000000000..52162b709 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/data.tf @@ -0,0 +1,5 @@ +data "azurerm_client_config" "current" {} + +data "azurerm_monitor_diagnostic_categories" "diagnostic_categories_ai_studio_hub" { + resource_id = azapi_resource.ai_studio_hub.id +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/diagnostics.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/diagnostics.tf new file mode 100644 index 000000000..1824f5fe0 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/diagnostics.tf @@ -0,0 +1,29 @@ +resource "azurerm_monitor_diagnostic_setting" "diagnostic_setting_ai_studio_hub" { + for_each = { for index, value in var.diagnostics_configurations : + index => { + log_analytics_workspace_id = value.log_analytics_workspace_id, + storage_account_id = value.storage_account_id + } + } + name = "applicationLogs-${each.key}" + target_resource_id = azapi_resource.ai_studio_hub.id + log_analytics_workspace_id = each.value.log_analytics_workspace_id == "" ? null : each.value.log_analytics_workspace_id + storage_account_id = each.value.storage_account_id == "" ? null : each.value.storage_account_id + + dynamic "enabled_log" { + iterator = entry + for_each = data.azurerm_monitor_diagnostic_categories.diagnostic_categories_ai_studio_hub.log_category_groups + content { + category_group = entry.value + } + } + + dynamic "metric" { + iterator = entry + for_each = data.azurerm_monitor_diagnostic_categories.diagnostic_categories_ai_studio_hub.metrics + content { + category = entry.value + enabled = true + } + } +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/locals.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/locals.tf new file mode 100644 index 000000000..f61588270 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/locals.tf @@ -0,0 +1,14 @@ +locals { + # Encryption configuration + encryption = var.customer_managed_key == null ? null : { + keyVaultProperties = { + keyIdentifier = var.customer_managed_key.key_vault_key_versionless_id + keyVaultArmId = var.customer_managed_key.key_vault_id + identityClientId = var.customer_managed_key.user_assigned_identity_client_id + } + status = "Enabled" + identity = { + userAssignedIdentity = var.customer_managed_key.user_assigned_identity_id + } + } +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/main.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/main.tf new file mode 100644 index 000000000..4103f86e0 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/main.tf @@ -0,0 +1,64 @@ +resource "azapi_resource" "ai_studio_hub" { + type = "Microsoft.MachineLearningServices/workspaces@2024-04-01" + name = var.ai_studio_hub_name + location = var.location + parent_id = "/subscriptions/${data.azurerm_client_config.current.subscription_id}/resourcegroups/${var.resource_group_name}" + tags = var.tags + dynamic "identity" { + for_each = var.customer_managed_key != null ? [{ + type = "SystemAssigned, UserAssigned" + identity_ids = [ + var.customer_managed_key.user_assigned_identity_id + ] + }] : [{ + type = "SystemAssigned" + identity_ids = null + }] + content { + type = identity.value.type + identity_ids = identity.value.identity_ids + } + } + + body = jsonencode({ + kind = "Hub" + properties = { + applicationInsights = var.application_insights_id + containerRegistry = var.container_registry_id + keyVault = var.key_vault_id + storageAccount = var.storage_account_id + allowPublicAccessWhenBehindVnet = false + description = "Azure AI Studio Hub" + enableDataIsolation = true + encryption = local.encryption + friendlyName = title(replace(var.ai_studio_hub_name, "-", " ")) + hbiWorkspace = true + imageBuildCompute = null + ipAllowlist = [] + managedNetwork = { + isolationMode = "AllowOnlyApprovedOutbound" + outboundRules = {} # local.ai_studio_hub_outbound_rules # Will be managed using a separate module due to service limitations: https://github.com/PerfectThymeTech/terraform-azurerm-modules/tree/main/modules/aistudiooutboundrules + status = { + sparkReady = true + status = "Active" + } + } + primaryUserAssignedIdentity = null + publicNetworkAccess = "Disabled" + softDeleteRetentionInDays = 7 + systemDatastoresAuthMode = "identity" + v1LegacyMode = false + } + sku = { + name = "Basic" + tier = "Basic" + } + }) + + response_export_values = [] + schema_validation_enabled = false # Can be reverted once this is closed: https://github.com/Azure/terraform-provider-azapi/issues/524 + locks = [] + ignore_body_changes = ["properties.managedNetwork"] + ignore_casing = false + ignore_missing_property = true +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/outputs.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/outputs.tf new file mode 100644 index 000000000..8e1c43d02 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/outputs.tf @@ -0,0 +1,27 @@ +output "ai_studio_hub_id" { + description = "Specifies the resource id of the ai studio hub." + value = azapi_resource.ai_studio_hub.id + sensitive = false +} + +output "ai_studio_hub_name" { + description = "Specifies the name of the ai studio hub." + value = azapi_resource.ai_studio_hub.name + sensitive = true +} + +output "ai_studio_hub_principal_id" { + description = "Specifies the principal id of the ai studio hub." + value = azapi_resource.ai_studio_hub.identity[0].principal_id + sensitive = true +} + +output "ai_studio_hub_setup_completed" { + description = "Specifies whether the connectivity and identity has been successfully configured." + value = true + sensitive = false + + depends_on = [ + time_sleep.sleep_connectivity, + ] +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/terraform.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/terraform.tf new file mode 100644 index 000000000..08f74c85b --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/terraform.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">=0.12" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 3.100.0" + } + azapi = { + source = "Azure/azapi" + version = ">= 1.14.0" + } + time = { + source = "hashicorp/time" + version = ">= 0.9.1" + } + } +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/variables.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/variables.tf new file mode 100644 index 000000000..eb659b761 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/variables.tf @@ -0,0 +1,237 @@ +# General variables +variable "location" { + description = "Specifies the location of all resources." + type = string + sensitive = false + nullable = false +} + +variable "resource_group_name" { + description = "Specifies the resource group name in which all resources will get deployed." + type = string + sensitive = false + nullable = false + validation { + condition = length(var.resource_group_name) >= 2 + error_message = "Please specify a valid resource group name." + } +} + +variable "tags" { + description = "Specifies a key value map of tags to set on every taggable resources." + type = map(string) + sensitive = false + nullable = false + default = {} +} + +# AI studio hub variables +variable "ai_studio_hub_name" { + description = "Specifies the name of the ai studio hub." + type = string + sensitive = false + nullable = false +} + +variable "ai_studio_hub_provision_managed_network" { + description = "Specifies whether the managed vnet should be providioned as part of the ai studio hub deployment." + type = bool + sensitive = false + nullable = false + default = false +} + +variable "application_insights_id" { + description = "Specifies the id of application insights that will be connected to the ai studio hub." + type = string + sensitive = false + nullable = false + validation { + condition = length(split("/", var.application_insights_id)) == 9 + error_message = "Please specify a valid resource id." + } +} + +variable "container_registry_id" { + description = "Specifies the id of the container registry that will be connected to the ai studio hub." + type = string + sensitive = false + nullable = false + validation { + condition = length(split("/", var.container_registry_id)) == 9 + error_message = "Please specify a valid resource id." + } +} + +variable "key_vault_id" { + description = "Specifies the id of the key vaul that will be connected to the ai studio hub." + type = string + sensitive = false + nullable = false + validation { + condition = length(split("/", var.key_vault_id)) == 9 + error_message = "Please specify a valid resource id." + } +} + +variable "storage_account_id" { + description = "Specifies the id of the storage account that will be connected to the ai studio hub." + type = string + sensitive = false + nullable = false + validation { + condition = length(split("/", var.storage_account_id)) == 9 + error_message = "Please specify a valid resource id." + } +} + +# variable "ai_studio_hub_outbound_rules_fqdns" { # Will be managed using a separate module due to service limitations: https://github.com/PerfectThymeTech/terraform-azurerm-modules/tree/main/modules/aistudiooutboundrules +# description = "Specifies the outbound FQDN rules that should be added to the AI Studio Hub. Only provide FQDNs without specific paths such as 'microsoft.com' or '*.microsoft.com' but NOT 'microsoft.com/mypath'." +# type = list(string) +# sensitive = false +# default = [] +# validation { +# condition = alltrue([for outbound_rule_fqdn in toset(var.ai_studio_hub_outbound_rules_fqdns) : !strcontains(outbound_rule_fqdn, "/")]) +# error_message = "Please specify valid FQDNs without paths (e.g. '/'.)." +# } +# } + +# variable "ai_studio_hub_outbound_rules_private_endpoints" { # Will be managed using a separate module due to service limitations: https://github.com/PerfectThymeTech/terraform-azurerm-modules/tree/main/modules/aistudiooutboundrules +# description = "Specifies the private endpoint rules that should be added to the AI Studio Hub." +# type = list(object({ +# private_connection_resource_id = string +# subresource_name = string +# })) +# sensitive = false +# default = [] +# validation { +# condition = alltrue([ +# length([for outbound_rule_private_endpoint in toset(var.ai_studio_hub_outbound_rules_private_endpoints) : true if outbound_rule_private_endpoint.private_connection_resource_id == "" || outbound_rule_private_endpoint.subresource_name == ""]) <= 0 +# ]) +# error_message = "Please specify valid configurations." +# } +# } + +# variable "ai_studio_hub_outbound_rules_service_endpoints" { # Will be managed using a separate module due to service limitations: https://github.com/PerfectThymeTech/terraform-azurerm-modules/tree/main/modules/aistudiooutboundrules +# description = "Specifies the service endpoint rules that should be added to the AI Studio Hub." +# type = list(object({ +# service_tag = string +# protocol = optional(string, "TCP") +# port_range = optional(string, "443") +# })) +# sensitive = false +# default = [] +# validation { +# condition = alltrue([ +# length([for outbound_rule_service_endpoint in toset(var.ai_studio_hub_outbound_rules_service_endpoints) : true if outbound_rule_service_endpoint.service_tag == ""]) <= 0, +# length([for outbound_rule_service_endpoint in toset(var.ai_studio_hub_outbound_rules_service_endpoints) : true if !contains(["TCP", "UDP", "ICMP"], outbound_rule_service_endpoint.protocol)]) <= 0, +# ]) +# error_message = "Please specify valid configurations." +# } +# } + +variable "ai_studio_hub_connections" { + description = "Specifies the connections that should be added to the AI Studio Hub. Only provide connections to be shared with all projects at the hub level." + type = map(object({ + auth_type = optional(string, "AAD") + category = string + credentials = optional(any, null) + target = string + metadata = any + })) + sensitive = false + default = {} + validation { + condition = alltrue([ + length([for ai_studio_hub_connection in var.ai_studio_hub_connections : true if !contains(["AAD", "AccessKey", "AccountKey", "ApiKey", "CustomKeys", "ManagedIdentity", "None", "OAuth2", "PAT", "SAS", "ServicePrincipal", "UsernamePassword"], ai_studio_hub_connection.auth_type)]) <= 0, + length([for ai_studio_hub_connection in var.ai_studio_hub_connections : true if !contains(["ADLSGen2", "AzureBlob", "AzureDataExplorer", "AzureMariaDb", "AzureMySqlDb", "AzureOneLake", "AzureOpenAI", "AzurePostgresDb", "AzureSqlDb", "AzureSqlMi", "AzureSynapseAnalytics", "AzureTableStorage", "BingLLMSearch", "Cassandra", "CognitiveSearch", "CognitiveService", "ContainerRegistry", "CosmosDb", "CosmosDbMongoDbApi", "GenericContainerRegistry", "GenericHttp", "GenericRest", "Git", "ODataRest", "Odbc", "OpenAI", "PythonFeed", "Redis", ""], ai_studio_hub_connection.category)]) <= 0, + length([for ai_studio_hub_connection in var.ai_studio_hub_connections : true if !startswith(ai_studio_hub_connection.target, "https://")]) <= 0, + ]) + error_message = "Please specify valid connection configurations." + } +} + +# Diagnostics variables +variable "diagnostics_configurations" { + description = "Specifies the diagnostic configuration for the service." + type = list(object({ + log_analytics_workspace_id = optional(string, ""), + storage_account_id = optional(string, "") + })) + sensitive = false + default = [] + validation { + condition = alltrue([ + length([for diagnostics_configuration in toset(var.diagnostics_configurations) : diagnostics_configuration if diagnostics_configuration.log_analytics_workspace_id == "" && diagnostics_configuration.storage_account_id == ""]) <= 0 + ]) + error_message = "Please specify a valid resource ID." + } +} + +# Network variables +variable "subnet_id" { + description = "Specifies the resource id of a subnet in which the private endpoints get created." + type = string + sensitive = false + validation { + condition = length(split("/", var.subnet_id)) == 11 + error_message = "Please specify a valid subnet id." + } +} + +variable "connectivity_delay_in_seconds" { + description = "Specifies the delay in seconds after the private endpoint deployment (required for the DNS automation via Policies)." + type = number + sensitive = false + nullable = false + default = 120 + validation { + condition = var.connectivity_delay_in_seconds >= 0 + error_message = "Please specify a valid non-negative number." + } +} + +variable "private_dns_zone_id_machine_learning_api" { + description = "Specifies the resource ID of the private DNS zone for the Purview account. Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_machine_learning_api == "" || (length(split("/", var.private_dns_zone_id_machine_learning_api)) == 9 && endswith(var.private_dns_zone_id_machine_learning_api, "privatelink.api.azureml.ms")) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} + +variable "private_dns_zone_id_machine_learning_notebooks" { + description = "Specifies the resource ID of the private DNS zone for the Purview account. Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_machine_learning_notebooks == "" || (length(split("/", var.private_dns_zone_id_machine_learning_notebooks)) == 9 && endswith(var.private_dns_zone_id_machine_learning_notebooks, "privatelink.notebooks.azure.net")) + 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." + type = object({ + key_vault_id = string, + key_vault_key_versionless_id = string, + user_assigned_identity_id = string, + user_assigned_identity_client_id = string, + }) + sensitive = false + nullable = true + default = null + validation { + condition = alltrue([ + var.customer_managed_key == null || length(split("/", try(var.customer_managed_key.key_vault_id, ""))) == 9, + var.customer_managed_key == null || startswith(try(var.customer_managed_key.key_vault_key_versionless_id, ""), "https://"), + var.customer_managed_key == null || length(split("/", try(var.customer_managed_key.user_assigned_identity_id, ""))) == 9, + var.customer_managed_key == null || length(try(var.customer_managed_key.user_assigned_identity_client_id, "")) >= 2, + ]) + error_message = "Please specify a valid resource ID." + } +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/README.md b/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/README.md new file mode 100644 index 000000000..9eb351394 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/README.md @@ -0,0 +1,112 @@ + +# Azure AI Studio Hub Outbound Rules Terraform Module + +## Info for user-defined outbound rule management + +This module has been created to centrally manage user-defined outbound rules for an Azure AI Hub. + +New user-defined outbound rules can technically already be added individually via the `Microsoft.MachineLearningServices/workspaces/outboundRules` resource type. However, the next outbound rule in a list can only be added after the previous rule has been added successfully. Since, Terraform does not support a batch size parameter when creating resources using the `for_each` meta-argument, user-defined outbound rules must be added in batches and cannot be created individually. If you don't follow the batch approach, you will see unexpected behaviors and configuration results when adding a large number of rules individually via the `for_each` meta-argument. + +When working on larger projects, the outbound rules may also depend on other Terraform resources to be created. Due to these internal dependencies you may also require creating these outbound rules after creating the AI Hub. Hence, the outbound rules cannot be defined when creating the initial AI Hub resource. or you may end up with cyclic dependencies in your Terraform configuration. This can be avoided by using this Terraform module. + +This module has been created to manage all user-defined outbound rules centrally and apply the configuration in batches and overcome all aforementioned issues. + +## Documentation + + +## Requirements + +The following requirements are needed by this module: + +- [terraform](#requirement\_terraform) (>=0.12) + +- [azapi](#requirement\_azapi) (>= 1.14.0) + +## Modules + +No modules. + + + +## Required Inputs + +The following input variables are required: + +### [ai\_studio\_hub\_id](#input\_ai\_studio\_hub\_id) + +Description: Specifies the resource id of the ai studio hub. + +Type: `string` + +### [ai\_studio\_hub\_storage\_account\_id](#input\_ai\_studio\_hub\_storage\_account\_id) + +Description: Specifies the id of the storage account that is connected to the ai studio hub. + +Type: `string` + +## Optional Inputs + +The following input variables are optional (have default values): + +### [ai\_studio\_hub\_approve\_private\_endpoints](#input\_ai\_studio\_hub\_approve\_private\_endpoints) + +Description: Specifies whether the managed private endpoints should be approved as part of the ai studio hub deployment. + +Type: `bool` + +Default: `false` + +### [ai\_studio\_hub\_outbound\_rules\_fqdns](#input\_ai\_studio\_hub\_outbound\_rules\_fqdns) + +Description: Specifies the outbound FQDN rules that should be added to the AI Studio Hub. Only provide FQDNs without specific paths such as 'microsoft.com' or '*.microsoft.com' but NOT 'microsoft.com/mypath'. + +Type: `map(string)` + +Default: `{}` + +### [ai\_studio\_hub\_outbound\_rules\_private\_endpoints](#input\_ai\_studio\_hub\_outbound\_rules\_private\_endpoints) + +Description: Specifies the private endpoint rules that should be added to the AI Studio Hub. + +Type: + +```hcl +map(object({ + private_connection_resource_id = string + subresource_name = string + })) +``` + +Default: `{}` + +### [ai\_studio\_hub\_outbound\_rules\_service\_endpoints](#input\_ai\_studio\_hub\_outbound\_rules\_service\_endpoints) + +Description: Specifies the service endpoint rules that should be added to the AI Studio Hub. + +Type: + +```hcl +map(object({ + service_tag = string + protocol = optional(string, "TCP") + port_range = optional(string, "443") + })) +``` + +Default: `{}` + +### [ai\_studio\_hub\_provision\_managed\_network](#input\_ai\_studio\_hub\_provision\_managed\_network) + +Description: Specifies whether the managed vnet should be provisioned as part of the ai studio hub deployment. + +Type: `bool` + +Default: `false` + +## Outputs + +No outputs. + + + + \ No newline at end of file diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/README_footer.md b/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/README_footer.md new file mode 100644 index 000000000..e69de29bb diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/README_header.md b/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/README_header.md new file mode 100644 index 000000000..d3346172c --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/README_header.md @@ -0,0 +1,11 @@ +# Azure AI Studio Hub Outbound Rules Terraform Module + +## Info for user-defined outbound rule management + +This module has been created to centrally manage user-defined outbound rules for an Azure AI Hub. + +New user-defined outbound rules can technically already be added individually via the `Microsoft.MachineLearningServices/workspaces/outboundRules` resource type. However, the next outbound rule in a list can only be added after the previous rule has been added successfully. Since, Terraform does not support a batch size parameter when creating resources using the `for_each` meta-argument, user-defined outbound rules must be added in batches and cannot be created individually. If you don't follow the batch approach, you will see unexpected behaviors and configuration results when adding a large number of rules individually via the `for_each` meta-argument. + +When working on larger projects, the outbound rules may also depend on other Terraform resources to be created. Due to these internal dependencies you may also require creating these outbound rules after creating the AI Hub. Hence, the outbound rules cannot be defined when creating the initial AI Hub resource. or you may end up with cyclic dependencies in your Terraform configuration. This can be avoided by using this Terraform module. + +This module has been created to manage all user-defined outbound rules centrally and apply the configuration in batches and overcome all aforementioned issues. diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/locals.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/locals.tf new file mode 100644 index 000000000..cf43bc01d --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/locals.tf @@ -0,0 +1,129 @@ +locals { + # Outbound rules - private endpoints + default_ai_studio_hub_outbound_rules_private_endpoints = { + "default-storage-table" = { + private_connection_resource_id = var.ai_studio_hub_storage_account_id + subresource_name = "table" + }, + "default-storage-queue" = { + private_connection_resource_id = var.ai_studio_hub_storage_account_id + subresource_name = "queue" + } + } + ai_studio_hub_outbound_rules_private_endpoints = { + for key, value in merge(var.ai_studio_hub_outbound_rules_private_endpoints, local.default_ai_studio_hub_outbound_rules_private_endpoints) : + key => { + type = "PrivateEndpoint" + category = "UserDefined" + status = "Active" + destination = { + serviceResourceId = value.private_connection_resource_id + subresourceTarget = value.subresource_name + sparkEnabled = true + sparkStatus = "Active" + } + } + } + + # Outbound rules - service endpoints + default_ai_studio_hub_outbound_rules_service_endpoints = { # Related to AI Studio & AML samples + "AzureOpenDatasets-TCP-443" = { + service_tag = "AzureOpenDatasets" + protocol = "TCP" + port_range = "443" + } + } + ai_studio_hub_outbound_rules_service_endpoints = { + for key, value in merge(var.ai_studio_hub_outbound_rules_service_endpoints, local.default_ai_studio_hub_outbound_rules_service_endpoints) : + key => { + type = "ServiceTag" + category = "UserDefined" + destination = { + serviceTag = value.service_tag, + protocol = value.protocol, + portRanges = value.port_range, + action = "Allow" + }, + status = "Active" + } + } + + # Outbound rules - fqdns + default_ai_studio_hub_outbound_rules_fqdns = [ + # General dependencies + "graph.microsoft.com", + "*.aznbcontent.net", + "aka.ms", + "automlresources-prod.azureedge.net", + + # Required pypi dependencies + "pypi.org", + "pythonhosted.org", + "*.pythonhosted.org", + "anaconda.com", + "*.anaconda.com", + "*.anaconda.org", + "pytorch.org", + "*.pytorch.org", + "*.tensorflow.org", + + # Required R dependencies (Docs: https://learn.microsoft.com/en-us/azure/machine-learning/how-to-access-azureml-behind-firewall?view=azureml-api-2&tabs=ipaddress%2Cpublic#scenario-install-rstudio-on-compute-instance) + "cloud.r-project.org", + "ghcr.io", + "pkg-containers.githubusercontent.com", + + # VSCode dependencies (Docs: https://code.visualstudio.com/docs/setup/network#_common-hostnames) - Not all are needed in AML + "*.vscode.dev", + "vscode.blob.core.windows.net", + "*.gallerycdn.vsassets.io", + "raw.githubusercontent.com", + "*.vscode-unpkg.net", + "*.vscode-cdn.net", + "*.vscodeexperiments.azureedge.net", + "default.exp-tas.com", + "code.visualstudio.com", + "update.code.visualstudio.com", + "*.vo.msecnd.net", + "marketplace.visualstudio.com", + "vscode.download.prss.microsoft.com", + + # AI Studio & AML samples + "azclientextensionsync.blob.core.windows.net", + "azureexamples.blob.core.windows.net", + "azuremlexamples.blob.core.windows.net", + "openaipublic.blob.core.windows.net", + "notebiwesteurope.blob.core.windows.net", + "i40vsblobprodsu6weus59.blob.core.windows.net", + + # Model Catalogue Huggingface + "docker.io", + "*.docker.io", + "*.docker.com", + "production.cloudflare.docker.com", + "cdn.auth0.com", + "cdn-lfs.huggingface.co", + "huggingface.co", + + # Ubuntu dependencies --> Update with specific rules + "*.maven.org", + "archive.ubuntu.com", + "security.ubuntu.com", + "ppa.launchpad.net", + ] + default_ai_studio_hub_outbound_rules_fqdns_map = { + for item in toset(local.default_ai_studio_hub_outbound_rules_fqdns) : + replace(replace(item, "*", "all"), "/[^[:alnum:]]/", "-") => item + } + ai_studio_hub_outbound_rules_fqdns = { + for key, value in merge(var.ai_studio_hub_outbound_rules_fqdns, local.default_ai_studio_hub_outbound_rules_fqdns_map) : + key => { + category = "UserDefined" + type = "FQDN" + destination = value + status = "Active" + } + } + + # Merge rules + ai_studio_hub_outbound_rules = merge(local.ai_studio_hub_outbound_rules_private_endpoints, local.ai_studio_hub_outbound_rules_service_endpoints, local.ai_studio_hub_outbound_rules_fqdns) +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/main.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/main.tf new file mode 100644 index 000000000..830ca7ed5 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/main.tf @@ -0,0 +1,56 @@ +resource "azapi_update_resource" "ai_studio_hub_outbound_rules" { + type = "Microsoft.MachineLearningServices/workspaces@2024-04-01" + resource_id = var.ai_studio_hub_id + + body = jsonencode({ + properties = { + managedNetwork = { + isolationMode = "AllowOnlyApprovedOutbound" + outboundRules = local.ai_studio_hub_outbound_rules + status = { + sparkReady = true + status = "Active" + } + } + } + }) + + response_export_values = [] + locks = [] + ignore_casing = false + ignore_missing_property = false +} + +resource "azapi_resource_action" "ai_studio_hub_provision_managed_network" { + count = var.ai_studio_hub_provision_managed_network ? 1 : 0 + + type = "Microsoft.MachineLearningServices/workspaces@2024-04-01" + resource_id = var.ai_studio_hub_id + + action = "provisionManagedNetwork" + method = "POST" + body = jsonencode({ + includeSpark = true + }) + + response_export_values = [] + depends_on = [] +} + +resource "null_resource" "ai_studio_hub_private_endpoints_approval" { + for_each = var.ai_studio_hub_approve_private_endpoints ? local.ai_studio_hub_outbound_rules_private_endpoints : {} + + triggers = { + private_endpoint_name = "${each.key}" + } + provisioner "local-exec" { + working_dir = "${path.module}/scripts/" + interpreter = ["pwsh", "-Command"] + command = "./Approve-ManagedPrivateEndpoint.ps1 -ResourceId '${each.value.destination.serviceResourceId}' -ManagedPrivateEndpointName '${each.key}'" + } + + depends_on = [ + azapi_update_resource.ai_studio_hub_outbound_rules, + azapi_resource_action.ai_studio_hub_provision_managed_network, + ] +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/scripts/Approve-ManagedPrivateEndpoint.ps1 b/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/scripts/Approve-ManagedPrivateEndpoint.ps1 new file mode 100644 index 000000000..14bf36bbd --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/scripts/Approve-ManagedPrivateEndpoint.ps1 @@ -0,0 +1,108 @@ +# Define script arguments +[CmdletBinding()] +param ( + [Parameter(Mandatory = $true)] + [String] + $ResourceId, + + [Parameter(Mandatory = $true)] + [String] + $ManagedPrivateEndpointName, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [int] + $CheckIntervalInSeconds = 10, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [int] + $Retries = 10, + + [Parameter(Mandatory = $false, ValueFromRemainingArguments = $true)] + [string[]] + $Remaining +) + +# Change the ErrorActionPreference to 'Stop' to fail in case of an error +$ErrorActionPreference = "Stop" + +function Get-PrivateEndpointId { + param ( + [Parameter(Mandatory = $true)] + [String] + $ManagedPrivateEndpointName, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [int] + $CheckIntervalInSeconds = 10, + + [Parameter(Mandatory = $false)] + [ValidateNotNullOrEmpty()] + [int] + $Retries = 10 + ) + + # Initialize variable + $privateEndpointId = $null + + # Get Private Endpoint ID + for ($i = 0; $i -lt $Retries; $i++) { + $privateEndpointId = $(az network private-endpoint-connection list --id $ResourceId --query "[?contains(properties.privateEndpoint.id, '$ManagedPrivateEndpointName')].id | [0]" -o json) | ConvertFrom-Json + + if ($privateEndpointId) { + Write-Host "Private Endpoint found with id: '$($privateEndpointId)'. Continuing with approval." + break + } + Write-Host "Private Endpoint not found. Sleeping for $($CheckIntervalInSeconds) seconds ..." + Start-Sleep -Seconds $CheckIntervalInSeconds + } + + if (-not $privateEndpointId) { + Write-Error "Private Endpoint not found. Failed to approve Private Endpoint." + throw "Private Endpoint not found. Failed to approve Private Endpoint." + } + + return $privateEndpointId +} + +function Approve-PrivateEndpoint { + param ( + [Parameter(Mandatory = $true)] + [String] + $ManagedPrivateEndpointName, + + [Parameter(Mandatory = $true)] + [String] + $PrivateEndpointId + ) + + # Check status of private endpoint + Write-Host "Checking status of Private Endpoint" + $privateEndpointstatus = $(az network private-endpoint-connection list --id $ResourceId --query "[?contains(properties.privateEndpoint.id, '$ManagedPrivateEndpointName')].properties.privateLinkServiceConnectionState.status | [0]" -o json) | ConvertFrom-Json + + if ($privateEndpointStatus -eq "Approved") { + # Private Endpoint Connection already approved + Write-Host "Private Endpoint Connection already approved" + } + elseif ($privateEndpointStatus -eq "Failed") { + # Private Endpoint Connection has failed + Write-Error "Private Endpoint Connection has failed" + throw "Private Endpoint Connection has failed" + } + else { + # Approve Private Endpoint Connection + Write-Host "Approving Private Endpoint Connection" + az network private-endpoint-connection approve --id $PrivateEndpointId --description "Approved in Terraform" + } +} + +$privateEndpointId = Get-PrivateEndpointId ` + -ManagedPrivateEndpointName $ManagedPrivateEndpointName ` + -CheckIntervalInSeconds $CheckIntervalInSeconds ` + -Retries $Retries + +Approve-PrivateEndpoint ` + -ManagedPrivateEndpointName $ManagedPrivateEndpointName ` + -PrivateEndpointId $privateEndpointId diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/terraform.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/terraform.tf new file mode 100644 index 000000000..2b1dbcbde --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/terraform.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">=0.12" + + required_providers { + azapi = { + source = "Azure/azapi" + version = ">= 1.14.0" + } + } +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/variables.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/variables.tf new file mode 100644 index 000000000..d504003e6 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/variables.tf @@ -0,0 +1,91 @@ +# General variables + +# AI studio hub variables +variable "ai_studio_hub_id" { + description = "Specifies the resource id of the ai studio hub." + type = string + sensitive = false + nullable = false + validation { + condition = length(split("/", var.ai_studio_hub_id)) == 9 + error_message = "Please specify a valid resource id." + } +} + +variable "ai_studio_hub_storage_account_id" { + description = "Specifies the id of the storage account that is connected to the ai studio hub." + type = string + sensitive = false + nullable = false + validation { + condition = length(split("/", var.ai_studio_hub_storage_account_id)) == 9 + error_message = "Please specify a valid resource id." + } +} + +variable "ai_studio_hub_outbound_rules_fqdns" { + description = "Specifies the outbound FQDN rules that should be added to the AI Studio Hub. Only provide FQDNs without specific paths such as 'microsoft.com' or '*.microsoft.com' but NOT 'microsoft.com/mypath'." + type = map(string) + sensitive = false + default = {} + validation { + condition = alltrue([for outbound_rule_fqdn in var.ai_studio_hub_outbound_rules_fqdns : !strcontains(outbound_rule_fqdn, "/")]) + error_message = "Please specify valid FQDNs without paths (e.g. '/'.)." + } +} + +variable "ai_studio_hub_outbound_rules_private_endpoints" { + description = "Specifies the private endpoint rules that should be added to the AI Studio Hub." + type = map(object({ + private_connection_resource_id = string + subresource_name = string + })) + sensitive = false + default = {} + validation { + condition = alltrue([ + length([for outbound_rule_private_endpoint in var.ai_studio_hub_outbound_rules_private_endpoints : true if outbound_rule_private_endpoint.private_connection_resource_id == "" || outbound_rule_private_endpoint.subresource_name == ""]) <= 0 + ]) + error_message = "Please specify valid configurations." + } +} + +variable "ai_studio_hub_outbound_rules_service_endpoints" { + description = "Specifies the service endpoint rules that should be added to the AI Studio Hub." + type = map(object({ + service_tag = string + protocol = optional(string, "TCP") + port_range = optional(string, "443") + })) + sensitive = false + default = {} + validation { + condition = alltrue([ + length([for outbound_rule_service_endpoint in var.ai_studio_hub_outbound_rules_service_endpoints : true if outbound_rule_service_endpoint.service_tag == ""]) <= 0, + length([for outbound_rule_service_endpoint in var.ai_studio_hub_outbound_rules_service_endpoints : true if !contains(["TCP", "UDP", "ICMP"], outbound_rule_service_endpoint.protocol)]) <= 0, + ]) + error_message = "Please specify valid configurations." + } +} + +variable "ai_studio_hub_provision_managed_network" { + description = "Specifies whether the managed vnet should be provisioned as part of the ai studio hub deployment." + type = bool + sensitive = false + nullable = false + default = false +} + +variable "ai_studio_hub_approve_private_endpoints" { + description = "Specifies whether the managed private endpoints should be approved as part of the ai studio hub deployment." + type = bool + sensitive = false + nullable = false + default = false +} + +# Diagnostics variables + +# Network variables + +# Customer-managed key variables diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/README.md b/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/README.md new file mode 100644 index 000000000..21bd8527a --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/README.md @@ -0,0 +1,114 @@ + +# Azure AI Studio Project Terraform Module + +## Documentation + + +## Requirements + +The following requirements are needed by this module: + +- [terraform](#requirement\_terraform) (>=0.12) + +- [azapi](#requirement\_azapi) (>= 1.14.0) + +- [azurerm](#requirement\_azurerm) (>= 3.100.0) + +## Modules + +No modules. + + + +## Required Inputs + +The following input variables are required: + +### [ai\_studio\_hub\_id](#input\_ai\_studio\_hub\_id) + +Description: Specifies the resource id of the ai studio hub. + +Type: `string` + +### [ai\_studio\_project\_name](#input\_ai\_studio\_project\_name) + +Description: Specifies the name of the ai studio project. + +Type: `string` + +### [location](#input\_location) + +Description: Specifies the location of all resources. + +Type: `string` + +### [resource\_group\_name](#input\_resource\_group\_name) + +Description: Specifies the resource group name in which all resources will get deployed. + +Type: `string` + +## Optional Inputs + +The following input variables are optional (have default values): + +### [ai\_studio\_project\_connections](#input\_ai\_studio\_project\_connections) + +Description: Specifies the connections that should be added to the AI Studio Hub. Only provide connections to be shared with all projects at the hub level. + +Type: + +```hcl +map(object({ + auth_type = optional(string, "AAD") + category = string + credentials = optional(any, null) + target = string + metadata = any + })) +``` + +Default: `{}` + +### [diagnostics\_configurations](#input\_diagnostics\_configurations) + +Description: Specifies the diagnostic configuration for the service. + +Type: + +```hcl +list(object({ + log_analytics_workspace_id = optional(string, ""), + storage_account_id = optional(string, "") + })) +``` + +Default: `[]` + +### [tags](#input\_tags) + +Description: Specifies a key value map of tags to set on every taggable resources. + +Type: `map(string)` + +Default: `{}` + +## Outputs + +The following outputs are exported: + +### [ai\_studio\_project\_id](#output\_ai\_studio\_project\_id) + +Description: Specifies the resource ID of the ai studio project. + +### [ai\_studio\_project\_name](#output\_ai\_studio\_project\_name) + +Description: Specifies the name of the ai studio project. + +### [ai\_studio\_project\_principal\_id](#output\_ai\_studio\_project\_principal\_id) + +Description: Specifies the principal id of the ai studio project. + + + + \ No newline at end of file diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/README_footer.md b/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/README_footer.md new file mode 100644 index 000000000..e69de29bb diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/README_header.md b/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/README_header.md new file mode 100644 index 000000000..0807f0c3f --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/README_header.md @@ -0,0 +1 @@ +# Azure AI Studio Project Terraform Module diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/connections.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/connections.tf new file mode 100644 index 000000000..565b54145 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/connections.tf @@ -0,0 +1,26 @@ +resource "azapi_resource" "ai_studio_project_connection" { + for_each = var.ai_studio_project_connections + + type = "Microsoft.MachineLearningServices/workspaces/connections@2024-04-01" + name = each.key + parent_id = azapi_resource.ai_studio_project.id + + body = jsonencode({ + properties = { + authType = each.value.auth_type + category = each.value.category + credentials = each.value.credentials + isSharedToAll = false + sharedUserList = [] + target = each.value.target + metadata = each.value.metadata + } + + }) + + response_export_values = [] + schema_validation_enabled = false # Can be reverted once this is closed: https://github.com/Azure/terraform-provider-azapi/issues/524 + locks = [] + ignore_casing = false + ignore_missing_property = true +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/data.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/data.tf new file mode 100644 index 000000000..8e0377ee4 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/data.tf @@ -0,0 +1,5 @@ +data "azurerm_client_config" "current" {} + +data "azurerm_monitor_diagnostic_categories" "diagnostic_categories_ai_studio_project" { + resource_id = azapi_resource.ai_studio_project.id +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/diagnostics.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/diagnostics.tf new file mode 100644 index 000000000..e5e6e7491 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/diagnostics.tf @@ -0,0 +1,29 @@ +resource "azurerm_monitor_diagnostic_setting" "diagnostic_setting_application_insights" { + for_each = { for index, value in var.diagnostics_configurations : + index => { + log_analytics_workspace_id = value.log_analytics_workspace_id, + storage_account_id = value.storage_account_id + } + } + name = "applicationLogs-${each.key}" + target_resource_id = azapi_resource.ai_studio_project.id + log_analytics_workspace_id = each.value.log_analytics_workspace_id == "" ? null : each.value.log_analytics_workspace_id + storage_account_id = each.value.storage_account_id == "" ? null : each.value.storage_account_id + + dynamic "enabled_log" { + iterator = entry + for_each = data.azurerm_monitor_diagnostic_categories.diagnostic_categories_ai_studio_project.log_category_groups + content { + category_group = entry.value + } + } + + dynamic "metric" { + iterator = entry + for_each = data.azurerm_monitor_diagnostic_categories.diagnostic_categories_ai_studio_project.metrics + content { + category = entry.value + enabled = true + } + } +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/main.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/main.tf new file mode 100644 index 000000000..b34250d16 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/main.tf @@ -0,0 +1,26 @@ +resource "azapi_resource" "ai_studio_project" { + type = "Microsoft.MachineLearningServices/workspaces@2024-04-01" + name = var.ai_studio_project_name + location = var.location + parent_id = "/subscriptions/${data.azurerm_client_config.current.subscription_id}/resourcegroups/${var.resource_group_name}" + tags = var.tags + identity { + type = "SystemAssigned" + identity_ids = [] + } + + body = jsonencode({ + kind = "Project" + properties = { + description = "AI Studio Project - ${var.ai_studio_project_name}" + friendlyName = title(replace(var.ai_studio_project_name, "-", " ")) + hubResourceId = var.ai_studio_hub_id + } + }) + + response_export_values = [] + schema_validation_enabled = false # Can be reverted once this is closed: https://github.com/Azure/terraform-provider-azapi/issues/524 + locks = [] + ignore_casing = false + ignore_missing_property = true +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/outputs.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/outputs.tf new file mode 100644 index 000000000..793207500 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/outputs.tf @@ -0,0 +1,17 @@ +output "ai_studio_project_id" { + description = "Specifies the resource ID of the ai studio project." + value = azapi_resource.ai_studio_project.id + sensitive = false +} + +output "ai_studio_project_name" { + description = "Specifies the name of the ai studio project." + value = azapi_resource.ai_studio_project.name + sensitive = true +} + +output "ai_studio_project_principal_id" { + description = "Specifies the principal id of the ai studio project." + value = azapi_resource.ai_studio_project.identity[0].principal_id + sensitive = true +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/terraform.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/terraform.tf new file mode 100644 index 000000000..efcd86f1b --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/terraform.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">=0.12" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 3.100.0" + } + azapi = { + source = "Azure/azapi" + version = ">= 1.14.0" + } + } +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/variables.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/variables.tf new file mode 100644 index 000000000..665ec6dff --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/variables.tf @@ -0,0 +1,87 @@ +# General variables +variable "location" { + description = "Specifies the location of all resources." + type = string + sensitive = false + nullable = false +} + +variable "resource_group_name" { + description = "Specifies the resource group name in which all resources will get deployed." + type = string + sensitive = false + nullable = false + validation { + condition = length(var.resource_group_name) >= 2 + error_message = "Please specify a valid resource group name." + } +} + +variable "tags" { + description = "Specifies a key value map of tags to set on every taggable resources." + type = map(string) + sensitive = false + nullable = false + default = {} +} + +# AI Studio Project variables +variable "ai_studio_project_name" { + description = "Specifies the name of the ai studio project." + type = string + sensitive = false + nullable = false +} + +variable "ai_studio_hub_id" { + description = "Specifies the resource id of the ai studio hub." + type = string + sensitive = false + nullable = false + validation { + condition = length(split("/", var.ai_studio_hub_id)) == 9 + error_message = "Please specify a valid resource id." + } +} + +variable "ai_studio_project_connections" { + description = "Specifies the connections that should be added to the AI Studio Hub. Only provide connections to be shared with all projects at the hub level." + type = map(object({ + auth_type = optional(string, "AAD") + category = string + credentials = optional(any, null) + target = string + metadata = any + })) + sensitive = false + default = {} + validation { + condition = alltrue([ + length([for ai_studio_project_connection in var.ai_studio_project_connections : true if !contains(["AAD", "AccessKey", "AccountKey", "ApiKey", "CustomKeys", "ManagedIdentity", "None", "OAuth2", "PAT", "SAS", "ServicePrincipal", "UsernamePassword"], ai_studio_project_connection.auth_type)]) <= 0, + length([for ai_studio_project_connection in var.ai_studio_project_connections : true if !contains(["ADLSGen2", "AzureBlob", "AzureDataExplorer", "AzureMariaDb", "AzureMySqlDb", "AzureOneLake", "AzureOpenAI", "AzurePostgresDb", "AzureSqlDb", "AzureSqlMi", "AzureSynapseAnalytics", "AzureTableStorage", "BingLLMSearch", "Cassandra", "CognitiveSearch", "CognitiveService", "ContainerRegistry", "CosmosDb", "CosmosDbMongoDbApi", "GenericContainerRegistry", "GenericHttp", "GenericRest", "Git", "ODataRest", "Odbc", "OpenAI", "PythonFeed", "Redis", ""], ai_studio_project_connection.category)]) <= 0, + length([for ai_studio_project_connection in var.ai_studio_project_connections : true if !startswith(ai_studio_project_connection.target, "https://")]) <= 0, + ]) + error_message = "Please specify valid connection configurations." + } +} + +# Diagnostics variables +variable "diagnostics_configurations" { + description = "Specifies the diagnostic configuration for the service." + type = list(object({ + log_analytics_workspace_id = optional(string, ""), + storage_account_id = optional(string, "") + })) + sensitive = false + default = [] + validation { + condition = alltrue([ + length([for diagnostics_configuration in toset(var.diagnostics_configurations) : diagnostics_configuration if diagnostics_configuration.log_analytics_workspace_id == "" && diagnostics_configuration.storage_account_id == ""]) <= 0 + ]) + error_message = "Please specify a valid resource ID." + } +} + +# Network variables + +# Customer-managed key variables diff --git a/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/README.md b/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/README.md new file mode 100644 index 000000000..6e6d6931a --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/README.md @@ -0,0 +1,102 @@ + +# Azure Application Insights Terraform Module + +## Documentation + + +## Requirements + +The following requirements are needed by this module: + +- [terraform](#requirement\_terraform) (>=0.12) + +- [azurerm](#requirement\_azurerm) (>= 3.100.0) + +## Modules + +No modules. + + + +## Required Inputs + +The following input variables are required: + +### [application\_insights\_log\_analytics\_workspace\_id](#input\_application\_insights\_log\_analytics\_workspace\_id) + +Description: Specifies the log analytics workspace of the application insights service. + +Type: `string` + +### [application\_insights\_name](#input\_application\_insights\_name) + +Description: Specifies the name of the application insights service. + +Type: `string` + +### [location](#input\_location) + +Description: Specifies the location of all resources. + +Type: `string` + +### [resource\_group\_name](#input\_resource\_group\_name) + +Description: Specifies the resource group name in which all resources will get deployed. + +Type: `string` + +## Optional Inputs + +The following input variables are optional (have default values): + +### [application\_insights\_application\_type](#input\_application\_insights\_application\_type) + +Description: Specifies the application type of the application insights service. + +Type: `string` + +Default: `"web"` + +### [diagnostics\_configurations](#input\_diagnostics\_configurations) + +Description: Specifies the diagnostic configuration for the service. + +Type: + +```hcl +list(object({ + log_analytics_workspace_id = optional(string, ""), + storage_account_id = optional(string, "") + })) +``` + +Default: `[]` + +### [tags](#input\_tags) + +Description: Specifies a key value map of tags to set on every taggable resources. + +Type: `map(string)` + +Default: `{}` + +## Outputs + +The following outputs are exported: + +### [application\_insights\_connection\_string](#output\_application\_insights\_connection\_string) + +Description: Specifies the connection string of application insights. + +### [application\_insights\_id](#output\_application\_insights\_id) + +Description: Specifies the resource ID of application insights. + +### [application\_insights\_instrumentation\_key](#output\_application\_insights\_instrumentation\_key) + +Description: Specifies the instrumentation key of application insights. + + + + \ No newline at end of file diff --git a/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/README_footer.md b/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/README_footer.md new file mode 100644 index 000000000..e69de29bb diff --git a/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/README_header.md b/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/README_header.md new file mode 100644 index 000000000..cc545c7cf --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/README_header.md @@ -0,0 +1 @@ +# Azure Application Insights Terraform Module diff --git a/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/data.tf b/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/data.tf new file mode 100644 index 000000000..b36921001 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/data.tf @@ -0,0 +1,3 @@ +data "azurerm_monitor_diagnostic_categories" "diagnostic_categories_application_insights" { + resource_id = azurerm_application_insights.application_insights.id +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/diagnostics.tf b/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/diagnostics.tf new file mode 100644 index 000000000..9fc4972c2 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/diagnostics.tf @@ -0,0 +1,29 @@ +resource "azurerm_monitor_diagnostic_setting" "diagnostic_setting_application_insights" { + for_each = { for index, value in var.diagnostics_configurations : + index => { + log_analytics_workspace_id = value.log_analytics_workspace_id, + storage_account_id = value.storage_account_id + } + } + name = "applicationLogs-${each.key}" + target_resource_id = azurerm_application_insights.application_insights.id + log_analytics_workspace_id = each.value.log_analytics_workspace_id == "" ? null : each.value.log_analytics_workspace_id + storage_account_id = each.value.storage_account_id == "" ? null : each.value.storage_account_id + + dynamic "enabled_log" { + iterator = entry + for_each = data.azurerm_monitor_diagnostic_categories.diagnostic_categories_application_insights.log_category_groups + content { + category_group = entry.value + } + } + + dynamic "metric" { + iterator = entry + for_each = data.azurerm_monitor_diagnostic_categories.diagnostic_categories_application_insights.metrics + content { + category = entry.value + enabled = true + } + } +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/main.tf b/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/main.tf new file mode 100644 index 000000000..f62604d21 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/main.tf @@ -0,0 +1,17 @@ +resource "azurerm_application_insights" "application_insights" { + name = var.application_insights_name + location = var.location + resource_group_name = var.resource_group_name + tags = var.tags + + application_type = var.application_insights_application_type + daily_data_cap_notifications_disabled = false + disable_ip_masking = false + force_customer_storage_for_profiler = false + internet_ingestion_enabled = true + internet_query_enabled = true + local_authentication_disabled = false # Can be switched once AAD auth is supported + retention_in_days = 90 + sampling_percentage = 100 + workspace_id = var.application_insights_log_analytics_workspace_id +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/outputs.tf b/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/outputs.tf new file mode 100644 index 000000000..cb49b119f --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/outputs.tf @@ -0,0 +1,17 @@ +output "application_insights_id" { + description = "Specifies the resource ID of application insights." + value = azurerm_application_insights.application_insights.id + sensitive = false +} + +output "application_insights_instrumentation_key" { + description = "Specifies the instrumentation key of application insights." + value = azurerm_application_insights.application_insights.instrumentation_key + sensitive = true +} + +output "application_insights_connection_string" { + description = "Specifies the connection string of application insights." + value = azurerm_application_insights.application_insights.connection_string + sensitive = true +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/terraform.tf b/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/terraform.tf new file mode 100644 index 000000000..1bf28e3c7 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/terraform.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">=0.12" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 3.100.0" + } + } +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/variables.tf b/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/variables.tf new file mode 100644 index 000000000..aa2d44497 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/applicationinsights/variables.tf @@ -0,0 +1,72 @@ +# General variables +variable "location" { + description = "Specifies the location of all resources." + type = string + sensitive = false + nullable = false +} + +variable "resource_group_name" { + description = "Specifies the resource group name in which all resources will get deployed." + type = string + sensitive = false + nullable = false + validation { + condition = length(var.resource_group_name) >= 2 + error_message = "Please specify a valid resource group name." + } +} + +variable "tags" { + description = "Specifies a key value map of tags to set on every taggable resources." + type = map(string) + sensitive = false + nullable = false + default = {} +} + +# Application insights variables +variable "application_insights_name" { + description = "Specifies the name of the application insights service." + type = string + sensitive = false + nullable = false +} + +variable "application_insights_application_type" { + description = "Specifies the application type of the application insights service." + type = string + sensitive = false + nullable = false + default = "web" +} + +variable "application_insights_log_analytics_workspace_id" { + description = "Specifies the log analytics workspace of the application insights service." + type = string + sensitive = false + nullable = false + validation { + condition = length(split("/", var.application_insights_log_analytics_workspace_id)) == 9 + error_message = "Please specify a valid resource id." + } +} + +# Diagnostics variables +variable "diagnostics_configurations" { + description = "Specifies the diagnostic configuration for the service." + type = list(object({ + log_analytics_workspace_id = optional(string, ""), + storage_account_id = optional(string, "") + })) + sensitive = false + default = [] + validation { + condition = alltrue([ + length([for diagnostics_configuration in toset(var.diagnostics_configurations) : diagnostics_configuration if diagnostics_configuration.log_analytics_workspace_id == "" && diagnostics_configuration.storage_account_id == ""]) <= 0 + ]) + error_message = "Please specify a valid resource ID." + } +} + +# Network variables diff --git a/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/README.md b/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/README.md new file mode 100644 index 000000000..a760bc033 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/README.md @@ -0,0 +1,197 @@ + +# Azure Container Registry Terraform Module + +## Documentation + + +## Requirements + +The following requirements are needed by this module: + +- [terraform](#requirement\_terraform) (>=0.12) + +- [azurerm](#requirement\_azurerm) (>= 3.100.0) + +- [time](#requirement\_time) (>= 0.9.1) + +## Modules + +No modules. + + + +## Required Inputs + +The following input variables are required: + +### [container\_registry\_name](#input\_container\_registry\_name) + +Description: Specifies the name of the container registry. + +Type: `string` + +### [location](#input\_location) + +Description: Specifies the location of all resources. + +Type: `string` + +### [resource\_group\_name](#input\_resource\_group\_name) + +Description: Specifies the resource group name in which all resources will get deployed. + +Type: `string` + +### [subnet\_id](#input\_subnet\_id) + +Description: Specifies the resource id of a subnet in which the private endpoints get created. + +Type: `string` + +## Optional Inputs + +The following input variables are optional (have default values): + +### [connectivity\_delay\_in\_seconds](#input\_connectivity\_delay\_in\_seconds) + +Description: Specifies the delay in seconds after the private endpoint deployment (required for the DNS automation via Policies). + +Type: `number` + +Default: `120` + +### [container\_registry\_admin\_enabled](#input\_container\_registry\_admin\_enabled) + +Description: Specifies whether admin users should be enabled for the container registry. + +Type: `bool` + +Default: `false` + +### [container\_registry\_anonymous\_pull\_enabled](#input\_container\_registry\_anonymous\_pull\_enabled) + +Description: Specifies whether anonymous pull should be enabled for the container registry. + +Type: `bool` + +Default: `false` + +### [container\_registry\_data\_endpoint\_enabled](#input\_container\_registry\_data\_endpoint\_enabled) + +Description: Specifies whether data pull should be enabled for the container registry. + +Type: `bool` + +Default: `false` + +### [container\_registry\_export\_policy\_enabled](#input\_container\_registry\_export\_policy\_enabled) + +Description: Specifies whether export policy should be enabled for the container registry. + +Type: `bool` + +Default: `false` + +### [container\_registry\_quarantine\_policy\_enabled](#input\_container\_registry\_quarantine\_policy\_enabled) + +Description: Specifies whether quarantine policy should be enabled for the container registry. + +Type: `bool` + +Default: `false` + +### [container\_registry\_retention\_policy\_in\_days](#input\_container\_registry\_retention\_policy\_in\_days) + +Description: Specifies retention policy in days for the container registry. + +Type: `number` + +Default: `7` + +### [container\_registry\_trust\_policy\_enabled](#input\_container\_registry\_trust\_policy\_enabled) + +Description: Specifies whether trust policy should be enabled for the container registry. + +Type: `bool` + +Default: `false` + +### [container\_registry\_zone\_redundancy\_enabled](#input\_container\_registry\_zone\_redundancy\_enabled) + +Description: Specifies whether zone redundancy should be enabled for the container registry. + +Type: `bool` + +Default: `false` + +### [customer\_managed\_key](#input\_customer\_managed\_key) + +Description: Specifies the customer managed key configurations. + +Type: + +```hcl +object({ + key_vault_id = string, + key_vault_key_versionless_id = string, + user_assigned_identity_id = string, + user_assigned_identity_client_id = string, + }) +``` + +Default: `null` + +### [diagnostics\_configurations](#input\_diagnostics\_configurations) + +Description: Specifies the diagnostic configuration for the service. + +Type: + +```hcl +list(object({ + log_analytics_workspace_id = optional(string, ""), + storage_account_id = optional(string, "") + })) +``` + +Default: `[]` + +### [private\_dns\_zone\_id\_container\_registry](#input\_private\_dns\_zone\_id\_container\_registry) + +Description: Specifies the resource ID of the private DNS zone for the container registry. Not required if DNS A-records get created via Azure Policy. + +Type: `string` + +Default: `""` + +### [tags](#input\_tags) + +Description: Specifies a key value map of tags to set on every taggable resources. + +Type: `map(string)` + +Default: `{}` + +## Outputs + +The following outputs are exported: + +### [container\_registry\_id](#output\_container\_registry\_id) + +Description: Specifies the resource id of the container registry. + +### [container\_registry\_name](#output\_container\_registry\_name) + +Description: Specifies the resource name of the container registry. + +### [container\_registry\_principal\_id](#output\_container\_registry\_principal\_id) + +Description: Specifies the principal id of the container registry. + +### [container\_registry\_setup\_completed](#output\_container\_registry\_setup\_completed) + +Description: Specifies whether the connectivity and identity has been successfully configured. + + + + \ No newline at end of file diff --git a/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/README_footer.md b/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/README_footer.md new file mode 100644 index 000000000..e69de29bb diff --git a/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/README_header.md b/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/README_header.md new file mode 100644 index 000000000..ef0d2a213 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/README_header.md @@ -0,0 +1 @@ +# Azure Container Registry Terraform Module diff --git a/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/connectivity.tf b/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/connectivity.tf new file mode 100644 index 000000000..9616b8a12 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/connectivity.tf @@ -0,0 +1,38 @@ +resource "azurerm_private_endpoint" "container_registry_private_endpoint" { + name = "${azurerm_container_registry.container_registry.name}-registry-pe" + location = var.location + resource_group_name = azurerm_container_registry.container_registry.resource_group_name + tags = var.tags + + custom_network_interface_name = "${azurerm_container_registry.container_registry.name}-nic" + private_service_connection { + name = "${azurerm_container_registry.container_registry.name}-svc" + is_manual_connection = false + private_connection_resource_id = azurerm_container_registry.container_registry.id + subresource_names = ["registry"] + } + subnet_id = var.subnet_id + dynamic "private_dns_zone_group" { + for_each = var.private_dns_zone_id_container_registry == "" ? [] : [1] + content { + name = "${azurerm_container_registry.container_registry.name}-arecord" + private_dns_zone_ids = [ + var.private_dns_zone_id_container_registry + ] + } + } + + lifecycle { + ignore_changes = [ + private_dns_zone_group + ] + } +} + +resource "time_sleep" "sleep_connectivity" { + create_duration = "${var.connectivity_delay_in_seconds}s" + + depends_on = [ + azurerm_private_endpoint.container_registry_private_endpoint + ] +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/data.tf b/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/data.tf new file mode 100644 index 000000000..fe9f6414f --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/data.tf @@ -0,0 +1,3 @@ +data "azurerm_monitor_diagnostic_categories" "diagnostic_categories_container_registry" { + resource_id = azurerm_container_registry.container_registry.id +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/main.tf b/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/main.tf new file mode 100644 index 000000000..d964318a6 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/main.tf @@ -0,0 +1,57 @@ +resource "azurerm_container_registry" "container_registry" { + name = var.container_registry_name + location = var.location + resource_group_name = var.resource_group_name + tags = var.tags + dynamic "identity" { + for_each = var.customer_managed_key != null ? [{ + type = "SystemAssigned, UserAssigned" + identity_ids = [ + var.customer_managed_key.user_assigned_identity_id + ] + }] : [{ + type = "SystemAssigned" + identity_ids = null + }] + content { + type = identity.value.type + identity_ids = identity.value.identity_ids + } + } + + admin_enabled = var.container_registry_admin_enabled + anonymous_pull_enabled = var.container_registry_anonymous_pull_enabled + data_endpoint_enabled = var.container_registry_data_endpoint_enabled + dynamic "encryption" { + for_each = var.customer_managed_key != null ? [1] : [] + content { + enabled = true + identity_client_id = var.customer_managed_key.user_assigned_identity_client_id + key_vault_key_id = var.customer_managed_key.key_vault_key_versionless_id + } + } + export_policy_enabled = var.container_registry_export_policy_enabled + network_rule_bypass_option = "AzureServices" + network_rule_set = [ + { + default_action = "Deny" + ip_rule = [] + virtual_network = [] + } + ] + public_network_access_enabled = false + quarantine_policy_enabled = var.container_registry_quarantine_policy_enabled + retention_policy = [ + { + days = var.container_registry_retention_policy_in_days + enabled = true + } + ] + sku = "Premium" + trust_policy = [ + { + enabled = var.container_registry_trust_policy_enabled + } + ] + zone_redundancy_enabled = var.container_registry_zone_redundancy_enabled +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/outputs.tf b/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/outputs.tf new file mode 100644 index 000000000..41ece85ee --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/outputs.tf @@ -0,0 +1,27 @@ +output "container_registry_id" { + description = "Specifies the resource id of the container registry." + value = azurerm_container_registry.container_registry.id + sensitive = false +} + +output "container_registry_name" { + description = "Specifies the resource name of the container registry." + value = azurerm_container_registry.container_registry.name + sensitive = false +} + +output "container_registry_principal_id" { + description = "Specifies the principal id of the container registry." + value = azurerm_container_registry.container_registry.identity[0].principal_id + sensitive = false +} + +output "container_registry_setup_completed" { + description = "Specifies whether the connectivity and identity has been successfully configured." + value = true + sensitive = false + + depends_on = [ + time_sleep.sleep_connectivity, + ] +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/terraform.tf b/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/terraform.tf new file mode 100644 index 000000000..5dd9b37d3 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/terraform.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">=0.12" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 3.100.0" + } + time = { + source = "hashicorp/time" + version = ">= 0.9.1" + } + } +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/variables.tf b/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/variables.tf new file mode 100644 index 000000000..3a71e084c --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/containerregistry/variables.tf @@ -0,0 +1,177 @@ +# General variables +variable "location" { + description = "Specifies the location of all resources." + type = string + sensitive = false + nullable = false +} + +variable "resource_group_name" { + description = "Specifies the resource group name in which all resources will get deployed." + type = string + sensitive = false + nullable = false + validation { + condition = length(var.resource_group_name) >= 2 + error_message = "Please specify a valid resource group name." + } +} + +variable "tags" { + description = "Specifies a key value map of tags to set on every taggable resources." + type = map(string) + sensitive = false + default = {} + nullable = false +} + +# Container registry variables +variable "container_registry_name" { + description = "Specifies the name of the container registry." + type = string + sensitive = false + nullable = false + validation { + condition = length(var.container_registry_name) >= 2 && length(regexall("[^[:alnum:]]", var.container_registry_name)) <= 0 + error_message = "Please specify a valid name." + } +} + +variable "container_registry_admin_enabled" { + description = "Specifies whether admin users should be enabled for the container registry." + type = bool + sensitive = false + nullable = false + default = false +} + +variable "container_registry_anonymous_pull_enabled" { + description = "Specifies whether anonymous pull should be enabled for the container registry." + type = bool + sensitive = false + nullable = false + default = false +} + +variable "container_registry_data_endpoint_enabled" { + description = "Specifies whether data pull should be enabled for the container registry." + type = bool + sensitive = false + nullable = false + default = false +} + +variable "container_registry_export_policy_enabled" { + description = "Specifies whether export policy should be enabled for the container registry." + type = bool + sensitive = false + nullable = false + default = false +} + +variable "container_registry_quarantine_policy_enabled" { + description = "Specifies whether quarantine policy should be enabled for the container registry." + type = bool + sensitive = false + nullable = false + default = false +} + +variable "container_registry_retention_policy_in_days" { + description = "Specifies retention policy in days for the container registry." + type = number + sensitive = false + nullable = false + default = 7 +} + +variable "container_registry_trust_policy_enabled" { + description = "Specifies whether trust policy should be enabled for the container registry." + type = bool + sensitive = false + nullable = false + default = false +} + +variable "container_registry_zone_redundancy_enabled" { + description = "Specifies whether zone redundancy should be enabled for the container registry." + type = bool + sensitive = false + nullable = false + default = false +} + +# Diagnostics variables +variable "diagnostics_configurations" { + description = "Specifies the diagnostic configuration for the service." + type = list(object({ + log_analytics_workspace_id = optional(string, ""), + storage_account_id = optional(string, "") + })) + sensitive = false + nullable = false + default = [] + validation { + condition = alltrue([ + length([for diagnostics_configuration in toset(var.diagnostics_configurations) : diagnostics_configuration if diagnostics_configuration.log_analytics_workspace_id == "" && diagnostics_configuration.storage_account_id == ""]) <= 0 + ]) + error_message = "Please specify a valid resource ID." + } +} + +# Network variables +variable "subnet_id" { + description = "Specifies the resource id of a subnet in which the private endpoints get created." + type = string + sensitive = false + validation { + condition = length(split("/", var.subnet_id)) == 11 + error_message = "Please specify a valid subnet id." + } +} + +variable "connectivity_delay_in_seconds" { + description = "Specifies the delay in seconds after the private endpoint deployment (required for the DNS automation via Policies)." + type = number + sensitive = false + nullable = false + default = 120 + validation { + condition = var.connectivity_delay_in_seconds >= 0 + error_message = "Please specify a valid non-negative number." + } +} + +variable "private_dns_zone_id_container_registry" { + description = "Specifies the resource ID of the private DNS zone for the container registry. Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_container_registry == "" || (length(split("/", var.private_dns_zone_id_container_registry)) == 9 && endswith(var.private_dns_zone_id_container_registry, "privatelink.azurecr.io")) + 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." + type = object({ + key_vault_id = string, + key_vault_key_versionless_id = string, + user_assigned_identity_id = string, + user_assigned_identity_client_id = string, + }) + sensitive = false + nullable = true + default = null + validation { + condition = alltrue([ + var.customer_managed_key == null || length(split("/", try(var.customer_managed_key.key_vault_id, ""))) == 9, + var.customer_managed_key == null || startswith(try(var.customer_managed_key.key_vault_key_versionless_id, ""), "https://"), + var.customer_managed_key == null || length(split("/", try(var.customer_managed_key.user_assigned_identity_id, ""))) == 9, + var.customer_managed_key == null || length(try(var.customer_managed_key.user_assigned_identity_client_id, "")) >= 2, + ]) + error_message = "Please specify a valid resource ID." + } +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/keyvault/README.md b/quickstart/301-ai-studio-secure-e2e/modules/keyvault/README.md new file mode 100644 index 000000000..a01eda19e --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/keyvault/README.md @@ -0,0 +1,132 @@ + +# Azure Key Vault Terraform Module + +## Documentation + + +## Requirements + +The following requirements are needed by this module: + +- [terraform](#requirement\_terraform) (>=0.12) + +- [azurerm](#requirement\_azurerm) (>= 3.50.0) + +- [time](#requirement\_time) (>= 0.9.1) + +## Modules + +No modules. + + + +## Required Inputs + +The following input variables are required: + +### [key\_vault\_name](#input\_key\_vault\_name) + +Description: Specifies the name of the Key Vault. Changing this forces a new resource to be created. + +Type: `string` + +### [location](#input\_location) + +Description: Specifies the location of all resources. + +Type: `string` + +### [resource\_group\_name](#input\_resource\_group\_name) + +Description: Specifies the resource group name in which all resources will get deployed. + +Type: `string` + +### [subnet\_id](#input\_subnet\_id) + +Description: Specifies the resource id of a subnet in which the private endpoints get created. + +Type: `string` + +## Optional Inputs + +The following input variables are optional (have default values): + +### [connectivity\_delay\_in\_seconds](#input\_connectivity\_delay\_in\_seconds) + +Description: Specifies the delay in seconds after the private endpoint deployment (required for the DNS automation via Policies). + +Type: `number` + +Default: `120` + +### [diagnostics\_configurations](#input\_diagnostics\_configurations) + +Description: Specifies the diagnostic configuration for the service. + +Type: + +```hcl +list(object({ + log_analytics_workspace_id = optional(string, ""), + storage_account_id = optional(string, "") + })) +``` + +Default: `[]` + +### [key\_vault\_sku\_name](#input\_key\_vault\_sku\_name) + +Description: Specifies the name of the SKU used for this Key Vault. Possible values are standard and premium. + +Type: `string` + +Default: `"standard"` + +### [key\_vault\_soft\_delete\_retention\_days](#input\_key\_vault\_soft\_delete\_retention\_days) + +Description: Specifies the number of days that items should be retained for once soft-deleted. This value can be between 7 and 90 (the default) days. + +Type: `number` + +Default: `7` + +### [private\_dns\_zone\_id\_vault](#input\_private\_dns\_zone\_id\_vault) + +Description: Specifies the resource ID of the private DNS zone for Azure Key Vault. Not required if DNS A-records get created via Azure Policy. + +Type: `string` + +Default: `""` + +### [tags](#input\_tags) + +Description: Specifies a key value map of tags to set on every taggable resources. + +Type: `map(string)` + +Default: `{}` + +## Outputs + +The following outputs are exported: + +### [key\_vault\_id](#output\_key\_vault\_id) + +Description: Specifies the key vault resource id. + +### [key\_vault\_name](#output\_key\_vault\_name) + +Description: Specifies the key vault resource name. + +### [key\_vault\_setup\_completed](#output\_key\_vault\_setup\_completed) + +Description: Specifies whether the connectivity and identity has been successfully configured. + +### [key\_vault\_uri](#output\_key\_vault\_uri) + +Description: Specifies the key vault resource vault uri. + + + + \ No newline at end of file diff --git a/quickstart/301-ai-studio-secure-e2e/modules/keyvault/README_footer.md b/quickstart/301-ai-studio-secure-e2e/modules/keyvault/README_footer.md new file mode 100644 index 000000000..e69de29bb diff --git a/quickstart/301-ai-studio-secure-e2e/modules/keyvault/README_header.md b/quickstart/301-ai-studio-secure-e2e/modules/keyvault/README_header.md new file mode 100644 index 000000000..76ae1c6fa --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/keyvault/README_header.md @@ -0,0 +1 @@ +# Azure Key Vault Terraform Module diff --git a/quickstart/301-ai-studio-secure-e2e/modules/keyvault/connectivity.tf b/quickstart/301-ai-studio-secure-e2e/modules/keyvault/connectivity.tf new file mode 100644 index 000000000..987d57028 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/keyvault/connectivity.tf @@ -0,0 +1,38 @@ +resource "azurerm_private_endpoint" "private_endpoint_cognitive_account_vault" { + name = "${azurerm_key_vault.key_vault.name}-vault-pe" + location = azurerm_key_vault.key_vault.location + resource_group_name = azurerm_key_vault.key_vault.resource_group_name + tags = var.tags + + custom_network_interface_name = "${azurerm_key_vault.key_vault.name}-vault-nic" + private_service_connection { + name = "${azurerm_key_vault.key_vault.name}-vault-svc" + is_manual_connection = false + private_connection_resource_id = azurerm_key_vault.key_vault.id + subresource_names = ["vault"] + } + subnet_id = var.subnet_id + dynamic "private_dns_zone_group" { + for_each = var.private_dns_zone_id_vault == "" ? [] : [1] + content { + name = "${azurerm_key_vault.key_vault.name}-arecord" + private_dns_zone_ids = [ + var.private_dns_zone_id_vault + ] + } + } + + lifecycle { + ignore_changes = [ + private_dns_zone_group + ] + } +} + +resource "time_sleep" "sleep_connectivity" { + create_duration = "${var.connectivity_delay_in_seconds}s" + + depends_on = [ + azurerm_private_endpoint.private_endpoint_cognitive_account_vault + ] +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/keyvault/data.tf b/quickstart/301-ai-studio-secure-e2e/modules/keyvault/data.tf new file mode 100644 index 000000000..a9dc44571 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/keyvault/data.tf @@ -0,0 +1,5 @@ +data "azurerm_client_config" "current" {} + +data "azurerm_monitor_diagnostic_categories" "diagnostic_categories_key_vault" { + resource_id = azurerm_key_vault.key_vault.id +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/keyvault/diagnostics.tf b/quickstart/301-ai-studio-secure-e2e/modules/keyvault/diagnostics.tf new file mode 100644 index 000000000..954b3aee1 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/keyvault/diagnostics.tf @@ -0,0 +1,29 @@ +resource "azurerm_monitor_diagnostic_setting" "diagnostic_setting_key_vault" { + for_each = { for index, value in var.diagnostics_configurations : + index => { + log_analytics_workspace_id = value.log_analytics_workspace_id, + storage_account_id = value.storage_account_id + } + } + name = "applicationLogs-${each.key}" + target_resource_id = azurerm_key_vault.key_vault.id + log_analytics_workspace_id = each.value.log_analytics_workspace_id == "" ? null : each.value.log_analytics_workspace_id + storage_account_id = each.value.storage_account_id == "" ? null : each.value.storage_account_id + + dynamic "enabled_log" { + iterator = entry + for_each = data.azurerm_monitor_diagnostic_categories.diagnostic_categories_key_vault.log_category_groups + content { + category_group = entry.value + } + } + + dynamic "metric" { + iterator = entry + for_each = data.azurerm_monitor_diagnostic_categories.diagnostic_categories_key_vault.metrics + content { + category = entry.value + enabled = true + } + } +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/keyvault/main.tf b/quickstart/301-ai-studio-secure-e2e/modules/keyvault/main.tf new file mode 100644 index 000000000..30cf1bf5b --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/keyvault/main.tf @@ -0,0 +1,23 @@ +resource "azurerm_key_vault" "key_vault" { + name = var.key_vault_name + location = var.location + resource_group_name = var.resource_group_name + tags = var.tags + + access_policy = [] + enable_rbac_authorization = true + enabled_for_deployment = false + enabled_for_disk_encryption = false + enabled_for_template_deployment = false + network_acls { + bypass = "AzureServices" + default_action = "Deny" + ip_rules = [] + virtual_network_subnet_ids = [] + } + public_network_access_enabled = false + purge_protection_enabled = true + sku_name = var.key_vault_sku_name + soft_delete_retention_days = var.key_vault_soft_delete_retention_days + tenant_id = data.azurerm_client_config.current.tenant_id +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/keyvault/outputs.tf b/quickstart/301-ai-studio-secure-e2e/modules/keyvault/outputs.tf new file mode 100644 index 000000000..861756851 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/keyvault/outputs.tf @@ -0,0 +1,28 @@ +output "key_vault_id" { + description = "Specifies the key vault resource id." + value = azurerm_key_vault.key_vault.id + sensitive = false +} + +output "key_vault_name" { + description = "Specifies the key vault resource name." + value = azurerm_key_vault.key_vault.name + sensitive = false +} + +output "key_vault_uri" { + description = "Specifies the key vault resource vault uri." + value = azurerm_key_vault.key_vault.vault_uri + sensitive = false +} + +output "key_vault_setup_completed" { + description = "Specifies whether the connectivity and identity has been successfully configured." + value = true + sensitive = false + + depends_on = [ + azurerm_role_assignment.current_roleassignment_key_vault, + time_sleep.sleep_connectivity, + ] +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/keyvault/roleassignments.tf b/quickstart/301-ai-studio-secure-e2e/modules/keyvault/roleassignments.tf new file mode 100644 index 000000000..9ba58977c --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/keyvault/roleassignments.tf @@ -0,0 +1,5 @@ +resource "azurerm_role_assignment" "current_roleassignment_key_vault" { + scope = azurerm_key_vault.key_vault.id + role_definition_name = "Key Vault Administrator" + principal_id = data.azurerm_client_config.current.object_id +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/keyvault/terraform.tf b/quickstart/301-ai-studio-secure-e2e/modules/keyvault/terraform.tf new file mode 100644 index 000000000..ac15f3f59 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/keyvault/terraform.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">=0.12" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 3.50.0" + } + time = { + source = "hashicorp/time" + version = ">= 0.9.1" + } + } +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/keyvault/variables.tf b/quickstart/301-ai-studio-secure-e2e/modules/keyvault/variables.tf new file mode 100644 index 000000000..317c487ec --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/keyvault/variables.tf @@ -0,0 +1,108 @@ +# General variables +variable "location" { + description = "Specifies the location of all resources." + type = string + sensitive = false +} + +variable "resource_group_name" { + description = "Specifies the resource group name in which all resources will get deployed." + type = string + sensitive = false + validation { + condition = length(var.resource_group_name) >= 2 + error_message = "Please specify a valid resource group name." + } +} + +variable "tags" { + description = "Specifies a key value map of tags to set on every taggable resources." + type = map(string) + sensitive = false + default = {} +} + +# Key Vault variables +variable "key_vault_name" { + description = "Specifies the name of the Key Vault. Changing this forces a new resource to be created." + type = string + sensitive = false + validation { + condition = length(var.key_vault_name) >= 2 + error_message = "Please specify a valid name." + } +} + +variable "key_vault_sku_name" { + description = "Specifies the name of the SKU used for this Key Vault. Possible values are standard and premium." + type = string + sensitive = false + default = "standard" + validation { + condition = contains(["standard", "premium"], var.key_vault_sku_name) + error_message = "Please specify a valid key vault sku name." + } +} + +variable "key_vault_soft_delete_retention_days" { + description = "Specifies the number of days that items should be retained for once soft-deleted. This value can be between 7 and 90 (the default) days." + type = number + sensitive = false + default = 7 + validation { + condition = var.key_vault_soft_delete_retention_days >= 7 + error_message = "Please specify a valid key vault soft delete retention in days." + } +} + +# Diagnostics variables +variable "diagnostics_configurations" { + description = "Specifies the diagnostic configuration for the service." + type = list(object({ + log_analytics_workspace_id = optional(string, ""), + storage_account_id = optional(string, "") + })) + sensitive = false + default = [] + validation { + condition = alltrue([ + length([for diagnostics_configuration in toset(var.diagnostics_configurations) : diagnostics_configuration if diagnostics_configuration.log_analytics_workspace_id == "" && diagnostics_configuration.storage_account_id == ""]) <= 0 + ]) + error_message = "Please specify a valid resource ID." + } +} + +# Network variables +variable "subnet_id" { + description = "Specifies the resource id of a subnet in which the private endpoints get created." + type = string + sensitive = false + validation { + condition = length(split("/", var.subnet_id)) == 11 + error_message = "Please specify a valid subnet id." + } +} + +variable "connectivity_delay_in_seconds" { + description = "Specifies the delay in seconds after the private endpoint deployment (required for the DNS automation via Policies)." + type = number + sensitive = false + nullable = false + default = 120 + validation { + condition = var.connectivity_delay_in_seconds >= 0 + error_message = "Please specify a valid non-negative number." + } +} + +variable "private_dns_zone_id_vault" { + description = "Specifies the resource ID of the private DNS zone for Azure Key Vault. Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + nullable = false + default = "" + validation { + condition = var.private_dns_zone_id_vault == "" || (length(split("/", var.private_dns_zone_id_vault)) == 9 && endswith(var.private_dns_zone_id_vault, "privatelink.vaultcore.azure.net")) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/README.md b/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/README.md new file mode 100644 index 000000000..da7a20f21 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/README.md @@ -0,0 +1,92 @@ + +# Azure Log Analytics Terraform Module + +## Documentation + + +## Requirements + +The following requirements are needed by this module: + +- [terraform](#requirement\_terraform) (>=0.12) + +- [azurerm](#requirement\_azurerm) (>= 3.100.0) + +## Modules + +No modules. + + + +## Required Inputs + +The following input variables are required: + +### [location](#input\_location) + +Description: Specifies the location of all resources. + +Type: `string` + +### [log\_analytics\_workspace\_name](#input\_log\_analytics\_workspace\_name) + +Description: Specifies the name of the log analytics workspace. + +Type: `string` + +### [resource\_group\_name](#input\_resource\_group\_name) + +Description: Specifies the resource group name in which all resources will get deployed. + +Type: `string` + +## Optional Inputs + +The following input variables are optional (have default values): + +### [diagnostics\_configurations](#input\_diagnostics\_configurations) + +Description: Specifies the diagnostic configuration for the service. + +Type: + +```hcl +list(object({ + log_analytics_workspace_id = optional(string, ""), + storage_account_id = optional(string, "") + })) +``` + +Default: `[]` + +### [log\_analytics\_workspace\_retention\_in\_days](#input\_log\_analytics\_workspace\_retention\_in\_days) + +Description: Specifies the retention in days for the log analytics workspace. + +Type: `number` + +Default: `30` + +### [tags](#input\_tags) + +Description: Specifies a key value map of tags to set on every taggable resources. + +Type: `map(string)` + +Default: `{}` + +## Outputs + +The following outputs are exported: + +### [log\_analytics\_workspace\_id](#output\_log\_analytics\_workspace\_id) + +Description: Specifies the resource ID of the log analytics workspace. + +### [log\_analytics\_workspace\_name](#output\_log\_analytics\_workspace\_name) + +Description: Specifies the name of the log analytics workspace. + + + + \ No newline at end of file diff --git a/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/README_footer.md b/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/README_footer.md new file mode 100644 index 000000000..e69de29bb diff --git a/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/README_header.md b/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/README_header.md new file mode 100644 index 000000000..74ebfbe66 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/README_header.md @@ -0,0 +1 @@ +# Azure Log Analytics Terraform Module diff --git a/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/data.tf b/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/data.tf new file mode 100644 index 000000000..e239c5f2c --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/data.tf @@ -0,0 +1,3 @@ +data "azurerm_monitor_diagnostic_categories" "diagnostic_categories_log_analytics_workspace" { + resource_id = azurerm_log_analytics_workspace.log_analytics_workspace.id +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/diagnostics.tf b/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/diagnostics.tf new file mode 100644 index 000000000..d13093e87 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/diagnostics.tf @@ -0,0 +1,29 @@ +resource "azurerm_monitor_diagnostic_setting" "diagnostic_setting_log_analytics_workspace" { + for_each = { for index, value in var.diagnostics_configurations : + index => { + log_analytics_workspace_id = value.log_analytics_workspace_id, + storage_account_id = value.storage_account_id + } + } + name = "applicationLogs-${each.key}" + target_resource_id = azurerm_log_analytics_workspace.log_analytics_workspace.id + log_analytics_workspace_id = each.value.log_analytics_workspace_id == "" ? null : each.value.log_analytics_workspace_id + storage_account_id = each.value.storage_account_id == "" ? null : each.value.storage_account_id + + dynamic "enabled_log" { + iterator = entry + for_each = data.azurerm_monitor_diagnostic_categories.diagnostic_categories_log_analytics_workspace.log_category_groups + content { + category_group = entry.value + } + } + + dynamic "metric" { + iterator = entry + for_each = data.azurerm_monitor_diagnostic_categories.diagnostic_categories_log_analytics_workspace.metrics + content { + category = entry.value + enabled = true + } + } +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/main.tf b/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/main.tf new file mode 100644 index 000000000..21358dcc3 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/main.tf @@ -0,0 +1,17 @@ +resource "azurerm_log_analytics_workspace" "log_analytics_workspace" { + name = var.log_analytics_workspace_name + location = var.location + resource_group_name = var.resource_group_name + tags = var.tags + + allow_resource_only_permissions = true + cmk_for_query_forced = false + daily_quota_gb = -1 + immediate_data_purge_on_30_days_enabled = false + internet_ingestion_enabled = true + internet_query_enabled = true + local_authentication_disabled = true + retention_in_days = var.log_analytics_workspace_retention_in_days + reservation_capacity_in_gb_per_day = null + sku = "PerGB2018" +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/outputs.tf b/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/outputs.tf new file mode 100644 index 000000000..797fa9965 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/outputs.tf @@ -0,0 +1,11 @@ +output "log_analytics_workspace_id" { + description = "Specifies the resource ID of the log analytics workspace." + value = azurerm_log_analytics_workspace.log_analytics_workspace.id + sensitive = false +} + +output "log_analytics_workspace_name" { + description = "Specifies the name of the log analytics workspace." + value = azurerm_log_analytics_workspace.log_analytics_workspace.name + sensitive = true +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/terraform.tf b/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/terraform.tf new file mode 100644 index 000000000..1bf28e3c7 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/terraform.tf @@ -0,0 +1,10 @@ +terraform { + required_version = ">=0.12" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 3.100.0" + } + } +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/variables.tf b/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/variables.tf new file mode 100644 index 000000000..1a0edcdf4 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/loganalytics/variables.tf @@ -0,0 +1,64 @@ +# General variables +variable "location" { + description = "Specifies the location of all resources." + type = string + sensitive = false + nullable = false +} + +variable "resource_group_name" { + description = "Specifies the resource group name in which all resources will get deployed." + type = string + sensitive = false + nullable = false + validation { + condition = length(var.resource_group_name) >= 2 + error_message = "Please specify a valid resource group name." + } +} + +variable "tags" { + description = "Specifies a key value map of tags to set on every taggable resources." + type = map(string) + sensitive = false + nullable = false + default = {} +} + +# Log Analytics workspace variables +variable "log_analytics_workspace_name" { + description = "Specifies the name of the log analytics workspace." + type = string + sensitive = false + nullable = false +} + +variable "log_analytics_workspace_retention_in_days" { + description = "Specifies the retention in days for the log analytics workspace." + type = number + sensitive = false + default = 30 + validation { + condition = var.log_analytics_workspace_retention_in_days >= 30 && var.log_analytics_workspace_retention_in_days <= 730 + error_message = "Please specify a valid number of days." + } +} + +# Diagnostics variables +variable "diagnostics_configurations" { + description = "Specifies the diagnostic configuration for the service." + type = list(object({ + log_analytics_workspace_id = optional(string, ""), + storage_account_id = optional(string, "") + })) + sensitive = false + default = [] + validation { + condition = alltrue([ + length([for diagnostics_configuration in toset(var.diagnostics_configurations) : diagnostics_configuration if diagnostics_configuration.log_analytics_workspace_id == "" && diagnostics_configuration.storage_account_id == ""]) <= 0 + ]) + error_message = "Please specify a valid resource ID." + } +} + +# Network variables diff --git a/quickstart/301-ai-studio-secure-e2e/modules/prerequisites/prerequisites.tf b/quickstart/301-ai-studio-secure-e2e/modules/prerequisites/prerequisites.tf new file mode 100644 index 000000000..1b0c41db0 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/prerequisites/prerequisites.tf @@ -0,0 +1,170 @@ +# Variables +variable "location" { + description = "Specifies the location of all resources." + type = string + sensitive = false + nullable = false +} + +variable "tags" { + description = "Specifies a key value map of tags to set on every taggable resources." + type = map(string) + sensitive = false + nullable = false + default = {} +} + +variable "environment" { + description = "Specifies the environment of the deployment." + type = string + sensitive = false + default = "dev" + validation { + condition = contains(["dev", "tst", "qa", "prd"], var.environment) + error_message = "Please use an allowed value: \"dev\", \"tst\", \"qa\" or \"prd\"." + } +} + +variable "prefix" { + description = "Specifies the prefix for all resources created in this deployment." + type = string + sensitive = false + validation { + condition = length(var.prefix) >= 2 && length(var.prefix) <= 10 + error_message = "Please specify a prefix with more than two and less than 10 characters." + } +} + +variable "vnet_address_space" { + description = "Specifies the address space for the vnet created in this deployment." + type = string + sensitive = false + default = "10.0.0.0/24" + validation { + condition = try(cidrnetmask(var.address_space), "invalid") != "invalid" + error_message = "Please specify a valid CIDR range of size at least /28." + } +} + +# Locals +locals { + prefix = "${lower(var.prefix)}-${var.environment}" + + private_dns_zone_names = [ + # Storage + "privatelink.blob.core.windows.net", + "privatelink.file.core.windows.net", + "privatelink.table.core.windows.net", + "privatelink.queue.core.windows.net", + + # Container registry + "privatelink.azurecr.io", + + # Key vault + "privatelink.vaultcore.azure.net", + + # AI studio + "privatelink.api.azureml.ms", + "privatelink.notebooks.azure.net", + ] +} + +# Resources +resource "azurerm_resource_group" "resource_group" { + name = "rg-${local.prefix}" + location = var.location + tags = var.tags +} + +resource "azurerm_private_dns_zone" "private_dns_zones" { + for_each = toset(local.private_dns_zone_names) + + name = each.key + resource_group_name = azurerm_resource_group.resource_group.name + tags = var.tags +} + +resource "azurerm_private_dns_zone_virtual_network_link" "private_dns_zone_virtual_network_links" { + for_each = toset(local.private_dns_zone_names) + + name = each.key + resource_group_name = azurerm_private_dns_zone.private_dns_zones[each.key].resource_group_name + tags = var.tags + + private_dns_zone_name = azurerm_private_dns_zone.private_dns_zones[each.key].name + virtual_network_id = azurerm_virtual_network.virtual_network.id +} + +resource "azurerm_virtual_network" "virtual_network" { + name = "vnet-${local.prefix}" + location = var.location + resource_group_name = azurerm_resource_group.resource_group.name + tags = var.tags + + address_space = [var.vnet_address_space] +} + +resource "azurerm_subnet" "subnet" { + name = "PrivateEndpointSubnet" + virtual_network_name = azurerm_virtual_network.virtual_network.name + resource_group_name = azurerm_virtual_network.virtual_network.resource_group_name + + address_prefixes = [ + tostring(cidrsubnet(azurerm_virtual_network.virtual_network.address_space[0], 28 - tonumber(reverse(split("/", azurerm_virtual_network.virtual_network.address_space[0]))[0]), 1)) + ] +} + +# Outputs +output "subnet_id" { + description = "Specifies the resource id of the subnet." + value = azurerm_subnet.subnet.id + sensitive = false +} + +output "private_dns_zone_id_machine_learning_api" { + description = "Specifies the resource ID of the private DNS zone for machine learning." + value = azurerm_private_dns_zone.private_dns_zones["privatelink.api.azureml.ms"].id + sensitive = false +} + +output "private_dns_zone_id_machine_learning_notebooks" { + description = "Specifies the resource ID of the private DNS zone for machine learning notebooks." + value = azurerm_private_dns_zone.private_dns_zones["privatelink.notebooks.azure.net"].id + sensitive = false +} + +output "private_dns_zone_id_container_registry" { + description = "Specifies the resource ID of the private DNS zone for the container registry." + value = azurerm_private_dns_zone.private_dns_zones["privatelink.azurecr.io"].id + sensitive = false +} + +output "private_dns_zone_id_blob" { + description = "Specifies the resource ID of the private DNS zone for Azure Storage blob endpoints." + value = azurerm_private_dns_zone.private_dns_zones["privatelink.blob.core.windows.net"].id + sensitive = false +} + +output "private_dns_zone_id_file" { + description = "Specifies the resource ID of the private DNS zone for Azure Storage file endpoints." + value = azurerm_private_dns_zone.private_dns_zones["privatelink.file.core.windows.net"].id + sensitive = false +} + +output "private_dns_zone_id_table" { + description = "Specifies the resource ID of the private DNS zone for Azure Storage table endpoints." + value = azurerm_private_dns_zone.private_dns_zones["privatelink.table.core.windows.net"].id + sensitive = false +} + +output "private_dns_zone_id_queue" { + description = "Specifies the resource ID of the private DNS zone for Azure Storage queue endpoints." + value = azurerm_private_dns_zone.private_dns_zones["privatelink.queue.core.windows.net"].id + sensitive = false +} + +output "private_dns_zone_id_vault" { + description = "Specifies the resource ID of the private DNS zone for Azure Key Vault." + value = azurerm_private_dns_zone.private_dns_zones["privatelink.vaultcore.azure.net"].id + sensitive = false +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/storage/README.md b/quickstart/301-ai-studio-secure-e2e/modules/storage/README.md new file mode 100644 index 000000000..4854b4ac2 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/storage/README.md @@ -0,0 +1,382 @@ + +# Azure Storage Terraform Module + +## Documentation + + +## Requirements + +The following requirements are needed by this module: + +- [terraform](#requirement\_terraform) (>=0.12) + +- [azurerm](#requirement\_azurerm) (>= 3.50.0) + +- [time](#requirement\_time) (>= 0.9.1) + +## Modules + +No modules. + + + +## Required Inputs + +The following input variables are required: + +### [location](#input\_location) + +Description: Specifies the location of all resources. + +Type: `string` + +### [resource\_group\_name](#input\_resource\_group\_name) + +Description: Specifies the resource group name in which all resources will get deployed. + +Type: `string` + +### [storage\_account\_name](#input\_storage\_account\_name) + +Description: Specifies the name of the storage account. + +Type: `string` + +### [subnet\_id](#input\_subnet\_id) + +Description: Specifies the resource id of a subnet in which the private endpoints get created. + +Type: `string` + +## Optional Inputs + +The following input variables are optional (have default values): + +### [connectivity\_delay\_in\_seconds](#input\_connectivity\_delay\_in\_seconds) + +Description: Specifies the delay in seconds after the private endpoint deployment (required for the DNS automation via Policies). + +Type: `number` + +Default: `120` + +### [customer\_managed\_key](#input\_customer\_managed\_key) + +Description: Specifies the customer managed key configurations. + +Type: + +```hcl +object({ + key_vault_id = string, + key_vault_key_versionless_id = string, + user_assigned_identity_id = string, + user_assigned_identity_client_id = string, + }) +``` + +Default: `null` + +### [diagnostics\_configurations](#input\_diagnostics\_configurations) + +Description: Specifies the diagnostic configuration for the service. + +Type: + +```hcl +list(object({ + log_analytics_workspace_id = optional(string, ""), + storage_account_id = optional(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 Azure Policy. + +Type: `string` + +Default: `""` + +### [private\_dns\_zone\_id\_dfs](#input\_private\_dns\_zone\_id\_dfs) + +Description: Specifies the resource ID of the private DNS zone for Azure Storage dfs endpoints. Not required if DNS A-records get created via Azure Policy. + +Type: `string` + +Default: `""` + +### [private\_dns\_zone\_id\_file](#input\_private\_dns\_zone\_id\_file) + +Description: Specifies the resource ID of the private DNS zone for Azure Storage file endpoints. Not required if DNS A-records get created via Azure Policy. + +Type: `string` + +Default: `""` + +### [private\_dns\_zone\_id\_queue](#input\_private\_dns\_zone\_id\_queue) + +Description: Specifies the resource ID of the private DNS zone for Azure Storage queue endpoints. Not required if DNS A-records get created via Azure Policy. + +Type: `string` + +Default: `""` + +### [private\_dns\_zone\_id\_table](#input\_private\_dns\_zone\_id\_table) + +Description: Specifies the resource ID of the private DNS zone for Azure Storage table endpoints. Not required if DNS A-records get created via Azure Policy. + +Type: `string` + +Default: `""` + +### [private\_dns\_zone\_id\_web](#input\_private\_dns\_zone\_id\_web) + +Description: Specifies the resource ID of the private DNS zone for Azure Storage web endpoints. Not required if DNS A-records get created via Azure Policy. + +Type: `string` + +Default: `""` + +### [private\_endpoint\_subresource\_names](#input\_private\_endpoint\_subresource\_names) + +Description: Specifies a list of group ids for which private endpoints will be created (e.g. 'blob', 'file', etc.). If sub resource is defined a private endpoint will be created. + +Type: `set(string)` + +Default: + +```json +[ + "blob" +] +``` + +### [storage\_access\_tier](#input\_storage\_access\_tier) + +Description: Specifies the access tier of the storage account. Valid options are 'Hot' and 'Cool' (Default: 'Hot'). + +Type: `string` + +Default: `"Hot"` + +### [storage\_account\_replication\_type](#input\_storage\_account\_replication\_type) + +Description: Specifies the replication type of the storage account. + +Type: `string` + +Default: `"ZRS"` + +### [storage\_account\_tier](#input\_storage\_account\_tier) + +Description: Specifies the account tier of the storage account. + +Type: `string` + +Default: `"Standard"` + +### [storage\_account\_type](#input\_storage\_account\_type) + +Description: Specifies the account tier of the storage account. + +Type: `string` + +Default: `"StorageV2"` + +### [storage\_blob\_change\_feed\_enabled](#input\_storage\_blob\_change\_feed\_enabled) + +Description: Specifies whether the blob change feed should be enabled. + +Type: `bool` + +Default: `false` + +### [storage\_blob\_container\_delete\_retention\_in\_days](#input\_storage\_blob\_container\_delete\_retention\_in\_days) + +Description: Specifies the blob container delete retention policy in days (soft-delete). + +Type: `number` + +Default: `7` + +### [storage\_blob\_cors\_rules](#input\_storage\_blob\_cors\_rules) + +Description: Specifies storage account blob cors rules. + +Type: + +```hcl +map(object({ + allowed_headers = list(string) + allowed_methods = list(string) + allowed_origins = list(string) + exposed_headers = list(string) + max_age_in_seconds = optional(number, 1800) + })) +``` + +Default: `{}` + +### [storage\_blob\_delete\_retention\_in\_days](#input\_storage\_blob\_delete\_retention\_in\_days) + +Description: Specifies the blob delete retention policy in days (soft-delete). + +Type: `number` + +Default: `7` + +### [storage\_blob\_last\_access\_time\_enabled](#input\_storage\_blob\_last\_access\_time\_enabled) + +Description: Specifies whether the blob last access time feature should be enabled. + +Type: `bool` + +Default: `false` + +### [storage\_blob\_versioning\_enabled](#input\_storage\_blob\_versioning\_enabled) + +Description: Specifies whether the blob versioning feature should be enabled. + +Type: `bool` + +Default: `false` + +### [storage\_container\_names](#input\_storage\_container\_names) + +Description: Specifies the storage container names to create within the storage account. + +Type: `list(string)` + +Default: `[]` + +### [storage\_is\_hns\_enabled](#input\_storage\_is\_hns\_enabled) + +Description: Specifies whetehr hierarchical namespace (Data Lake) should be enabled. + +Type: `bool` + +Default: `false` + +### [storage\_network\_bypass](#input\_storage\_network\_bypass) + +Description: Specifies bypass options for the storage account network rules. List can include "None", "AzureServices", "Metrics" and "Logs" + +Type: `set(string)` + +Default: + +```json +[ + "None" +] +``` + +### [storage\_network\_private\_link\_access](#input\_storage\_network\_private\_link\_access) + +Description: Specifies resource instance rules of the storage account. + +Type: `set(string)` + +Default: `[]` + +### [storage\_nfsv3\_enabled](#input\_storage\_nfsv3\_enabled) + +Description: Specifies whether NFSv3 should be enabled. + +Type: `bool` + +Default: `false` + +### [storage\_public\_network\_access\_enabled](#input\_storage\_public\_network\_access\_enabled) + +Description: Specifies whether the storage account should have public network access. + +Type: `bool` + +Default: `true` + +### [storage\_sftp\_enabled](#input\_storage\_sftp\_enabled) + +Description: Specifies whether the storage account should enable sftp. + +Type: `bool` + +Default: `false` + +### [storage\_shared\_access\_key\_enabled](#input\_storage\_shared\_access\_key\_enabled) + +Description: Specifies whether the storage account should enable the shared key for access. + +Type: `bool` + +Default: `false` + +### [storage\_static\_website](#input\_storage\_static\_website) + +Description: Specifies the website configuration to enable static websites on the storage account. + +Type: + +```hcl +list(object({ + index_document = string + error_404_document = string + })) +``` + +Default: `[]` + +### [tags](#input\_tags) + +Description: Specifies a key value map of tags to set on every taggable resources. + +Type: `map(string)` + +Default: `{}` + +## Outputs + +The following outputs are exported: + +### [storage\_account\_id](#output\_storage\_account\_id) + +Description: Specifies the resource id of the storage account. + +### [storage\_account\_name](#output\_storage\_account\_name) + +Description: Specifies the resource name of the storage account. + +### [storage\_account\_primary\_blob\_endpoint](#output\_storage\_account\_primary\_blob\_endpoint) + +Description: Specifies the primary blob endpoint of the storage account. + +### [storage\_account\_primary\_dfs\_endpoint](#output\_storage\_account\_primary\_dfs\_endpoint) + +Description: Specifies the primary dfs endpoint of the storage account. + +### [storage\_account\_primary\_file\_endpoint](#output\_storage\_account\_primary\_file\_endpoint) + +Description: Specifies the primary file endpoint of the storage account. + +### [storage\_account\_primary\_queue\_endpoint](#output\_storage\_account\_primary\_queue\_endpoint) + +Description: Specifies the primary queue endpoint of the storage account. + +### [storage\_account\_primary\_table\_endpoint](#output\_storage\_account\_primary\_table\_endpoint) + +Description: Specifies the primary table endpoint of the storage account. + +### [storage\_account\_primary\_web\_endpoint](#output\_storage\_account\_primary\_web\_endpoint) + +Description: Specifies the primary web endpoint of the storage account. + +### [storage\_setup\_completed](#output\_storage\_setup\_completed) + +Description: Specifies whether the connectivity and identity has been successfully configured. + + + + \ No newline at end of file diff --git a/quickstart/301-ai-studio-secure-e2e/modules/storage/README_footer.md b/quickstart/301-ai-studio-secure-e2e/modules/storage/README_footer.md new file mode 100644 index 000000000..e69de29bb diff --git a/quickstart/301-ai-studio-secure-e2e/modules/storage/README_header.md b/quickstart/301-ai-studio-secure-e2e/modules/storage/README_header.md new file mode 100644 index 000000000..36db39c38 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/storage/README_header.md @@ -0,0 +1 @@ +# Azure Storage Terraform Module diff --git a/quickstart/301-ai-studio-secure-e2e/modules/storage/connectivity.tf b/quickstart/301-ai-studio-secure-e2e/modules/storage/connectivity.tf new file mode 100644 index 000000000..b98d5e415 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/storage/connectivity.tf @@ -0,0 +1,40 @@ +resource "azurerm_private_endpoint" "private_endpoint_storage_account" { + for_each = var.private_endpoint_subresource_names + + name = "${azurerm_storage_account.storage_account.name}-${each.value}-pe" + location = var.location + resource_group_name = azurerm_storage_account.storage_account.resource_group_name + tags = var.tags + + custom_network_interface_name = "${azurerm_storage_account.storage_account.name}-${each.value}-nic" + private_service_connection { + name = "${azurerm_storage_account.storage_account.name}-${each.value}-svc" + is_manual_connection = false + private_connection_resource_id = azurerm_storage_account.storage_account.id + subresource_names = [each.value] + } + subnet_id = var.subnet_id + dynamic "private_dns_zone_group" { + for_each = local.private_dns_zones_map[each.value] == "" ? [] : [1] + content { + name = "${azurerm_storage_account.storage_account.name}-arecord" + private_dns_zone_ids = [ + local.private_dns_zones_map[each.value] + ] + } + } + + lifecycle { + ignore_changes = [ + private_dns_zone_group + ] + } +} + +resource "time_sleep" "sleep_connectivity" { + create_duration = "${var.connectivity_delay_in_seconds}s" + + depends_on = [ + azurerm_private_endpoint.private_endpoint_storage_account + ] +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/storage/data.tf b/quickstart/301-ai-studio-secure-e2e/modules/storage/data.tf new file mode 100644 index 000000000..d6b91be4e --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/storage/data.tf @@ -0,0 +1,5 @@ +data "azurerm_client_config" "current" {} + +data "azurerm_monitor_diagnostic_categories" "diagnostic_categories_storage_account" { + resource_id = azurerm_storage_account.storage_account.id +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/storage/diagnostics.tf b/quickstart/301-ai-studio-secure-e2e/modules/storage/diagnostics.tf new file mode 100644 index 000000000..5265c6d08 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/storage/diagnostics.tf @@ -0,0 +1,29 @@ +resource "azurerm_monitor_diagnostic_setting" "diagnostic_setting_storage_account" { + for_each = { for index, value in var.diagnostics_configurations : + index => { + log_analytics_workspace_id = value.log_analytics_workspace_id, + storage_account_id = value.storage_account_id + } + } + name = "applicationLogs-${each.key}" + target_resource_id = azurerm_storage_account.storage_account.id + log_analytics_workspace_id = each.value.log_analytics_workspace_id == "" ? null : each.value.log_analytics_workspace_id + storage_account_id = each.value.storage_account_id == "" ? null : each.value.storage_account_id + + dynamic "enabled_log" { + iterator = entry + for_each = data.azurerm_monitor_diagnostic_categories.diagnostic_categories_storage_account.log_category_groups + content { + category_group = entry.value + } + } + + dynamic "metric" { + iterator = entry + for_each = data.azurerm_monitor_diagnostic_categories.diagnostic_categories_storage_account.metrics + content { + category = entry.value + enabled = true + } + } +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/storage/locals.tf b/quickstart/301-ai-studio-secure-e2e/modules/storage/locals.tf new file mode 100644 index 000000000..67fd33cff --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/storage/locals.tf @@ -0,0 +1,10 @@ +locals { + private_dns_zones_map = { + blob = var.private_dns_zone_id_blob + file = var.private_dns_zone_id_file + table = var.private_dns_zone_id_table + queue = var.private_dns_zone_id_queue + web = var.private_dns_zone_id_web + dfs = var.private_dns_zone_id_dfs + } +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/storage/main.tf b/quickstart/301-ai-studio-secure-e2e/modules/storage/main.tf new file mode 100644 index 000000000..084c1c84e --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/storage/main.tf @@ -0,0 +1,113 @@ +resource "azurerm_storage_account" "storage_account" { + name = var.storage_account_name + location = var.location + resource_group_name = var.resource_group_name + tags = var.tags + dynamic "identity" { + for_each = var.customer_managed_key != null ? [1] : [] + content { + type = "UserAssigned" + identity_ids = [ + var.customer_managed_key.user_assigned_identity_id + ] + } + } + + access_tier = var.storage_access_tier + account_kind = var.storage_account_type + account_tier = var.storage_account_tier + account_replication_type = var.storage_account_replication_type + allow_nested_items_to_be_public = false + allowed_copy_scope = "AAD" + dynamic "static_website" { + for_each = var.storage_static_website + content { + index_document = static_website.value.index_document + error_404_document = static_website.value.error_404_document + } + } + blob_properties { + change_feed_enabled = var.storage_blob_change_feed_enabled + container_delete_retention_policy { + days = var.storage_blob_container_delete_retention_in_days + } + delete_retention_policy { + days = var.storage_blob_delete_retention_in_days + } + dynamic "cors_rule" { + for_each = var.storage_blob_cors_rules + content { + allowed_headers = cors_rule.value.allowed_headers + allowed_methods = cors_rule.value.allowed_methods + allowed_origins = cors_rule.value.allowed_origins + exposed_headers = cors_rule.value.exposed_headers + max_age_in_seconds = cors_rule.value.max_age_in_seconds + } + } + last_access_time_enabled = var.storage_blob_last_access_time_enabled + versioning_enabled = var.storage_blob_versioning_enabled + } + cross_tenant_replication_enabled = false + default_to_oauth_authentication = true + enable_https_traffic_only = true + infrastructure_encryption_enabled = true + is_hns_enabled = var.storage_is_hns_enabled + large_file_share_enabled = false + min_tls_version = "TLS1_2" + network_rules { + default_action = "Deny" + bypass = var.storage_network_bypass + ip_rules = [] + virtual_network_subnet_ids = [] + dynamic "private_link_access" { + for_each = var.storage_network_private_link_access + content { + endpoint_resource_id = private_link_access.value + endpoint_tenant_id = data.azurerm_client_config.current.tenant_id + } + } + } + nfsv3_enabled = var.storage_nfsv3_enabled + public_network_access_enabled = var.storage_public_network_access_enabled + queue_encryption_key_type = "Account" + table_encryption_key_type = "Account" + routing { + choice = "MicrosoftRouting" + publish_internet_endpoints = false + publish_microsoft_endpoints = false + } + sas_policy { + expiration_period = "1.00:00:00" + expiration_action = "Log" + } + sftp_enabled = var.storage_sftp_enabled + shared_access_key_enabled = var.storage_shared_access_key_enabled + + lifecycle { + ignore_changes = [ + customer_managed_key + ] + } +} + +resource "azurerm_storage_container" "storage_container" { + for_each = toset(var.storage_container_names) + + storage_account_name = azurerm_storage_account.storage_account.name + name = each.key + container_access_type = "private" + + depends_on = [ + azurerm_role_assignment.current_roleassignment_storage_blob_data_owner, + time_sleep.sleep_connectivity, + ] +} + +resource "azurerm_storage_account_customer_managed_key" "storage_account_customer_managed_key" { + count = var.customer_managed_key != null ? 1 : 0 + + storage_account_id = azurerm_storage_account.storage_account.id + key_vault_id = var.customer_managed_key.key_vault_id + key_name = reverse(split("/", var.customer_managed_key.key_vault_key_versionless_id))[0] + user_assigned_identity_id = var.customer_managed_key.user_assigned_identity_id +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/storage/outputs.tf b/quickstart/301-ai-studio-secure-e2e/modules/storage/outputs.tf new file mode 100644 index 000000000..380db10f6 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/storage/outputs.tf @@ -0,0 +1,58 @@ +output "storage_account_id" { + description = "Specifies the resource id of the storage account." + value = azurerm_storage_account.storage_account.id + sensitive = false +} + +output "storage_account_name" { + description = "Specifies the resource name of the storage account." + value = azurerm_storage_account.storage_account.name + sensitive = false +} + +output "storage_account_primary_blob_endpoint" { + description = "Specifies the primary blob endpoint of the storage account." + value = azurerm_storage_account.storage_account.primary_blob_endpoint + sensitive = false +} + +output "storage_account_primary_file_endpoint" { + description = "Specifies the primary file endpoint of the storage account." + value = azurerm_storage_account.storage_account.primary_file_endpoint + sensitive = false +} + +output "storage_account_primary_queue_endpoint" { + description = "Specifies the primary queue endpoint of the storage account." + value = azurerm_storage_account.storage_account.primary_queue_endpoint + sensitive = false +} + +output "storage_account_primary_table_endpoint" { + description = "Specifies the primary table endpoint of the storage account." + value = azurerm_storage_account.storage_account.primary_table_endpoint + sensitive = false +} + +output "storage_account_primary_web_endpoint" { + description = "Specifies the primary web endpoint of the storage account." + value = azurerm_storage_account.storage_account.primary_web_endpoint + sensitive = false +} + +output "storage_account_primary_dfs_endpoint" { + description = "Specifies the primary dfs endpoint of the storage account." + value = azurerm_storage_account.storage_account.primary_dfs_endpoint + sensitive = false +} + +output "storage_setup_completed" { + description = "Specifies whether the connectivity and identity has been successfully configured." + value = true + sensitive = false + + depends_on = [ + azurerm_role_assignment.current_roleassignment_storage_blob_data_owner, + time_sleep.sleep_connectivity, + ] +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/storage/roleassignments.tf b/quickstart/301-ai-studio-secure-e2e/modules/storage/roleassignments.tf new file mode 100644 index 000000000..311dbb65a --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/storage/roleassignments.tf @@ -0,0 +1,5 @@ +resource "azurerm_role_assignment" "current_roleassignment_storage_blob_data_owner" { + scope = azurerm_storage_account.storage_account.id + role_definition_name = "Storage Blob Data Owner" + principal_id = data.azurerm_client_config.current.object_id +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/storage/terraform.tf b/quickstart/301-ai-studio-secure-e2e/modules/storage/terraform.tf new file mode 100644 index 000000000..ac15f3f59 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/storage/terraform.tf @@ -0,0 +1,14 @@ +terraform { + required_version = ">=0.12" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 3.50.0" + } + time = { + source = "hashicorp/time" + version = ">= 0.9.1" + } + } +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/storage/variables.tf b/quickstart/301-ai-studio-secure-e2e/modules/storage/variables.tf new file mode 100644 index 000000000..ab108463e --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/storage/variables.tf @@ -0,0 +1,378 @@ +# General variables +variable "location" { + description = "Specifies the location of all resources." + type = string + sensitive = false + nullable = false +} + +variable "resource_group_name" { + description = "Specifies the resource group name in which all resources will get deployed." + type = string + sensitive = false + nullable = false + validation { + condition = length(var.resource_group_name) >= 2 + error_message = "Please specify a valid resource group name." + } +} + +variable "tags" { + description = "Specifies a key value map of tags to set on every taggable resources." + type = map(string) + sensitive = false + default = {} + nullable = false +} + +# Storage variables +variable "storage_account_name" { + description = "Specifies the name of the storage account." + type = string + sensitive = false + nullable = false + validation { + condition = length(var.storage_account_name) >= 2 && length(regexall("[^[:alnum:]]", var.storage_account_name)) <= 0 + error_message = "Please specify a valid name." + } +} + +variable "storage_access_tier" { + description = "Specifies the access tier of the storage account. Valid options are 'Hot' and 'Cool' (Default: 'Hot')." + type = string + sensitive = false + nullable = false + default = "Hot" + validation { + condition = contains(["Hot", "Cool"], var.storage_access_tier) + error_message = "Please specify a valid storage account access tier." + } +} + +variable "storage_account_type" { + description = "Specifies the account tier of the storage account." + type = string + sensitive = false + nullable = false + default = "StorageV2" + validation { + condition = contains(["BlobStorage", "BlockBlobStorage", "FileStorage", "Storage", "StorageV2"], var.storage_account_type) + error_message = "Please specify a valid storage account type." + } +} + +variable "storage_account_tier" { + description = "Specifies the account tier of the storage account." + type = string + sensitive = false + nullable = false + default = "Standard" + validation { + condition = contains(["Standard", "Premium"], var.storage_account_tier) + error_message = "Please specify a valid storage account tier." + } +} + +variable "storage_account_replication_type" { + description = "Specifies the replication type of the storage account." + type = string + sensitive = false + nullable = false + default = "ZRS" + validation { + condition = contains(["LRS", "GRS", "RAGRS", "ZRS", "GZRS", "RAGZRS"], var.storage_account_replication_type) + error_message = "Please specify a valid storage account replication type." + } +} + +variable "storage_blob_change_feed_enabled" { + description = "Specifies whether the blob change feed should be enabled." + type = bool + sensitive = false + nullable = false + default = false +} + +variable "storage_blob_container_delete_retention_in_days" { + description = "Specifies the blob container delete retention policy in days (soft-delete)." + type = number + sensitive = false + nullable = false + default = 7 + validation { + condition = var.storage_blob_container_delete_retention_in_days >= 7 + error_message = "Please specify a valid storage account blob container delete retention policy." + } +} + +variable "storage_blob_delete_retention_in_days" { + description = "Specifies the blob delete retention policy in days (soft-delete)." + type = number + sensitive = false + nullable = false + default = 7 + validation { + condition = var.storage_blob_delete_retention_in_days >= 7 + error_message = "Please specify a valid storage account blob delete retention policy." + } +} + +variable "storage_blob_cors_rules" { + description = "Specifies storage account blob cors rules." + type = map(object({ + allowed_headers = list(string) + allowed_methods = list(string) + allowed_origins = list(string) + exposed_headers = list(string) + max_age_in_seconds = optional(number, 1800) + })) + sensitive = false + nullable = false + default = {} +} + +variable "storage_blob_last_access_time_enabled" { + description = "Specifies whether the blob last access time feature should be enabled." + type = bool + sensitive = false + nullable = false + default = false +} + +variable "storage_blob_versioning_enabled" { + description = "Specifies whether the blob versioning feature should be enabled." + type = bool + sensitive = false + nullable = false + default = false +} + +variable "storage_is_hns_enabled" { + description = "Specifies whetehr hierarchical namespace (Data Lake) should be enabled." + type = bool + sensitive = false + nullable = false + default = false +} + +variable "storage_network_bypass" { + description = "Specifies bypass options for the storage account network rules. List can include \"None\", \"AzureServices\", \"Metrics\" and \"Logs\"" + type = set(string) + sensitive = false + nullable = false + default = ["None"] + validation { + condition = alltrue([ + length([for value in toset(var.storage_network_bypass) : value if !contains(["None", "AzureServices", "Metrics", "Logs"], value)]) <= 0 + ]) + error_message = "Please provide a valid list. Valid values: \"None\", \"AzureServices\", \"Metrics\" and \"Logs\"." + } +} + +variable "storage_network_private_link_access" { + description = "Specifies resource instance rules of the storage account." + type = set(string) + sensitive = false + nullable = false + default = [] + validation { + condition = alltrue([ + length([for value in toset(var.storage_network_private_link_access) : value if length(split("/", value)) < 8]) <= 0 + ]) + error_message = "Please provide a valid resource id that has the following format: \"/subscriptions/.../resourceGroups/.../providers/.../.../...\"." + } +} + +variable "storage_public_network_access_enabled" { + description = "Specifies whether the storage account should have public network access." + type = bool + sensitive = false + nullable = false + default = true +} + +variable "storage_nfsv3_enabled" { + description = "Specifies whether NFSv3 should be enabled." + type = bool + sensitive = false + nullable = false + default = false +} + +variable "storage_sftp_enabled" { + description = "Specifies whether the storage account should enable sftp." + type = bool + sensitive = false + nullable = false + default = false +} + +variable "storage_shared_access_key_enabled" { + description = "Specifies whether the storage account should enable the shared key for access." + type = bool + sensitive = false + nullable = false + default = false +} + +variable "storage_container_names" { + description = "Specifies the storage container names to create within the storage account." + type = list(string) + sensitive = false + nullable = false + default = [] +} + +variable "storage_static_website" { + description = "Specifies the website configuration to enable static websites on the storage account." + type = list(object({ + index_document = string + error_404_document = string + })) + sensitive = false + nullable = false + default = [] +} + +# Diagnostics variables +variable "diagnostics_configurations" { + description = "Specifies the diagnostic configuration for the service." + type = list(object({ + log_analytics_workspace_id = optional(string, ""), + storage_account_id = optional(string, "") + })) + sensitive = false + default = [] + validation { + condition = alltrue([ + length([for diagnostics_configuration in toset(var.diagnostics_configurations) : diagnostics_configuration if diagnostics_configuration.log_analytics_workspace_id == "" && diagnostics_configuration.storage_account_id == ""]) <= 0 + ]) + error_message = "Please specify a valid resource ID." + } +} + +# Network variables +variable "subnet_id" { + description = "Specifies the resource id of a subnet in which the private endpoints get created." + type = string + sensitive = false + validation { + condition = length(split("/", var.subnet_id)) == 11 + error_message = "Please specify a valid subnet id." + } +} + +variable "connectivity_delay_in_seconds" { + description = "Specifies the delay in seconds after the private endpoint deployment (required for the DNS automation via Policies)." + type = number + sensitive = false + nullable = false + default = 120 + validation { + condition = var.connectivity_delay_in_seconds >= 0 + error_message = "Please specify a valid non-negative number." + } +} + +variable "private_endpoint_subresource_names" { + description = "Specifies a list of group ids for which private endpoints will be created (e.g. 'blob', 'file', etc.). If sub resource is defined a private endpoint will be created." + type = set(string) + sensitive = false + nullable = false + default = ["blob"] + validation { + condition = alltrue([ + length([for private_endpoint_subresource_name in var.private_endpoint_subresource_names : private_endpoint_subresource_name if !contains(["blob", "file", "queue", "table", "dfs", "web"], private_endpoint_subresource_name)]) <= 0 + ]) + error_message = "Please specify a valid group id." + } +} + +variable "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 Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_blob == "" || (length(split("/", var.private_dns_zone_id_blob)) == 9 && endswith(var.private_dns_zone_id_blob, "privatelink.blob.core.windows.net")) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} + +variable "private_dns_zone_id_file" { + description = "Specifies the resource ID of the private DNS zone for Azure Storage file endpoints. Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_file == "" || (length(split("/", var.private_dns_zone_id_file)) == 9 && endswith(var.private_dns_zone_id_file, "privatelink.file.core.windows.net")) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} + +variable "private_dns_zone_id_table" { + description = "Specifies the resource ID of the private DNS zone for Azure Storage table endpoints. Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_table == "" || (length(split("/", var.private_dns_zone_id_table)) == 9 && endswith(var.private_dns_zone_id_table, "privatelink.table.core.windows.net")) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} + +variable "private_dns_zone_id_queue" { + description = "Specifies the resource ID of the private DNS zone for Azure Storage queue endpoints. Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_queue == "" || (length(split("/", var.private_dns_zone_id_queue)) == 9 && endswith(var.private_dns_zone_id_queue, "privatelink.queue.core.windows.net")) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} + +variable "private_dns_zone_id_web" { + description = "Specifies the resource ID of the private DNS zone for Azure Storage web endpoints. Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_web == "" || (length(split("/", var.private_dns_zone_id_web)) == 9 && endswith(var.private_dns_zone_id_web, "privatelink.web.core.windows.net")) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} + +variable "private_dns_zone_id_dfs" { + description = "Specifies the resource ID of the private DNS zone for Azure Storage dfs endpoints. Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_dfs == "" || (length(split("/", var.private_dns_zone_id_dfs)) == 9 && endswith(var.private_dns_zone_id_dfs, "privatelink.dfs.core.windows.net")) + 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." + type = object({ + key_vault_id = string, + key_vault_key_versionless_id = string, + user_assigned_identity_id = string, + user_assigned_identity_client_id = string, + }) + sensitive = false + nullable = true + default = null + validation { + condition = alltrue([ + var.customer_managed_key == null || length(split("/", try(var.customer_managed_key.key_vault_id, ""))) == 9, + var.customer_managed_key == null || startswith(try(var.customer_managed_key.key_vault_key_versionless_id, ""), "https://"), + var.customer_managed_key == null || length(split("/", try(var.customer_managed_key.user_assigned_identity_id, ""))) == 9, + var.customer_managed_key == null || length(try(var.customer_managed_key.user_assigned_identity_client_id, "")) >= 2, + ]) + error_message = "Please specify a valid resource ID." + } +} diff --git a/quickstart/301-ai-studio-secure-e2e/providers.tf b/quickstart/301-ai-studio-secure-e2e/providers.tf new file mode 100644 index 000000000..d5aa19678 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/providers.tf @@ -0,0 +1,34 @@ +provider "azurerm" { + environment = "public" + skip_provider_registration = false + storage_use_azuread = true + + features { + cognitive_account { + purge_soft_delete_on_destroy = true + } + key_vault { + purge_soft_delete_on_destroy = false + purge_soft_deleted_certificates_on_destroy = false + purge_soft_deleted_keys_on_destroy = false + purge_soft_deleted_secrets_on_destroy = false + recover_soft_deleted_key_vaults = true + recover_soft_deleted_certificates = true + recover_soft_deleted_keys = true + recover_soft_deleted_secrets = true + } + log_analytics_workspace { + permanently_delete_on_destroy = false + } + resource_group { + prevent_deletion_if_contains_resources = false + } + } +} + +provider "azapi" { + default_location = var.location + default_tags = var.tags + environment = "public" + skip_provider_registration = false +} diff --git a/quickstart/301-ai-studio-secure-e2e/resourcegroup.tf b/quickstart/301-ai-studio-secure-e2e/resourcegroup.tf new file mode 100644 index 000000000..0c99d838d --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/resourcegroup.tf @@ -0,0 +1,5 @@ +resource "azurerm_resource_group" "resource_group" { + name = "rg-${local.prefix}" + location = var.location + tags = var.tags +} diff --git a/quickstart/301-ai-studio-secure-e2e/terraform.tf b/quickstart/301-ai-studio-secure-e2e/terraform.tf new file mode 100644 index 000000000..5080dd9f8 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/terraform.tf @@ -0,0 +1,18 @@ +terraform { + required_version = ">=0.12" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = "3.111.0" + } + azapi = { + source = "Azure/azapi" + version = "1.14.0" + } + time = { + source = "hashicorp/time" + version = "0.9.1" + } + } +} diff --git a/quickstart/301-ai-studio-secure-e2e/variables.tf b/quickstart/301-ai-studio-secure-e2e/variables.tf new file mode 100644 index 000000000..173b6e04e --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/variables.tf @@ -0,0 +1,159 @@ +# General variables +variable "location" { + description = "Specifies the location of all resources." + type = string + sensitive = false + nullable = false +} + +variable "tags" { + description = "Specifies a key value map of tags to set on every taggable resources." + type = map(string) + sensitive = false + nullable = false + default = {} +} + +variable "environment" { + description = "Specifies the environment of the deployment." + type = string + sensitive = false + default = "dev" + validation { + condition = contains(["dev", "tst", "qa", "prd"], var.environment) + error_message = "Please use an allowed value: \"dev\", \"tst\", \"qa\" or \"prd\"." + } +} + +variable "prefix" { + description = "Specifies the prefix for all resources created in this deployment." + type = string + sensitive = false + validation { + condition = length(var.prefix) >= 2 && length(var.prefix) <= 10 + error_message = "Please specify a prefix with more than two and less than 10 characters." + } +} + +# Network variables +variable "subnet_id" { + description = "Specifies the resource id of a subnet in which the private endpoints get created." + type = string + sensitive = false + validation { + condition = length(split("/", var.subnet_id)) == 11 + error_message = "Please specify a valid subnet id." + } +} + +variable "private_dns_zone_id_machine_learning_api" { + description = "Specifies the resource ID of the private DNS zone for machine learning. Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_machine_learning_api == "" || (length(split("/", var.private_dns_zone_id_machine_learning_api)) == 9 && endswith(var.private_dns_zone_id_machine_learning_api, "privatelink.api.azureml.ms")) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} + +variable "private_dns_zone_id_machine_learning_notebooks" { + description = "Specifies the resource ID of the private DNS zone for machine learning notebooks. Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_machine_learning_notebooks == "" || (length(split("/", var.private_dns_zone_id_machine_learning_notebooks)) == 9 && endswith(var.private_dns_zone_id_machine_learning_notebooks, "privatelink.notebooks.azure.net")) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} + +variable "private_dns_zone_id_container_registry" { + description = "Specifies the resource ID of the private DNS zone for the container registry. Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_container_registry == "" || (length(split("/", var.private_dns_zone_id_container_registry)) == 9 && endswith(var.private_dns_zone_id_container_registry, "privatelink.azurecr.io")) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} + +variable "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 Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_blob == "" || (length(split("/", var.private_dns_zone_id_blob)) == 9 && endswith(var.private_dns_zone_id_blob, "privatelink.blob.core.windows.net")) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} + +variable "private_dns_zone_id_file" { + description = "Specifies the resource ID of the private DNS zone for Azure Storage file endpoints. Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_file == "" || (length(split("/", var.private_dns_zone_id_file)) == 9 && endswith(var.private_dns_zone_id_file, "privatelink.file.core.windows.net")) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} + +variable "private_dns_zone_id_table" { + description = "Specifies the resource ID of the private DNS zone for Azure Storage table endpoints. Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_table == "" || (length(split("/", var.private_dns_zone_id_table)) == 9 && endswith(var.private_dns_zone_id_table, "privatelink.table.core.windows.net")) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} + +variable "private_dns_zone_id_queue" { + description = "Specifies the resource ID of the private DNS zone for Azure Storage queue endpoints. Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + default = "" + validation { + condition = var.private_dns_zone_id_queue == "" || (length(split("/", var.private_dns_zone_id_queue)) == 9 && endswith(var.private_dns_zone_id_queue, "privatelink.queue.core.windows.net")) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} + +variable "private_dns_zone_id_vault" { + description = "Specifies the resource ID of the private DNS zone for Azure Key Vault. Not required if DNS A-records get created via Azure Policy." + type = string + sensitive = false + nullable = false + default = "" + validation { + condition = var.private_dns_zone_id_vault == "" || (length(split("/", var.private_dns_zone_id_vault)) == 9 && endswith(var.private_dns_zone_id_vault, "privatelink.vaultcore.azure.net")) + 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." + type = object({ + key_vault_id = string, + key_vault_key_versionless_id = string, + user_assigned_identity_id = string, + user_assigned_identity_client_id = string, + }) + sensitive = false + nullable = true + default = null + validation { + condition = alltrue([ + var.customer_managed_key == null || length(split("/", try(var.customer_managed_key.key_vault_id, ""))) == 9, + var.customer_managed_key == null || startswith(try(var.customer_managed_key.key_vault_key_versionless_id, ""), "https://"), + var.customer_managed_key == null || length(split("/", try(var.customer_managed_key.user_assigned_identity_id, ""))) == 9, + var.customer_managed_key == null || length(try(var.customer_managed_key.user_assigned_identity_client_id, "")) >= 2, + ]) + error_message = "Please specify a valid resource ID." + } +} diff --git a/quickstart/301-ai-studio-secure-e2e/vars.tfvars b/quickstart/301-ai-studio-secure-e2e/vars.tfvars new file mode 100644 index 000000000..e69de29bb From 350010a4a9b7796d8e0085a71e6314402e4f08e5 Mon Sep 17 00:00:00 2001 From: Marvin Buss Date: Fri, 12 Jul 2024 13:21:53 +0200 Subject: [PATCH 2/3] Add sample vars file --- .../vars.sample.tfvars | 19 +++++++++++++++++++ .../301-ai-studio-secure-e2e/vars.tfvars | 0 2 files changed, 19 insertions(+) create mode 100644 quickstart/301-ai-studio-secure-e2e/vars.sample.tfvars delete mode 100644 quickstart/301-ai-studio-secure-e2e/vars.tfvars diff --git a/quickstart/301-ai-studio-secure-e2e/vars.sample.tfvars b/quickstart/301-ai-studio-secure-e2e/vars.sample.tfvars new file mode 100644 index 000000000..83e7ee066 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/vars.sample.tfvars @@ -0,0 +1,19 @@ +# General variables +location = "eastus" +tags = {} +environment = "dev" +prefix = "myprfx" + +# Network variables +subnet_id = "/subscriptions//resourceGroups//providers/Microsoft.Network/virtualNetworks//subnets/" +private_dns_zone_id_machine_learning_api = "" +private_dns_zone_id_machine_learning_notebooks = "" +private_dns_zone_id_container_registry = "" +private_dns_zone_id_blob = "" +private_dns_zone_id_file = "" +private_dns_zone_id_table = "" +private_dns_zone_id_queue = "" +private_dns_zone_id_vault = "" + +# Customer-managed key variables +customer_managed_key = null diff --git a/quickstart/301-ai-studio-secure-e2e/vars.tfvars b/quickstart/301-ai-studio-secure-e2e/vars.tfvars deleted file mode 100644 index e69de29bb..000000000 From 6f01c8ac70b0b861279cb14d4f06a09212c33c3f Mon Sep 17 00:00:00 2001 From: Marvin Buss Date: Fri, 12 Jul 2024 14:14:28 +0200 Subject: [PATCH 3/3] Remove jsonencode for azapi resources --- .../modules/aistudiohub/connections.tf | 5 ++--- .../modules/aistudiohub/main.tf | 8 +++++--- .../modules/aistudiohub/outboundrules.tf | 14 ++++++++++++++ .../modules/aistudiooutboundrules/main.tf | 8 ++++---- .../modules/aistudioproject/connections.tf | 5 ++--- .../modules/aistudioproject/main.tf | 4 ++-- 6 files changed, 29 insertions(+), 15 deletions(-) create mode 100644 quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/outboundrules.tf diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/connections.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/connections.tf index 7b77ae22f..697d9e911 100644 --- a/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/connections.tf +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/connections.tf @@ -5,7 +5,7 @@ resource "azapi_resource" "ai_studio_hub_connections" { name = each.key parent_id = azapi_resource.ai_studio_hub.id - body = jsonencode({ + body = { properties = { authType = each.value.auth_type category = each.value.category @@ -15,8 +15,7 @@ resource "azapi_resource" "ai_studio_hub_connections" { target = each.value.target metadata = each.value.metadata } - - }) + } response_export_values = [] schema_validation_enabled = false # Can be reverted once this is closed: https://github.com/Azure/terraform-provider-azapi/issues/524 diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/main.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/main.tf index 4103f86e0..6790739c0 100644 --- a/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/main.tf +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/main.tf @@ -20,7 +20,7 @@ resource "azapi_resource" "ai_studio_hub" { } } - body = jsonencode({ + body = { kind = "Hub" properties = { applicationInsights = var.application_insights_id @@ -53,12 +53,14 @@ resource "azapi_resource" "ai_studio_hub" { name = "Basic" tier = "Basic" } - }) + } response_export_values = [] schema_validation_enabled = false # Can be reverted once this is closed: https://github.com/Azure/terraform-provider-azapi/issues/524 locks = [] - ignore_body_changes = ["properties.managedNetwork"] ignore_casing = false ignore_missing_property = true + lifecycle { + ignore_changes = ["body.properties.managedNetwork"] + } } diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/outboundrules.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/outboundrules.tf new file mode 100644 index 000000000..9b4e5f246 --- /dev/null +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudiohub/outboundrules.tf @@ -0,0 +1,14 @@ +resource "azapi_resource_action" "ai_studio_hub_provision_managed_network" { + count = var.ai_studio_hub_provision_managed_network ? 1 : 0 + + type = "Microsoft.MachineLearningServices/workspaces@2024-04-01" + resource_id = azapi_resource.ai_studio_hub.id + + action = "provisionManagedNetwork" + method = "POST" + body = { + includeSpark = true + } + + response_export_values = [] +} diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/main.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/main.tf index 830ca7ed5..c9c2efe0a 100644 --- a/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/main.tf +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudiooutboundrules/main.tf @@ -2,7 +2,7 @@ resource "azapi_update_resource" "ai_studio_hub_outbound_rules" { type = "Microsoft.MachineLearningServices/workspaces@2024-04-01" resource_id = var.ai_studio_hub_id - body = jsonencode({ + body = { properties = { managedNetwork = { isolationMode = "AllowOnlyApprovedOutbound" @@ -13,7 +13,7 @@ resource "azapi_update_resource" "ai_studio_hub_outbound_rules" { } } } - }) + } response_export_values = [] locks = [] @@ -29,9 +29,9 @@ resource "azapi_resource_action" "ai_studio_hub_provision_managed_network" { action = "provisionManagedNetwork" method = "POST" - body = jsonencode({ + body = { includeSpark = true - }) + } response_export_values = [] depends_on = [] diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/connections.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/connections.tf index 565b54145..1c80f25df 100644 --- a/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/connections.tf +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/connections.tf @@ -5,7 +5,7 @@ resource "azapi_resource" "ai_studio_project_connection" { name = each.key parent_id = azapi_resource.ai_studio_project.id - body = jsonencode({ + body = { properties = { authType = each.value.auth_type category = each.value.category @@ -15,8 +15,7 @@ resource "azapi_resource" "ai_studio_project_connection" { target = each.value.target metadata = each.value.metadata } - - }) + } response_export_values = [] schema_validation_enabled = false # Can be reverted once this is closed: https://github.com/Azure/terraform-provider-azapi/issues/524 diff --git a/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/main.tf b/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/main.tf index b34250d16..a978f706f 100644 --- a/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/main.tf +++ b/quickstart/301-ai-studio-secure-e2e/modules/aistudioproject/main.tf @@ -9,14 +9,14 @@ resource "azapi_resource" "ai_studio_project" { identity_ids = [] } - body = jsonencode({ + body = { kind = "Project" properties = { description = "AI Studio Project - ${var.ai_studio_project_name}" friendlyName = title(replace(var.ai_studio_project_name, "-", " ")) hubResourceId = var.ai_studio_hub_id } - }) + } response_export_values = [] schema_validation_enabled = false # Can be reverted once this is closed: https://github.com/Azure/terraform-provider-azapi/issues/524