diff --git a/.github/workflows/_terraformEnvironmentTemplate.yml b/.github/workflows/_terraformEnvironmentTemplate.yml index 3e8540c..4af6a28 100644 --- a/.github/workflows/_terraformEnvironmentTemplate.yml +++ b/.github/workflows/_terraformEnvironmentTemplate.yml @@ -12,15 +12,19 @@ on: required: true type: string description: "Specifies the working directory." + subscription_id: + required: true + type: string + description: "Specifies the Azure subscription id." + terraform_version: + required: true + type: string + description: "Specifies the terraform version." export_terraform_outputs: required: false type: boolean default: false description: "Specifies whether terraform outputs should be exported." - subscription_id: - required: true - type: string - description: "Specifies the Azure subscription id." secrets: TENANT_ID: required: true @@ -39,11 +43,19 @@ on: jobs: lint: name: Terraform Lint - runs-on: [self-hosted] + runs-on: [ubuntu-latest] continue-on-error: false needs: [] steps: + # Setup Terraform + - name: Setup Terraform + id: terraform_setup + uses: hashicorp/setup-terraform@v2 + with: + terraform_version: ${{ inputs.terraform_version }} + terraform_wrapper: true + # Check Out Repository - name: Check Out Repository id: checkout_repository @@ -71,6 +83,21 @@ jobs: ARM_USE_OIDC: false steps: + # Setup Node + - name: Setup Node + id: node_setup + uses: actions/setup-node@v3 + with: + node-version: 16 + + # Setup Terraform + - name: Setup Terraform + id: terraform_setup + uses: hashicorp/setup-terraform@v2 + with: + terraform_version: ${{ inputs.terraform_version }} + terraform_wrapper: true + # Check Out Repository - name: Check Out Repository id: checkout_repository @@ -115,6 +142,21 @@ jobs: ARM_USE_OIDC: false steps: + # Setup Node + - name: Setup Node + id: node_setup + uses: actions/setup-node@v3 + with: + node-version: 16 + + # Setup Terraform + - name: Setup Terraform + id: terraform_setup + uses: hashicorp/setup-terraform@v2 + with: + terraform_version: ${{ inputs.terraform_version }} + terraform_wrapper: true + # Check Out Repository - name: Check Out Repository id: checkout_repository diff --git a/.github/workflows/deployment.yml b/.github/workflows/deployment.yml index 62e1649..f77f7bf 100644 --- a/.github/workflows/deployment.yml +++ b/.github/workflows/deployment.yml @@ -4,23 +4,15 @@ on: branches: - main paths: - - "**.tf" - - "**.yml" - - "**.yaml" - - "!.github/workflows/**" - - "!.pre-commit-config.yaml" - - "!.terraform-docs.yml" + - "code/terraform/**" + - ".github/workflows/deployment" pull_request: branches: - main paths: - - "**.tf" - - "**.yml" - - "**.yaml" - - "!.github/workflows/**" - - "!.pre-commit-config.yaml" - - "!.terraform-docs.yml" + - "code/terraform/**" + - ".github/workflows/deployment" jobs: terraform: @@ -28,8 +20,10 @@ jobs: name: "Terraform Deployment" with: environment: "dev" - working_directory: "./tests/e2e" + working_directory: "./code/terraform" subscription_id: "8f171ff9-2b5b-4f0f-aed5-7fa360a1d094" + terraform_version: "1.5.6" + export_terraform_outputs: false secrets: TENANT_ID: ${{ secrets.TENANT_ID }} CLIENT_ID: ${{ secrets.CLIENT_ID }} diff --git a/code/terraform/applicationinsights.tf b/code/terraform/applicationinsights.tf new file mode 100644 index 0000000..797fbc5 --- /dev/null +++ b/code/terraform/applicationinsights.tf @@ -0,0 +1,44 @@ +resource "azurerm_application_insights" "application_insights" { + name = "${local.prefix}-ai001" + location = var.location + resource_group_name = data.azurerm_resource_group.resource_group.name + tags = var.tags + + application_type = "web" + 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 = azurerm_log_analytics_workspace.log_analytics_workspace.id +} + +data "azurerm_monitor_diagnostic_categories" "diagnostic_categories_application_insights" { + resource_id = azurerm_application_insights.application_insights.id +} + +resource "azurerm_monitor_diagnostic_setting" "diagnostic_setting_application_insights" { + name = "logAnalytics" + target_resource_id = azurerm_application_insights.application_insights.id + log_analytics_workspace_id = azurerm_log_analytics_workspace.log_analytics_workspace.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/code/terraform/containerregistry.tf b/code/terraform/containerregistry.tf new file mode 100644 index 0000000..b5218f3 --- /dev/null +++ b/code/terraform/containerregistry.tf @@ -0,0 +1,89 @@ +resource "azurerm_container_registry" "container_registry" { + name = replace("${local.prefix}-acr001", "-", "") + location = var.location + resource_group_name = data.azurerm_resource_group.resource_group.name + tags = var.tags + identity { + type = "SystemAssigned" + } + + admin_enabled = true + anonymous_pull_enabled = false + data_endpoint_enabled = false + export_policy_enabled = true + network_rule_bypass_option = "AzureServices" + network_rule_set = [ + { + default_action = "Deny" + ip_rule = [] + virtual_network = [] + } + ] + public_network_access_enabled = false + quarantine_policy_enabled = true + retention_policy = [ + { + days = 7 + enabled = true + } + ] + sku = "Premium" + trust_policy = [ + { + enabled = false + } + ] + zone_redundancy_enabled = true +} + +data "azurerm_monitor_diagnostic_categories" "diagnostic_categories_container_registry" { + resource_id = azurerm_container_registry.container_registry.id +} + +resource "azurerm_monitor_diagnostic_setting" "diagnostic_setting_container_registry" { + name = "logAnalytics" + target_resource_id = azurerm_container_registry.container_registry.id + log_analytics_workspace_id = azurerm_log_analytics_workspace.log_analytics_workspace.id + + dynamic "enabled_log" { + iterator = entry + for_each = data.azurerm_monitor_diagnostic_categories.diagnostic_categories_container_registry.log_category_groups + content { + category_group = entry.value + } + } + + dynamic "metric" { + iterator = entry + for_each = data.azurerm_monitor_diagnostic_categories.diagnostic_categories_container_registry.metrics + content { + category = entry.value + enabled = true + } + } +} + +resource "azurerm_private_endpoint" "container_registry_private_endpoint" { + name = "${azurerm_container_registry.container_registry.name}-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}-pe" + is_manual_connection = false + private_connection_resource_id = azurerm_container_registry.container_registry.id + subresource_names = ["registry"] + } + subnet_id = data.azurerm_subnet.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 + ] + } + } +} diff --git a/code/terraform/data.tf b/code/terraform/data.tf new file mode 100644 index 0000000..ca7d151 --- /dev/null +++ b/code/terraform/data.tf @@ -0,0 +1,11 @@ +data "azurerm_client_config" "current" {} + +data "azurerm_resource_group" "resource_group" { + name = var.resource_group_name +} + +data "azurerm_subnet" "subnet" { + name = local.subnet.name + virtual_network_name = local.subnet.virtual_network_name + resource_group_name = local.subnet.resource_group_name +} diff --git a/code/terraform/keyvault.tf b/code/terraform/keyvault.tf new file mode 100644 index 0000000..1b78537 --- /dev/null +++ b/code/terraform/keyvault.tf @@ -0,0 +1,75 @@ +resource "azurerm_key_vault" "key_vault" { + name = "${local.prefix}-kv001" + location = var.location + resource_group_name = data.azurerm_resource_group.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 = "premium" + soft_delete_retention_days = 7 + tenant_id = data.azurerm_client_config.current.tenant_id +} + +data "azurerm_monitor_diagnostic_categories" "diagnostic_categories_key_vault" { + resource_id = azurerm_key_vault.key_vault.id +} + +resource "azurerm_monitor_diagnostic_setting" "diagnostic_setting_key_vault" { + name = "logAnalytics" + target_resource_id = azurerm_key_vault.key_vault.id + log_analytics_workspace_id = azurerm_log_analytics_workspace.log_analytics_workspace.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 + } + } +} + +resource "azurerm_private_endpoint" "key_vault_private_endpoint" { + name = "${azurerm_key_vault.key_vault.name}-pe" + location = var.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}-nic" + private_service_connection { + name = "${azurerm_key_vault.key_vault.name}-pe" + is_manual_connection = false + private_connection_resource_id = azurerm_key_vault.key_vault.id + subresource_names = ["vault"] + } + subnet_id = data.azurerm_subnet.subnet.id + dynamic "private_dns_zone_group" { + for_each = var.private_dns_zone_id_key_vault == "" ? [] : [1] + content { + name = "${azurerm_key_vault.key_vault.name}-arecord" + private_dns_zone_ids = [ + var.private_dns_zone_id_key_vault + ] + } + } +} diff --git a/code/terraform/locals.tf b/code/terraform/locals.tf new file mode 100644 index 0000000..60bc343 --- /dev/null +++ b/code/terraform/locals.tf @@ -0,0 +1,9 @@ +locals { + prefix = "${lower(var.prefix)}-${var.environment}" + + subnet = { + resource_group_name = split("/", var.subnet_id)[4] + virtual_network_name = split("/", var.subnet_id)[8] + name = split("/", var.subnet_id)[10] + } +} diff --git a/code/terraform/loganalyticsworkspace.tf b/code/terraform/loganalyticsworkspace.tf new file mode 100644 index 0000000..b319769 --- /dev/null +++ b/code/terraform/loganalyticsworkspace.tf @@ -0,0 +1,42 @@ +resource "azurerm_log_analytics_workspace" "log_analytics_workspace" { + name = "${local.prefix}-log001" + location = var.location + resource_group_name = data.azurerm_resource_group.resource_group.name + tags = var.tags + + allow_resource_only_permissions = true + cmk_for_query_forced = false + daily_quota_gb = -1 + internet_ingestion_enabled = true + internet_query_enabled = true + local_authentication_disabled = true + retention_in_days = 30 + sku = "PerGB2018" +} + +data "azurerm_monitor_diagnostic_categories" "diagnostic_categories_log_analytics_workspace" { + resource_id = azurerm_log_analytics_workspace.log_analytics_workspace.id +} + +resource "azurerm_monitor_diagnostic_setting" "diagnostic_setting_log_analytics_workspace" { + name = "logAnalytics" + target_resource_id = azurerm_log_analytics_workspace.log_analytics_workspace.id + log_analytics_workspace_id = azurerm_log_analytics_workspace.log_analytics_workspace.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/code/terraform/machinelearning.tf b/code/terraform/machinelearning.tf new file mode 100644 index 0000000..743b90a --- /dev/null +++ b/code/terraform/machinelearning.tf @@ -0,0 +1,130 @@ +resource "azurerm_machine_learning_workspace" "machine_learning_workspace" { + name = "${local.prefix}-mlw001" + location = var.location + resource_group_name = data.azurerm_resource_group.resource_group.name + tags = var.tags + identity { + type = "UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.user_assigned_identity.id + ] + } + + application_insights_id = azurerm_application_insights.application_insights.id + container_registry_id = azurerm_container_registry.container_registry.id + key_vault_id = azurerm_key_vault.key_vault.id + storage_account_id = azurerm_storage_account.storage.id + description = "Azure Machine Learning Workspace for environment ${var.environment} with prefix ${var.prefix}." + friendly_name = title(replace("${local.prefix}-mlw001", "-", " ")) + high_business_impact = true + image_build_compute_name = "imagebuilder001" + primary_user_assigned_identity = azurerm_user_assigned_identity.user_assigned_identity.id + public_network_access_enabled = false + sku_name = "Basic" + v1_legacy_mode_enabled = false + + depends_on = [ + azurerm_role_assignment.uai_role_assignment_resource_group_reader, + azurerm_role_assignment.uai_role_assignment_container_registry_contributor, + azurerm_role_assignment.uai_role_assignment_storage_contributor, + azurerm_role_assignment.uai_role_assignment_storage_blob_contributor, + azurerm_role_assignment.uai_role_assignment_key_vault_contributor, + azurerm_role_assignment.uai_role_assignment_key_vault_administrator, + azurerm_role_assignment.uai_role_assignment_application_insights_contributor + ] +} + +resource "azapi_update_resource" "machine_learning_managed_network" { + type = "Microsoft.MachineLearningServices/workspaces@2023-06-01-preview" + resource_id = azurerm_machine_learning_workspace.machine_learning_workspace.id + + body = jsonencode({ + properties = { + managedNetwork = { + isolationMode = "AllowOnlyApprovedOutbound" + status = { + status = "Active" + sparkReady = true + } + outboundRules = { + "${azurerm_storage_account.storage.name}-table" = { + type = "PrivateEndpoint" + category = "UserDefined" + status = "Active" + destination = { + serviceResourceId = azurerm_storage_account.storage.id + subresourceTarget = "table" + sparkEnabled = true + sparkStatus = "Active" + } + }, + "${azurerm_storage_account.storage.name}-queue" = { + type = "PrivateEndpoint" + category = "UserDefined" + status = "Active" + destination = { + serviceResourceId = azurerm_storage_account.storage.id + subresourceTarget = "queue" + sparkEnabled = true + sparkStatus = "Active" + } + } + } + } + systemDatastoresAuthMode = "identity" + } + }) +} + +data "azurerm_monitor_diagnostic_categories" "diagnostic_categories_machine_learning_workspace" { + resource_id = azurerm_machine_learning_workspace.machine_learning_workspace.id +} + +resource "azurerm_monitor_diagnostic_setting" "diagnostic_setting_machine_learning_workspace" { + name = "logAnalytics" + target_resource_id = azurerm_machine_learning_workspace.machine_learning_workspace.id + log_analytics_workspace_id = azurerm_log_analytics_workspace.log_analytics_workspace.id + + dynamic "enabled_log" { + iterator = entry + for_each = data.azurerm_monitor_diagnostic_categories.diagnostic_categories_machine_learning_workspace.log_category_groups + content { + category_group = entry.value + } + } + + dynamic "metric" { + iterator = entry + for_each = data.azurerm_monitor_diagnostic_categories.diagnostic_categories_machine_learning_workspace.metrics + content { + category = entry.value + enabled = true + } + } +} + +resource "azurerm_private_endpoint" "machine_learning_workspace_private_endpoint" { + name = "${azurerm_machine_learning_workspace.machine_learning_workspace.name}-pe" + location = var.location + resource_group_name = azurerm_machine_learning_workspace.machine_learning_workspace.resource_group_name + tags = var.tags + + custom_network_interface_name = "${azurerm_machine_learning_workspace.machine_learning_workspace.name}-nic" + private_service_connection { + name = "${azurerm_machine_learning_workspace.machine_learning_workspace.name}-pe" + is_manual_connection = false + private_connection_resource_id = azurerm_machine_learning_workspace.machine_learning_workspace.id + subresource_names = ["amlworkspace"] + } + subnet_id = data.azurerm_subnet.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 = "${azurerm_machine_learning_workspace.machine_learning_workspace.name}-arecord" + private_dns_zone_ids = [ + var.private_dns_zone_id_machine_learning_api, + var.private_dns_zone_id_machine_learning_notebooks + ] + } + } +} diff --git a/code/terraform/machinelearningcompute.tf b/code/terraform/machinelearningcompute.tf new file mode 100644 index 0000000..1246f5e --- /dev/null +++ b/code/terraform/machinelearningcompute.tf @@ -0,0 +1,100 @@ +resource "azurerm_machine_learning_compute_cluster" "machine_learning_compute_cluster_image_build" { + machine_learning_workspace_id = azurerm_machine_learning_workspace.machine_learning_workspace.id + name = "builder001" + location = var.location + tags = var.tags + identity { + type = "UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.user_assigned_identity.id + ] + } + + description = "Compute Cluster to build container images" + local_auth_enabled = false + scale_settings { + min_node_count = 0 + max_node_count = 1 + scale_down_nodes_after_idle_duration = "PT30S" + } + ssh_public_access_enabled = false + vm_priority = "Dedicated" + vm_size = upper("Standard_DS2_v2") + + depends_on = [ + azapi_update_resource.machine_learning_managed_network + ] + lifecycle { + ignore_changes = [ + subnet_resource_id + ] + } +} + +resource "azurerm_machine_learning_compute_cluster" "machine_learning_compute_cluster" { + for_each = var.machine_learning_compute_clusters + + machine_learning_workspace_id = azurerm_machine_learning_workspace.machine_learning_workspace.id + name = each.key + location = var.location + tags = var.tags + identity { + type = "UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.user_assigned_identity.id + ] + } + + description = "" + local_auth_enabled = false + scale_settings { + min_node_count = each.value.scale.min_node_count + max_node_count = each.value.scale.max_node_count + scale_down_nodes_after_idle_duration = each.value.scale.scale_down_nodes_after_idle_duration + } + ssh_public_access_enabled = false + vm_priority = each.value.vm_priority + vm_size = upper(each.value.vm_size) + + depends_on = [ + azapi_update_resource.machine_learning_managed_network + ] + lifecycle { + ignore_changes = [ + subnet_resource_id + ] + } +} + +resource "azurerm_machine_learning_compute_instance" "machine_learning_compute_instance" { + for_each = var.machine_learning_compute_instances + + machine_learning_workspace_id = azurerm_machine_learning_workspace.machine_learning_workspace.id + name = each.key + location = var.location + tags = var.tags + identity { + type = "UserAssigned" + identity_ids = [ + azurerm_user_assigned_identity.user_assigned_identity.id + ] + } + + assign_to_user { + tenant_id = data.azurerm_client_config.current.tenant_id + object_id = each.value.user_object_id + } + authorization_type = "personal" + description = "" + local_auth_enabled = false + virtual_machine_size = upper(each.value.vm_size) + + depends_on = [ + azapi_update_resource.machine_learning_managed_network + ] + lifecycle { + ignore_changes = [ + subnet_resource_id + ] + } +} diff --git a/code/terraform/machinelearningdatastores.tf b/code/terraform/machinelearningdatastores.tf new file mode 100644 index 0000000..e69de29 diff --git a/code/terraform/outputs.tf b/code/terraform/outputs.tf new file mode 100644 index 0000000..195be3b --- /dev/null +++ b/code/terraform/outputs.tf @@ -0,0 +1,5 @@ +output "test" { + description = "Sample output." + sensitive = false + value = "test" +} diff --git a/code/terraform/roleassignments.tf b/code/terraform/roleassignments.tf new file mode 100644 index 0000000..f0d5f8f --- /dev/null +++ b/code/terraform/roleassignments.tf @@ -0,0 +1,53 @@ +resource "azurerm_role_assignment" "current_roleassignment_storage" { + scope = azurerm_storage_account.storage.id + role_definition_name = "Storage Blob Data Contributor" + principal_id = data.azurerm_client_config.current.object_id +} + +resource "azurerm_role_assignment" "uai_role_assignment_resource_group_reader" { + scope = data.azurerm_resource_group.resource_group.id + role_definition_name = "Reader" + principal_id = azurerm_user_assigned_identity.user_assigned_identity.principal_id +} + +resource "azurerm_role_assignment" "uai_role_assignment_container_registry_contributor" { + scope = azurerm_container_registry.container_registry.id + role_definition_name = "Contributor" + principal_id = azurerm_user_assigned_identity.user_assigned_identity.principal_id +} + +resource "azurerm_role_assignment" "uai_role_assignment_storage_contributor" { + scope = azurerm_storage_account.storage.id + role_definition_name = "Contributor" + principal_id = azurerm_user_assigned_identity.user_assigned_identity.principal_id +} + +resource "azurerm_role_assignment" "uai_role_assignment_storage_blob_contributor" { + scope = azurerm_storage_account.storage.id + role_definition_name = "Storage Blob Data Contributor" + principal_id = azurerm_user_assigned_identity.user_assigned_identity.principal_id +} + +resource "azurerm_role_assignment" "uai_role_assignment_key_vault_contributor" { + scope = azurerm_key_vault.key_vault.id + role_definition_name = "Contributor" + principal_id = azurerm_user_assigned_identity.user_assigned_identity.principal_id +} + +resource "azurerm_role_assignment" "uai_role_assignment_key_vault_administrator" { + scope = azurerm_key_vault.key_vault.id + role_definition_name = "Key Vault Administrator" + principal_id = azurerm_user_assigned_identity.user_assigned_identity.principal_id +} + +resource "azurerm_role_assignment" "uai_role_assignment_application_insights_contributor" { + scope = azurerm_application_insights.application_insights.id + role_definition_name = "Contributor" + principal_id = azurerm_user_assigned_identity.user_assigned_identity.principal_id +} + +resource "azurerm_role_assignment" "uai_role_assignment_machine_learning_workspace_contributor" { + scope = azurerm_machine_learning_workspace.machine_learning_workspace.id + role_definition_name = "Contributor" + principal_id = azurerm_user_assigned_identity.user_assigned_identity.principal_id +} diff --git a/code/terraform/storage.tf b/code/terraform/storage.tf new file mode 100644 index 0000000..69d7225 --- /dev/null +++ b/code/terraform/storage.tf @@ -0,0 +1,243 @@ +resource "azurerm_storage_account" "storage" { + name = replace("${local.prefix}-st001", "-", "") + location = var.location + resource_group_name = data.azurerm_resource_group.resource_group.name + tags = var.tags + + access_tier = "Hot" + account_kind = "StorageV2" + account_replication_type = "ZRS" + account_tier = "Standard" + allow_nested_items_to_be_public = false + allowed_copy_scope = "AAD" + blob_properties { + change_feed_enabled = false + # change_feed_retention_in_days = 7 + container_delete_retention_policy { + days = 7 + } + cors_rule { + allowed_headers = ["x-ms-blob-type"] + allowed_methods = ["PUT"] + allowed_origins = ["https://*.azuredatabricks.net"] + exposed_headers = [""] + max_age_in_seconds = 1800 + } + cors_rule { + allowed_headers = ["*"] + allowed_methods = ["GET", "HEAD"] + allowed_origins = ["https://mlworkspace.azure.ai", "https://ml.azure.com", "https://*.ml.azure.com"] + exposed_headers = ["*"] + max_age_in_seconds = 1800 + } + delete_retention_policy { + days = 7 + } + # default_service_version = "2020-06-12" + last_access_time_enabled = false + versioning_enabled = false + } + cross_tenant_replication_enabled = false + default_to_oauth_authentication = true + enable_https_traffic_only = true + # immutability_policy { # Not supported for ADLS Gen2 + # state = "Disabled" + # allow_protected_append_writes = true + # period_since_creation_in_days = 7 + # } + infrastructure_encryption_enabled = true + is_hns_enabled = false + large_file_share_enabled = false + min_tls_version = "TLS1_2" + network_rules { + bypass = ["None"] + default_action = "Deny" + ip_rules = [] + virtual_network_subnet_ids = [] + dynamic "private_link_access" { + for_each = tolist(setunion(var.data_platform_subscription_ids, [data.azurerm_client_config.current.subscription_id])) + content { + endpoint_resource_id = "/subscriptions/${private_link_access.value}/resourcegroups/*/providers/Microsoft.Databricks/accessConnectors/*" + endpoint_tenant_id = data.azurerm_client_config.current.tenant_id + } + } + dynamic "private_link_access" { + for_each = tolist(setunion(var.data_platform_subscription_ids, [data.azurerm_client_config.current.subscription_id])) + content { + endpoint_resource_id = "/subscriptions/${private_link_access.value}/resourcegroups/*/providers/Microsoft.Synapse/workspaces/*" + endpoint_tenant_id = data.azurerm_client_config.current.tenant_id + } + } + dynamic "private_link_access" { + for_each = tolist(setunion(var.data_platform_subscription_ids, [data.azurerm_client_config.current.subscription_id])) + content { + endpoint_resource_id = "/subscriptions/${private_link_access.value}/resourcegroups/*/providers/Microsoft.MachineLearningServices/workspaces/*" + endpoint_tenant_id = data.azurerm_client_config.current.tenant_id + } + } + } + nfsv3_enabled = false + public_network_access_enabled = true + queue_encryption_key_type = "Account" + table_encryption_key_type = "Account" + routing { + choice = "MicrosoftRouting" + publish_internet_endpoints = false + publish_microsoft_endpoints = false + } + sftp_enabled = false + shared_access_key_enabled = true +} + +resource "azurerm_storage_management_policy" "storage_management_policy" { + storage_account_id = azurerm_storage_account.storage.id + + rule { + name = "default" + enabled = true + actions { + base_blob { + tier_to_cool_after_days_since_modification_greater_than = 360 + # delete_after_days_since_modification_greater_than = 720 + } + snapshot { + change_tier_to_cool_after_days_since_creation = 180 + delete_after_days_since_creation_greater_than = 360 + } + version { + change_tier_to_cool_after_days_since_creation = 180 + delete_after_days_since_creation = 360 + } + } + filters { + blob_types = ["blockBlob"] + prefix_match = [] + } + } +} + +resource "azurerm_storage_container" "storage_container_machine_learning_workspace" { + name = "azureml" + storage_account_name = azurerm_storage_account.storage.name + + container_access_type = "private" + + depends_on = [ + azurerm_role_assignment.current_roleassignment_storage, + azurerm_private_endpoint.storage_private_endpoint_blob, + ] +} + +resource "azurerm_storage_share" "storage_share_machine_learning_workspace" { + name = "code" + storage_account_name = azurerm_storage_account.storage.name + + access_tier = "TransactionOptimized" + enabled_protocol = "SMB" + quota = 5120 + + depends_on = [ + azurerm_role_assignment.current_roleassignment_storage, + azurerm_private_endpoint.storage_private_endpoint_file, + ] +} + +resource "azurerm_private_endpoint" "storage_private_endpoint_blob" { + name = "${azurerm_storage_account.storage.name}-blob-pe" + location = var.location + resource_group_name = azurerm_storage_account.storage.resource_group_name + tags = var.tags + + custom_network_interface_name = "${azurerm_storage_account.storage.name}-blob-nic" + private_service_connection { + name = "${azurerm_storage_account.storage.name}-blob-pe" + is_manual_connection = false + private_connection_resource_id = azurerm_storage_account.storage.id + subresource_names = ["blob"] + } + subnet_id = data.azurerm_subnet.subnet.id + dynamic "private_dns_zone_group" { + for_each = var.private_dns_zone_id_blob == "" ? [] : [1] + content { + name = "${azurerm_storage_account.storage.name}-blob-arecord" + private_dns_zone_ids = [ + var.private_dns_zone_id_blob + ] + } + } +} + +resource "azurerm_private_endpoint" "storage_private_endpoint_file" { + name = "${azurerm_storage_account.storage.name}-file-pe" + location = var.location + resource_group_name = azurerm_storage_account.storage.resource_group_name + tags = var.tags + + custom_network_interface_name = "${azurerm_storage_account.storage.name}-file-nic" + private_service_connection { + name = "${azurerm_storage_account.storage.name}-file-pe" + is_manual_connection = false + private_connection_resource_id = azurerm_storage_account.storage.id + subresource_names = ["file"] + } + subnet_id = data.azurerm_subnet.subnet.id + dynamic "private_dns_zone_group" { + for_each = var.private_dns_zone_id_file == "" ? [] : [1] + content { + name = "${azurerm_storage_account.storage.name}-file-arecord" + private_dns_zone_ids = [ + var.private_dns_zone_id_file + ] + } + } +} + +resource "azurerm_private_endpoint" "storage_private_endpoint_table" { + name = "${azurerm_storage_account.storage.name}-table-pe" + location = var.location + resource_group_name = azurerm_storage_account.storage.resource_group_name + tags = var.tags + + custom_network_interface_name = "${azurerm_storage_account.storage.name}-table-nic" + private_service_connection { + name = "${azurerm_storage_account.storage.name}-table-pe" + is_manual_connection = false + private_connection_resource_id = azurerm_storage_account.storage.id + subresource_names = ["table"] + } + subnet_id = data.azurerm_subnet.subnet.id + dynamic "private_dns_zone_group" { + for_each = var.private_dns_zone_id_table == "" ? [] : [1] + content { + name = "${azurerm_storage_account.storage.name}-table-arecord" + private_dns_zone_ids = [ + var.private_dns_zone_id_table + ] + } + } +} + +resource "azurerm_private_endpoint" "storage_private_endpoint_queue" { + name = "${azurerm_storage_account.storage.name}-queue-pe" + location = var.location + resource_group_name = azurerm_storage_account.storage.resource_group_name + tags = var.tags + + custom_network_interface_name = "${azurerm_storage_account.storage.name}-queue-nic" + private_service_connection { + name = "${azurerm_storage_account.storage.name}-queue-pe" + is_manual_connection = false + private_connection_resource_id = azurerm_storage_account.storage.id + subresource_names = ["queue"] + } + subnet_id = data.azurerm_subnet.subnet.id + dynamic "private_dns_zone_group" { + for_each = var.private_dns_zone_id_queue == "" ? [] : [1] + content { + name = "${azurerm_storage_account.storage.name}-queue-arecord" + private_dns_zone_ids = [ + var.private_dns_zone_id_queue + ] + } + } +} diff --git a/code/terraform/terraform.tf b/code/terraform/terraform.tf new file mode 100644 index 0000000..f9a0c5d --- /dev/null +++ b/code/terraform/terraform.tf @@ -0,0 +1,57 @@ +terraform { + required_version = ">=0.13" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">= 3.56.0" + } + azapi = { + source = "azure/azapi" + version = ">= 1.5.0" + } + } + + backend "azurerm" { + environment = "public" + resource_group_name = "mycrp-prd-cicd" + storage_account_name = "mycrpprdstg001" + container_name = "data-product-analytics" + key = "terraform.tfstate" + use_azuread_auth = true + # use_oidc = true + } +} + +provider "azurerm" { + disable_correlation_request_id = false + environment = "public" + skip_provider_registration = false + storage_use_azuread = true + # use_oidc = true + + features { + 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 + } + resource_group { + prevent_deletion_if_contains_resources = true + } + } +} + +provider "azapi" { + default_location = var.location + default_tags = var.tags + disable_correlation_request_id = false + environment = "public" + skip_provider_registration = false + # use_oidc = true +} diff --git a/code/terraform/userassignedidentity.tf b/code/terraform/userassignedidentity.tf new file mode 100644 index 0000000..8974440 --- /dev/null +++ b/code/terraform/userassignedidentity.tf @@ -0,0 +1,6 @@ +resource "azurerm_user_assigned_identity" "user_assigned_identity" { + name = "${local.prefix}-uai001" + location = var.location + resource_group_name = data.azurerm_resource_group.resource_group.name + tags = var.tags +} diff --git a/code/terraform/variables.tf b/code/terraform/variables.tf new file mode 100644 index 0000000..a4c1d77 --- /dev/null +++ b/code/terraform/variables.tf @@ -0,0 +1,185 @@ +variable "location" { + description = "Specifies the location for all Azure resources." + type = string + sensitive = false +} + +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 "tags" { + description = "Specifies the tags that you want to apply to all resources." + type = map(string) + sensitive = false + default = {} +} + +variable "resource_group_name" { + description = "Specifies the name of the resource group in which all resources will be deployed." + type = string + sensitive = false + validation { + condition = length(var.resource_group_name) >= 2 + error_message = "Please specify a valid resource group name." + } +} + +variable "subnet_id" { + description = "Specifies the resource ID of the subnet used for the Private Endpoints." + type = string + sensitive = false + validation { + condition = length(split("/", var.subnet_id)) == 11 + error_message = "Please specify a valid resource ID." + } +} + +variable "machine_learning_compute_clusters" { + type = map(object({ + vm_priority = optional(string, "Dedicated") + vm_size = optional(string, "Standard_DS2_v2") + scale = object({ + min_node_count = optional(any, 0) + max_node_count = optional(any, 3) + scale_down_nodes_after_idle_duration = optional(string, "PT60S") + }) + })) + sensitive = false + default = {} + description = "Specifies the compute cluster to be created for the Machine Learning Workspace." + validation { + condition = alltrue([ + length([for vm_priority in values(var.machine_learning_compute_clusters)[*].vm_priority : vm_priority if !contains(["Dedicated", "Dedicated"], vm_priority)]) <= 0 + ]) + error_message = "Please specify a compute cluster configuration." + } +} + +variable "machine_learning_compute_instances" { + type = map(object({ + user_object_id = string + vm_size = optional(string, "Standard_DS2_v2") + })) + sensitive = false + default = {} + description = "Specifies the compute instances to be created for the Machine Learning Workspace." + # validation { + # condition = alltrue([ + # length([for vm_priority in values(var.machine_learning_compute_clusters)[*].vm_priority : vm_priority if !contains(["Dedicated", "Dedicated"], vm_priority)]) <= 0 + # ]) + # error_message = "Please specify a compute instance configuration." + # } +} + +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_key_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 + default = "" + validation { + condition = var.private_dns_zone_id_key_vault == "" || (length(split("/", var.private_dns_zone_id_key_vault)) == 9 && endswith(var.private_dns_zone_id_key_vault, "privatelink.vaultcore.azure.net")) + error_message = "Please specify a valid resource ID for the private DNS Zone." + } +} + +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." + } +} + +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 "data_platform_subscription_ids" { + description = "Specifies the list of subscription IDs of your data platform." + type = list(string) + sensitive = false + default = [] +} diff --git a/code/terraform/vars.dev.tfvars b/code/terraform/vars.dev.tfvars new file mode 100644 index 0000000..fbfe127 --- /dev/null +++ b/code/terraform/vars.dev.tfvars @@ -0,0 +1,32 @@ +location = "northeurope" +environment = "dev" +prefix = "dpa" +tags = {} +resource_group_name = "myprod-dev-analytics-rg" +subnet_id = "/subscriptions/8f171ff9-2b5b-4f0f-aed5-7fa360a1d094/resourceGroups/mycrp-prd-logic-network-rg/providers/Microsoft.Network/virtualNetworks/mycrp-prd-logic-vnet001/subnets/PeSubnet" +machine_learning_compute_clusters = { + "cpu001" = { + vm_priority = "Dedicated" + vm_size = "Standard_DS2_v2" + scale = { + min_node_count = 0 + max_node_count = 3 + scale_down_nodes_after_idle_duration = "PT30S" + } + } +} +machine_learning_compute_instances = { + "mabuss" = { + vm_size = "Standard_DS2_v2" + user_object_id = "540d8186-5d32-4ab6-a962-0d91ba5bd2c2" + } +} +private_dns_zone_id_blob = "/subscriptions/8f171ff9-2b5b-4f0f-aed5-7fa360a1d094/resourceGroups/mycrp-prd-global-dns/providers/Microsoft.Network/privateDnsZones/privatelink.blob.core.windows.net" +private_dns_zone_id_file = "/subscriptions/8f171ff9-2b5b-4f0f-aed5-7fa360a1d094/resourceGroups/mycrp-prd-global-dns/providers/Microsoft.Network/privateDnsZones/privatelink.file.core.windows.net" +private_dns_zone_id_table = "/subscriptions/8f171ff9-2b5b-4f0f-aed5-7fa360a1d094/resourceGroups/mycrp-prd-global-dns/providers/Microsoft.Network/privateDnsZones/privatelink.table.core.windows.net" +private_dns_zone_id_queue = "/subscriptions/8f171ff9-2b5b-4f0f-aed5-7fa360a1d094/resourceGroups/mycrp-prd-global-dns/providers/Microsoft.Network/privateDnsZones/privatelink.queue.core.windows.net" +private_dns_zone_id_container_registry = "/subscriptions/8f171ff9-2b5b-4f0f-aed5-7fa360a1d094/resourceGroups/mycrp-prd-global-dns/providers/Microsoft.Network/privateDnsZones/privatelink.azurecr.io" +private_dns_zone_id_key_vault = "/subscriptions/8f171ff9-2b5b-4f0f-aed5-7fa360a1d094/resourceGroups/mycrp-prd-global-dns/providers/Microsoft.Network/privateDnsZones/privatelink.vaultcore.azure.net" +private_dns_zone_id_machine_learning_api = "/subscriptions/8f171ff9-2b5b-4f0f-aed5-7fa360a1d094/resourceGroups/mycrp-prd-global-dns/providers/Microsoft.Network/privateDnsZones/privatelink.api.azureml.ms" +private_dns_zone_id_machine_learning_notebooks = "/subscriptions/8f171ff9-2b5b-4f0f-aed5-7fa360a1d094/resourceGroups/mycrp-prd-global-dns/providers/Microsoft.Network/privateDnsZones/privatelink.notebooks.azure.net" +data_platform_subscription_ids = []