From 74acfe94c772dfea0973efa1934e41d15beac405 Mon Sep 17 00:00:00 2001 From: David O'Keeffe Date: Wed, 26 Aug 2026 12:02:58 +1000 Subject: [PATCH] feat: add serverless App Gateway TLS transit example --- README.md | 2 + .../adb-serverless-appgw-tls-transit/Makefile | 7 + .../README.md | 84 +++++++++ .../adb-serverless-appgw-tls-transit/main.tf | 29 +++ .../outputs.tf | 24 +++ .../providers.tf | 41 +++++ .../terraform.tfvars | 32 ++++ .../variables.tf | 84 +++++++++ .../adb-serverless-appgw-tls-transit/Makefile | 7 + .../README.md | 116 ++++++++++++ .../adb-serverless-appgw-tls-transit/appgw.tf | 151 +++++++++++++++ .../checks.tf | 6 + .../adb-serverless-appgw-tls-transit/ncc.tf | 173 ++++++++++++++++++ .../network.tf | 91 +++++++++ .../outputs.tf | 34 ++++ .../providers.tf | 26 +++ .../variables.tf | 171 +++++++++++++++++ 17 files changed, 1078 insertions(+) create mode 100644 examples/adb-serverless-appgw-tls-transit/Makefile create mode 100644 examples/adb-serverless-appgw-tls-transit/README.md create mode 100644 examples/adb-serverless-appgw-tls-transit/main.tf create mode 100644 examples/adb-serverless-appgw-tls-transit/outputs.tf create mode 100644 examples/adb-serverless-appgw-tls-transit/providers.tf create mode 100644 examples/adb-serverless-appgw-tls-transit/terraform.tfvars create mode 100644 examples/adb-serverless-appgw-tls-transit/variables.tf create mode 100644 modules/adb-serverless-appgw-tls-transit/Makefile create mode 100644 modules/adb-serverless-appgw-tls-transit/README.md create mode 100644 modules/adb-serverless-appgw-tls-transit/appgw.tf create mode 100644 modules/adb-serverless-appgw-tls-transit/checks.tf create mode 100644 modules/adb-serverless-appgw-tls-transit/ncc.tf create mode 100644 modules/adb-serverless-appgw-tls-transit/network.tf create mode 100644 modules/adb-serverless-appgw-tls-transit/outputs.tf create mode 100644 modules/adb-serverless-appgw-tls-transit/providers.tf create mode 100644 modules/adb-serverless-appgw-tls-transit/variables.tf diff --git a/README.md b/README.md index 1c66db11..589e623e 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ The folder `examples` contains the following Terraform implementation examples : | Azure | ~~adb-external-hive-metastore~~ **REMOVED** | This example was removed in February 2026. External Hive metastore has been superseded by [Unity Catalog](https://docs.databricks.com/en/data-governance/unity-catalog/index.html). Use [adb-unity-catalog-basic-demo](examples/adb-unity-catalog-basic-demo/) instead. | | Azure | [adb-kafka](examples/adb-kafka/) | ADB - single node kafka template | | Azure | [adb-private-links](examples/adb-private-links/) | Azure Databricks Private Links | +| Azure | [adb-serverless-appgw-tls-transit](examples/adb-serverless-appgw-tls-transit/) | Serverless compute to a TLS service through an Application Gateway v2 TCP/TLS proxy and NCC private endpoint | | Azure | [adb-squid-proxy](examples/adb-squid-proxy/) | ADB clusters with HTTP proxy | | Azure | [adb-teradata](examples/adb-teradata/) | ADB with single VM Teradata integration | | Azure | [adb-uc](examples/adb-uc/) | ADB Unity Catalog Process | @@ -77,6 +78,7 @@ The folder `modules` contains the following Terraform modules : | Azure | [adb-with-private-link-standard](modules/adb-with-private-link-standard/) | Provisioning Databricks on Azure with Private Link - Standard deployment | | Azure | [adb-exfiltration-protection](modules/adb-exfiltration-protection/) | A sample implementation of [Data Exfiltration Protection](https://www.databricks.com/blog/2020/03/27/data-exfiltration-protection-with-azure-databricks.html) | | Azure | [adb-with-private-links-exfiltration-protection](modules/adb-with-private-links-exfiltration-protection/) | Provisioning Databricks on Azure with Private Link and [Data Exfiltration Protection](https://www.databricks.com/blog/2020/03/27/data-exfiltration-protection-with-azure-databricks.html) | +| Azure | [adb-serverless-appgw-tls-transit](modules/adb-serverless-appgw-tls-transit/) | Serverless compute to a TLS service through an Application Gateway v2 TCP/TLS proxy and NCC private endpoint | | Azure | [adb-overwatch-regional-config](modules/adb-overwatch-regional-config/) | Overwatch regional configuration on Azure | | Azure | [adb-overwatch-mws-config](modules/adb-overwatch-mws-config/) | Overwatch multi-workspace deployment on Azure | | Azure | [adb-overwatch-main-ws](modules/adb-overwatch-main-ws/) | Main Overwatch workspace deployment | diff --git a/examples/adb-serverless-appgw-tls-transit/Makefile b/examples/adb-serverless-appgw-tls-transit/Makefile new file mode 100644 index 00000000..653039d8 --- /dev/null +++ b/examples/adb-serverless-appgw-tls-transit/Makefile @@ -0,0 +1,7 @@ +.PHONY: docs test_docs + +docs: + terraform-docs -c ../../.terraform-docs.yml . + +test_docs: + terraform-docs -c ../../.terraform-docs.yml --output-check . diff --git a/examples/adb-serverless-appgw-tls-transit/README.md b/examples/adb-serverless-appgw-tls-transit/README.md new file mode 100644 index 00000000..fc917882 --- /dev/null +++ b/examples/adb-serverless-appgw-tls-transit/README.md @@ -0,0 +1,84 @@ +# Serverless compute to a TLS service through Application Gateway v2 + +This example deploys the [`adb-serverless-appgw-tls-transit`](../../modules/adb-serverless-appgw-tls-transit) module. It connects Databricks Serverless compute to a customer-managed TLS-over-TCP service (for example, Confluent Cloud Kafka) through an Azure Application Gateway v2 TCP proxy and the Databricks Network Connectivity Configuration (NCC) service. + +```text +Databricks Serverless -> NCC private endpoint -> Application Gateway v2 + -> TLS backend +``` + +The Application Gateway passes encrypted TCP/TLS traffic through; it does not terminate TLS. The target service must be reachable from the transit VNet using a private endpoint, VNet peering, or another private route. This example accepts backend IPv4 addresses or FQDNs and does not provision the target service. + +## Prerequisites + +* A Premium Azure Databricks account and account-admin permissions. +* An Azure subscription and permissions to create a VNet, subnets, NSG, public IP, and Application Gateway. +* Azure CLI authenticated as a Databricks account admin. The module uses the documented Network Connectivity Configurations REST API because Application Gateway rules require `resource_id`, `group_id`, and `domain_names` together. +* A TLS-over-TCP backend reachable from the transit VNet and the FQDNs that Serverless clients dial. For Kafka, include the bootstrap FQDN and the broker names returned in metadata. + +## How to use + +1. Update `terraform.tfvars` with your Azure, Databricks, backend, and DNS values. +2. Run `terraform init`. +3. Run `terraform plan` and `terraform apply`. +4. Approve the pending private endpoint connection on the Application Gateway, unless `auto_approve_private_endpoint` is enabled and the Azure CLI identity can approve it. +5. Wait until the NCC private endpoint rule is `ESTABLISHED`, restart Serverless compute, and test the TLS service. + +The Standard_v2 SKU currently requires a public IP resource for gateway management. The example creates that resource but does not bind a listener to the public frontend. Databricks traffic uses the private frontend exposed through Application Gateway Private Link. If your subscription has Azure network isolation enabled and no public IP is required, adjust the Application Gateway definition accordingly. + + +## Requirements + +| Name | Version | +| ---- | ------- | +| [terraform](#requirement\_terraform) | >= 1.9.0 | +| [azapi](#requirement\_azapi) | 2.0.1 | +| [azurerm](#requirement\_azurerm) | >=4.31.0 | +| [databricks](#requirement\_databricks) | >=1.81.1 | +| [null](#requirement\_null) | >=3.2.0 | +| [time](#requirement\_time) | >=0.9.0 | + +## Providers + +No providers. + +## Modules + +| Name | Source | Version | +| ---- | ------ | ------- | +| [adb-serverless-appgw-tls-transit](#module\_adb-serverless-appgw-tls-transit) | ../../modules/adb-serverless-appgw-tls-transit | n/a | + +## Resources + +No resources. + +## Inputs + +| Name | Description | Type | Default | Required | +| ---- | ----------- | ---- | ------- | :------: | +| [azure\_region](#input\_azure\_region) | Azure region short name. Must match the workspace and NCC region. | `string` | n/a | yes | +| [azure\_subscription\_id](#input\_azure\_subscription\_id) | Azure subscription ID to deploy into. | `string` | n/a | yes | +| [databricks\_account\_id](#input\_databricks\_account\_id) | Databricks account ID (UUID). | `string` | n/a | yes | +| [databricks\_workspace\_id](#input\_databricks\_workspace\_id) | Databricks workspace ID to bind to the NCC. | `string` | n/a | yes | +| [serverless\_domain\_names](#input\_serverless\_domain\_names) | FQDNs that serverless clients dial. The NCC rule supports at most 10 names. | `list(string)` | n/a | yes | +| [appgw\_capacity](#input\_appgw\_capacity) | Fixed Standard\_v2 Application Gateway instance capacity. | `number` | `2` | no | +| [appgw\_name](#input\_appgw\_name) | Application Gateway name. | `string` | `"appgw-serverless-transit"` | no | +| [auto\_approve\_private\_endpoint](#input\_auto\_approve\_private\_endpoint) | Attempt to approve the Databricks-created private endpoint with Azure CLI. | `bool` | `false` | no | +| [backend\_addresses](#input\_backend\_addresses) | IPv4 addresses of TLS backends reachable from the transit VNet. | `list(string)` | `[]` | no | +| [backend\_fqdns](#input\_backend\_fqdns) | FQDNs of TLS backends reachable from the transit VNet. | `list(string)` | `[]` | no | +| [backend\_port](#input\_backend\_port) | TCP/TLS port used by the backend. Defaults to listener\_port. | `number` | `null` | no | +| [databricks\_host](#input\_databricks\_host) | Databricks account console host. | `string` | `"https://accounts.azuredatabricks.net"` | no | +| [listener\_port](#input\_listener\_port) | TCP/TLS port exposed by the Application Gateway. | `number` | `9092` | no | +| [rg\_name](#input\_rg\_name) | Resource group to create for the transit resources. | `string` | `"rg-appgw-tls-transit"` | no | +| [tags](#input\_tags) | Tags applied to created resources. | `map(string)` | `{}` | no | + +## Outputs + +| Name | Description | +| ---- | ----------- | +| [appgw\_frontend\_config\_name](#output\_appgw\_frontend\_config\_name) | Frontend configuration name used as the NCC rule group\_id. | +| [appgw\_id](#output\_appgw\_id) | Resource ID of the Application Gateway. | +| [ncc\_id](#output\_ncc\_id) | Databricks NCC ID. | +| [serverless\_domain\_names](#output\_serverless\_domain\_names) | FQDNs registered in the NCC private endpoint rule. | +| [transit\_vnet\_id](#output\_transit\_vnet\_id) | Transit VNet ID. Peer the target service network here or place a private endpoint in it. | + diff --git a/examples/adb-serverless-appgw-tls-transit/main.tf b/examples/adb-serverless-appgw-tls-transit/main.tf new file mode 100644 index 00000000..b687e279 --- /dev/null +++ b/examples/adb-serverless-appgw-tls-transit/main.tf @@ -0,0 +1,29 @@ +module "adb-serverless-appgw-tls-transit" { + source = "../../modules/adb-serverless-appgw-tls-transit" + + providers = { + azurerm = azurerm + azapi = azapi + databricks = databricks.accounts + null = null + time = time + } + + azure_subscription_id = var.azure_subscription_id + azure_region = var.azure_region + rg_name = var.rg_name + appgw_name = var.appgw_name + appgw_capacity = var.appgw_capacity + databricks_host = var.databricks_host + databricks_account_id = var.databricks_account_id + databricks_workspace_id = var.databricks_workspace_id + + backend_addresses = var.backend_addresses + backend_fqdns = var.backend_fqdns + listener_port = var.listener_port + backend_port = var.backend_port + serverless_domain_names = var.serverless_domain_names + auto_approve_private_endpoint = var.auto_approve_private_endpoint + + tags = var.tags +} diff --git a/examples/adb-serverless-appgw-tls-transit/outputs.tf b/examples/adb-serverless-appgw-tls-transit/outputs.tf new file mode 100644 index 00000000..56110982 --- /dev/null +++ b/examples/adb-serverless-appgw-tls-transit/outputs.tf @@ -0,0 +1,24 @@ +output "appgw_id" { + description = "Resource ID of the Application Gateway." + value = module.adb-serverless-appgw-tls-transit.appgw_id +} + +output "appgw_frontend_config_name" { + description = "Frontend configuration name used as the NCC rule group_id." + value = module.adb-serverless-appgw-tls-transit.appgw_frontend_config_name +} + +output "ncc_id" { + description = "Databricks NCC ID." + value = module.adb-serverless-appgw-tls-transit.ncc_id +} + +output "serverless_domain_names" { + description = "FQDNs registered in the NCC private endpoint rule." + value = module.adb-serverless-appgw-tls-transit.serverless_domain_names +} + +output "transit_vnet_id" { + description = "Transit VNet ID. Peer the target service network here or place a private endpoint in it." + value = module.adb-serverless-appgw-tls-transit.transit_vnet_id +} diff --git a/examples/adb-serverless-appgw-tls-transit/providers.tf b/examples/adb-serverless-appgw-tls-transit/providers.tf new file mode 100644 index 00000000..078a9667 --- /dev/null +++ b/examples/adb-serverless-appgw-tls-transit/providers.tf @@ -0,0 +1,41 @@ +terraform { + required_version = ">= 1.9.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">=4.31.0" + } + azapi = { + source = "Azure/azapi" + version = "2.0.1" + } + databricks = { + source = "databricks/databricks" + version = ">=1.81.1" + } + null = { + source = "hashicorp/null" + version = ">=3.2.0" + } + time = { + source = "hashicorp/time" + version = ">=0.9.0" + } + } +} + +provider "azurerm" { + subscription_id = var.azure_subscription_id + features {} +} + +provider "azapi" { + subscription_id = var.azure_subscription_id +} + +provider "databricks" { + alias = "accounts" + host = var.databricks_host + account_id = var.databricks_account_id +} diff --git a/examples/adb-serverless-appgw-tls-transit/terraform.tfvars b/examples/adb-serverless-appgw-tls-transit/terraform.tfvars new file mode 100644 index 00000000..8be4335e --- /dev/null +++ b/examples/adb-serverless-appgw-tls-transit/terraform.tfvars @@ -0,0 +1,32 @@ +azure_subscription_id = "00000000-0000-0000-0000-000000000000" +azure_region = "australiaeast" +rg_name = "rg-appgw-tls-transit" + +appgw_name = "appgw-serverless-transit" +appgw_capacity = 2 + +databricks_host = "https://accounts.azuredatabricks.net" +databricks_account_id = "00000000-0000-0000-0000-000000000000" +databricks_workspace_id = "1234567890123456" + +# Use either backend_addresses (IP addresses) or backend_fqdns (DNS names). +# The target must be reachable from the transit VNet. +backend_addresses = ["10.230.3.10"] +backend_fqdns = [] + +# Include the bootstrap name and any names returned by Kafka broker metadata. +serverless_domain_names = [ + "lkc-xxxxx..australiaeast.azure.confluent.cloud", + "*..australiaeast.azure.confluent.cloud", +] + +listener_port = 9092 +backend_port = null + +# Leave false to approve the App Gateway private endpoint manually in Azure. +auto_approve_private_endpoint = false + +tags = { + Environment = "dev" + Workload = "serverless-tls-privatelink" +} diff --git a/examples/adb-serverless-appgw-tls-transit/variables.tf b/examples/adb-serverless-appgw-tls-transit/variables.tf new file mode 100644 index 00000000..aa35f9d4 --- /dev/null +++ b/examples/adb-serverless-appgw-tls-transit/variables.tf @@ -0,0 +1,84 @@ +variable "azure_subscription_id" { + type = string + description = "Azure subscription ID to deploy into." +} + +variable "azure_region" { + type = string + description = "Azure region short name. Must match the workspace and NCC region." +} + +variable "rg_name" { + type = string + description = "Resource group to create for the transit resources." + default = "rg-appgw-tls-transit" +} + +variable "appgw_name" { + type = string + description = "Application Gateway name." + default = "appgw-serverless-transit" +} + +variable "appgw_capacity" { + type = number + description = "Fixed Standard_v2 Application Gateway instance capacity." + default = 2 +} + +variable "databricks_host" { + type = string + description = "Databricks account console host." + default = "https://accounts.azuredatabricks.net" +} + +variable "databricks_account_id" { + type = string + description = "Databricks account ID (UUID)." +} + +variable "databricks_workspace_id" { + type = string + description = "Databricks workspace ID to bind to the NCC." +} + +variable "backend_addresses" { + type = list(string) + description = "IPv4 addresses of TLS backends reachable from the transit VNet." + default = [] +} + +variable "backend_fqdns" { + type = list(string) + description = "FQDNs of TLS backends reachable from the transit VNet." + default = [] +} + +variable "serverless_domain_names" { + type = list(string) + description = "FQDNs that serverless clients dial. The NCC rule supports at most 10 names." +} + +variable "listener_port" { + type = number + description = "TCP/TLS port exposed by the Application Gateway." + default = 9092 +} + +variable "backend_port" { + type = number + description = "TCP/TLS port used by the backend. Defaults to listener_port." + default = null +} + +variable "auto_approve_private_endpoint" { + type = bool + description = "Attempt to approve the Databricks-created private endpoint with Azure CLI." + default = false +} + +variable "tags" { + type = map(string) + description = "Tags applied to created resources." + default = {} +} diff --git a/modules/adb-serverless-appgw-tls-transit/Makefile b/modules/adb-serverless-appgw-tls-transit/Makefile new file mode 100644 index 00000000..653039d8 --- /dev/null +++ b/modules/adb-serverless-appgw-tls-transit/Makefile @@ -0,0 +1,7 @@ +.PHONY: docs test_docs + +docs: + terraform-docs -c ../../.terraform-docs.yml . + +test_docs: + terraform-docs -c ../../.terraform-docs.yml --output-check . diff --git a/modules/adb-serverless-appgw-tls-transit/README.md b/modules/adb-serverless-appgw-tls-transit/README.md new file mode 100644 index 00000000..30c8d3f0 --- /dev/null +++ b/modules/adb-serverless-appgw-tls-transit/README.md @@ -0,0 +1,116 @@ +# Serverless compute to a TLS service through Application Gateway v2 + +This module provides private connectivity from **Databricks Serverless compute** to a customer-managed **TLS-over-TCP service** (for example, Confluent Cloud Kafka) through an Azure Application Gateway v2 TCP proxy and a Databricks Network Connectivity Configuration (NCC). + +```text +Databricks Serverless -- NCC private endpoint --> App Gateway v2 -- TLS/TCP --> backend service +``` + +The listener passes encrypted TCP/TLS traffic through end-to-end. Application Gateway does not terminate TLS, so the backend service remains responsible for TLS or mTLS. The module creates the transit VNet, Application Gateway, its native Private Link configuration, and the NCC. It does not create or configure the backend service or its route into the transit VNet. + +## Why the NCC rule uses the REST API + +The Application Gateway v2 NCC rule requires `resource_id`, `group_id`, and `domain_names` in one request. The Databricks Terraform provider resource does not currently expose this combination, so the module reconciles the rule through the documented Network Connectivity Configurations API. The local machine must have Azure CLI authenticated as a Databricks account admin. + +The reconciliation is idempotent: an existing rule matching the Application Gateway resource and frontend group is patched when domain names change instead of creating a duplicate rule. Destroying the module removes that rule. + +## Application Gateway public IP + +Standard_v2 currently requires a public IP resource for GatewayManager communication. The module creates the required public IP but does not attach a listener to that frontend. The TCP listener and Private Link association use a separate private frontend, and an NSG denies Internet ingress to the Application Gateway subnet while allowing the documented GatewayManager and AzureLoadBalancer service tags. + +If Azure network isolation is enabled for the subscription and removes this SKU requirement, review `appgw.tf` before removing the public frontend. + +## Prerequisites + +* Premium-tier Azure Databricks account and account-admin permissions. +* Azure permissions to create the resource group, VNet, subnets, NSG, public IP, and Application Gateway. +* Azure CLI authenticated as the Databricks account admin running Terraform. +* A TLS-over-TCP backend reachable from the transit VNet through private routing, VNet peering, or a private endpoint. +* All FQDNs that Serverless clients may dial. Kafka deployments should include the bootstrap name and names returned by broker metadata. + +## Deployment sequence + +1. Apply the module. +2. Approve the pending Application Gateway private endpoint in Azure, unless `auto_approve_private_endpoint` is enabled. +3. Wait for the NCC private endpoint rule to become `ESTABLISHED`. +4. Restart Serverless compute and test the backend connection. + + +## Requirements + +| Name | Version | +| ---- | ------- | +| [terraform](#requirement\_terraform) | >= 1.9.0 | +| [azapi](#requirement\_azapi) | 2.0.1 | +| [azurerm](#requirement\_azurerm) | >=4.31.0 | +| [databricks](#requirement\_databricks) | >=1.81.1 | +| [null](#requirement\_null) | >=3.2.0 | +| [time](#requirement\_time) | >=0.9.0 | + +## Providers + +| Name | Version | +| ---- | ------- | +| [azapi](#provider\_azapi) | 2.0.1 | +| [azurerm](#provider\_azurerm) | 5.1.0 | +| [databricks](#provider\_databricks) | 1.127.0 | +| [null](#provider\_null) | 3.3.1 | +| [time](#provider\_time) | 0.14.1 | + +## Modules + +No modules. + +## Resources + +| Name | Type | +| ---- | ---- | +| [azapi_resource.appgw](https://registry.terraform.io/providers/Azure/azapi/2.0.1/docs/resources/resource) | resource | +| [azurerm_network_security_group.appgw](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/network_security_group) | resource | +| [azurerm_public_ip.appgw](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/public_ip) | resource | +| [azurerm_resource_group.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/resource_group) | resource | +| [azurerm_subnet.appgw](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet) | resource | +| [azurerm_subnet.appgw_pls](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet) | resource | +| [azurerm_subnet_network_security_group_association.appgw](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/subnet_network_security_group_association) | resource | +| [azurerm_virtual_network.this](https://registry.terraform.io/providers/hashicorp/azurerm/latest/docs/resources/virtual_network) | resource | +| [databricks_mws_ncc_binding.this](https://registry.terraform.io/providers/databricks/databricks/latest/docs/resources/mws_ncc_binding) | resource | +| [databricks_mws_network_connectivity_config.this](https://registry.terraform.io/providers/databricks/databricks/latest/docs/resources/mws_network_connectivity_config) | resource | +| [null_resource.approve_private_endpoint](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | +| [null_resource.ncc_private_endpoint_rule](https://registry.terraform.io/providers/hashicorp/null/latest/docs/resources/resource) | resource | +| [time_sleep.wait_for_private_endpoint](https://registry.terraform.io/providers/hashicorp/time/latest/docs/resources/sleep) | resource | + +## Inputs + +| Name | Description | Type | Default | Required | +| ---- | ----------- | ---- | ------- | :------: | +| [azure\_region](#input\_azure\_region) | Azure region short name. Must match the workspace and NCC region. | `string` | n/a | yes | +| [azure\_subscription\_id](#input\_azure\_subscription\_id) | Azure subscription ID to deploy into. | `string` | n/a | yes | +| [databricks\_account\_id](#input\_databricks\_account\_id) | Databricks account ID (UUID). | `string` | n/a | yes | +| [databricks\_workspace\_id](#input\_databricks\_workspace\_id) | Databricks workspace ID to bind to the NCC. | `string` | n/a | yes | +| [serverless\_domain\_names](#input\_serverless\_domain\_names) | FQDNs that serverless clients dial. The NCC rule supports at most 10 names. | `list(string)` | n/a | yes | +| [appgw\_capacity](#input\_appgw\_capacity) | Fixed Standard\_v2 Application Gateway instance capacity. | `number` | `2` | no | +| [appgw\_name](#input\_appgw\_name) | Application Gateway name. | `string` | `"appgw-serverless-transit"` | no | +| [appgw\_pls\_subnet\_prefix](#input\_appgw\_pls\_subnet\_prefix) | Address prefix for the dedicated Application Gateway Private Link subnet. | `string` | `"10.230.2.0/24"` | no | +| [appgw\_subnet\_prefix](#input\_appgw\_subnet\_prefix) | Address prefix for the Application Gateway subnet. The subnet must provide at least ten usable host addresses. | `string` | `"10.230.1.0/24"` | no | +| [auto\_approve\_private\_endpoint](#input\_auto\_approve\_private\_endpoint) | Attempt to approve the Databricks-created private endpoint with Azure CLI. | `bool` | `false` | no | +| [backend\_addresses](#input\_backend\_addresses) | IPv4 addresses of TLS backends reachable from the transit VNet. | `list(string)` | `[]` | no | +| [backend\_fqdns](#input\_backend\_fqdns) | FQDNs of TLS backends reachable from the transit VNet. | `list(string)` | `[]` | no | +| [backend\_port](#input\_backend\_port) | TCP/TLS port used by the backend. Defaults to listener\_port. | `number` | `null` | no | +| [databricks\_host](#input\_databricks\_host) | Databricks account console host. | `string` | `"https://accounts.azuredatabricks.net"` | no | +| [listener\_port](#input\_listener\_port) | TCP/TLS port exposed by the Application Gateway. | `number` | `9092` | no | +| [rg\_name](#input\_rg\_name) | Resource group to create for the transit resources. | `string` | `"rg-appgw-tls-transit"` | no | +| [tags](#input\_tags) | Tags applied to created resources. | `map(string)` | `{}` | no | +| [vnet\_address\_space](#input\_vnet\_address\_space) | Address space for the transit VNet. | `list(string)` |
[
"10.230.0.0/16"
]
| no | + +## Outputs + +| Name | Description | +| ---- | ----------- | +| [appgw\_frontend\_config\_name](#output\_appgw\_frontend\_config\_name) | Private frontend configuration name used as the NCC rule group\_id. | +| [appgw\_frontend\_private\_ip](#output\_appgw\_frontend\_private\_ip) | Private IP of the Application Gateway listener frontend. | +| [appgw\_id](#output\_appgw\_id) | Resource ID of the Application Gateway. | +| [appgw\_name](#output\_appgw\_name) | Application Gateway name. | +| [ncc\_id](#output\_ncc\_id) | Databricks NCC ID. | +| [serverless\_domain\_names](#output\_serverless\_domain\_names) | FQDNs registered in the NCC private endpoint rule. | +| [transit\_vnet\_id](#output\_transit\_vnet\_id) | Transit VNet ID. | + diff --git a/modules/adb-serverless-appgw-tls-transit/appgw.tf b/modules/adb-serverless-appgw-tls-transit/appgw.tf new file mode 100644 index 00000000..f027ca08 --- /dev/null +++ b/modules/adb-serverless-appgw-tls-transit/appgw.tf @@ -0,0 +1,151 @@ +# Application Gateway v2 TCP/TLS proxy listener + native Private Link. +# azurerm does not currently expose the TCP listener properties, so the +# Application Gateway is represented with AzAPI. + +locals { + appgw_base_id = "${azurerm_resource_group.this.id}/providers/Microsoft.Network/applicationGateways/${var.appgw_name}" + listener_name = "listener-tls" + frontend_name = "frontend-private" + frontend_port = "port-${var.listener_port}" + pl_name = "privatelink-config" + backend_port = coalesce(var.backend_port, var.listener_port) + + # Derive the listener address from the subnet so it cannot accidentally be + # configured outside the Application Gateway subnet. + appgw_frontend_private_ip = cidrhost(var.appgw_subnet_prefix, 10) + + backend_addresses = [ + for address in var.backend_addresses : { + ipAddress = address + } + ] + + backend_fqdns = [ + for fqdn in var.backend_fqdns : { + fqdn = fqdn + } + ] +} + +resource "azurerm_public_ip" "appgw" { + name = "pip-${var.appgw_name}" + location = azurerm_resource_group.this.location + resource_group_name = azurerm_resource_group.this.name + allocation_method = "Static" + sku = "Standard" + tags = local.tags +} + +resource "azapi_resource" "appgw" { + type = "Microsoft.Network/applicationGateways@2024-05-01" + name = var.appgw_name + location = azurerm_resource_group.this.location + parent_id = azurerm_resource_group.this.id + tags = local.tags + + body = { + properties = { + sku = { + name = "Standard_v2" + tier = "Standard_v2" + capacity = var.appgw_capacity + } + + gatewayIPConfigurations = [{ + name = "appgw-ip-config" + properties = { subnet = { id = azurerm_subnet.appgw.id } } + }] + + # The public frontend is intentionally unused. Standard_v2 currently + # requires a public IP resource for GatewayManager communication, while + # the listener and the Private Link association use the private frontend. + frontendIPConfigurations = [ + { + name = local.frontend_name + properties = { + privateIPAllocationMethod = "Static" + privateIPAddress = local.appgw_frontend_private_ip + subnet = { id = azurerm_subnet.appgw.id } + privateLinkConfiguration = { id = "${local.appgw_base_id}/privateLinkConfigurations/${local.pl_name}" } + } + }, + { + name = "frontend-public-unused" + properties = { publicIPAddress = { id = azurerm_public_ip.appgw.id } } + } + ] + + frontendPorts = [{ + name = local.frontend_port + properties = { port = var.listener_port } + }] + + backendAddressPools = [{ + name = "backend-pool" + properties = { + backendAddresses = concat(local.backend_addresses, local.backend_fqdns) + } + }] + + probes = [{ + name = "probe-tcp" + properties = { + protocol = "Tcp" + port = local.backend_port + interval = 30 + timeout = 30 + unhealthyThreshold = 3 + } + }] + + backendSettingsCollection = [{ + name = "backend-settings-tcp" + properties = { + port = local.backend_port + protocol = "Tcp" + timeout = 60 + probe = { id = "${local.appgw_base_id}/probes/probe-tcp" } + } + }] + + listeners = [{ + name = local.listener_name + properties = { + frontendIPConfiguration = { id = "${local.appgw_base_id}/frontendIPConfigurations/${local.frontend_name}" } + frontendPort = { id = "${local.appgw_base_id}/frontendPorts/${local.frontend_port}" } + protocol = "Tcp" + } + }] + + routingRules = [{ + name = "rule-tls" + properties = { + ruleType = "Basic" + priority = 100 + listener = { id = "${local.appgw_base_id}/listeners/${local.listener_name}" } + backendAddressPool = { id = "${local.appgw_base_id}/backendAddressPools/backend-pool" } + backendSettings = { id = "${local.appgw_base_id}/backendSettingsCollection/backend-settings-tcp" } + } + }] + + privateLinkConfigurations = [{ + name = local.pl_name + properties = { + ipConfigurations = [{ + name = "pl-ipconfig" + properties = { + privateIPAllocationMethod = "Dynamic" + primary = true + subnet = { id = azurerm_subnet.appgw_pls.id } + } + }] + } + }] + } + } + + depends_on = [ + azurerm_subnet_network_security_group_association.appgw, + azurerm_public_ip.appgw, + ] +} diff --git a/modules/adb-serverless-appgw-tls-transit/checks.tf b/modules/adb-serverless-appgw-tls-transit/checks.tf new file mode 100644 index 00000000..921e1772 --- /dev/null +++ b/modules/adb-serverless-appgw-tls-transit/checks.tf @@ -0,0 +1,6 @@ +check "backend_target" { + assert { + condition = length(var.backend_addresses) + length(var.backend_fqdns) > 0 + error_message = "Provide at least one backend address or backend FQDN." + } +} diff --git a/modules/adb-serverless-appgw-tls-transit/ncc.tf b/modules/adb-serverless-appgw-tls-transit/ncc.tf new file mode 100644 index 00000000..60481e9b --- /dev/null +++ b/modules/adb-serverless-appgw-tls-transit/ncc.tf @@ -0,0 +1,173 @@ +# Databricks NCC and workspace binding. +# +# Application Gateway v2 private endpoint rules require resource_id, group_id, +# and domain_names together. The Databricks Terraform resource does not expose +# that combination, so this module uses the documented account API. + +resource "databricks_mws_network_connectivity_config" "this" { + name = "ncc-${var.appgw_name}" + region = var.azure_region +} + +resource "databricks_mws_ncc_binding" "this" { + network_connectivity_config_id = databricks_mws_network_connectivity_config.this.network_connectivity_config_id + workspace_id = tonumber(var.databricks_workspace_id) +} + +resource "null_resource" "ncc_private_endpoint_rule" { + triggers = { + ncc_id = databricks_mws_network_connectivity_config.this.network_connectivity_config_id + resource_id = azapi_resource.appgw.id + group_id = local.frontend_name + domain_names = jsonencode(var.serverless_domain_names) + account_id = var.databricks_account_id + host = var.databricks_host + } + + provisioner "local-exec" { + interpreter = ["bash", "-c"] + command = <<-EOT + set -euo pipefail + + TOKEN=$(az account get-access-token \ + --resource "2ff814a6-3304-4ab8-85cb-cd0e6f879c1d" \ + --query accessToken -o tsv) + test -n "$TOKEN" + + API="${var.databricks_host}/api/2.0/accounts/${var.databricks_account_id}/network-connectivity-configs/${databricks_mws_network_connectivity_config.this.network_connectivity_config_id}/private-endpoint-rules" + PAYLOAD_B64='${base64encode(jsonencode({ + domain_names = var.serverless_domain_names + resource_id = azapi_resource.appgw.id + group_id = local.frontend_name +}))}' + DOMAIN_PAYLOAD_B64='${base64encode(jsonencode({ domain_names = var.serverless_domain_names }))}' + TARGET_RESOURCE_ID_B64='${base64encode(azapi_resource.appgw.id)}' + TARGET_GROUP_ID_B64='${base64encode(local.frontend_name)}' + PAYLOAD=$(printf '%s' "$PAYLOAD_B64" | python3 -c 'import base64, sys; print(base64.b64decode(sys.stdin.buffer.read()).decode())') + DOMAIN_PAYLOAD=$(printf '%s' "$DOMAIN_PAYLOAD_B64" | python3 -c 'import base64, sys; print(base64.b64decode(sys.stdin.buffer.read()).decode())') + export TARGET_RESOURCE_ID_B64 TARGET_GROUP_ID_B64 + + # Reconcile instead of blindly POSTing: changing the domain list should + # update the existing rule, not create a duplicate rule in the NCC. + RULES=$(curl --silent --show-error --fail \ + --header "Authorization: Bearer $TOKEN" \ + --header "Content-Type: application/json" "$API") + RULE_ID=$(printf '%s' "$RULES" | python3 -c ' +import base64 +import json +import os +import sys + +body = json.load(sys.stdin) +rules = body.get("private_endpoint_rules", body.get("privateEndpointRules", [])) +target_resource_id = base64.b64decode(os.environ["TARGET_RESOURCE_ID_B64"]).decode() +target_group_id = base64.b64decode(os.environ["TARGET_GROUP_ID_B64"]).decode() +for rule in rules: + if (rule.get("resource_id") == target_resource_id and + rule.get("group_id") == target_group_id): + print(rule.get("private_endpoint_rule_id", rule.get("privateEndpoint_rule_id", rule.get("id", "")))) + break +') + + if test -n "$RULE_ID"; then + curl --silent --show-error --fail --request PATCH \ + --header "Authorization: Bearer $TOKEN" \ + --header "Content-Type: application/json" \ + --data "$DOMAIN_PAYLOAD" "$API/$RULE_ID?update_mask=domain_names" + else + curl --silent --show-error --fail --request POST \ + --header "Authorization: Bearer $TOKEN" \ + --header "Content-Type: application/json" \ + --data "$PAYLOAD" "$API" + fi + EOT +} + +provisioner "local-exec" { + when = destroy + interpreter = ["bash", "-c"] + command = <<-EOT + set -euo pipefail + + TOKEN=$(az account get-access-token \ + --resource "2ff814a6-3304-4ab8-85cb-cd0e6f879c1d" \ + --query accessToken -o tsv) + test -n "$TOKEN" + API="${self.triggers.host}/api/2.0/accounts/${self.triggers.account_id}/network-connectivity-configs/${self.triggers.ncc_id}/private-endpoint-rules" + TARGET_RESOURCE_ID_B64='${base64encode(self.triggers.resource_id)}' + TARGET_GROUP_ID_B64='${base64encode(self.triggers.group_id)}' + export TARGET_RESOURCE_ID_B64 TARGET_GROUP_ID_B64 + RULES=$(curl --silent --show-error --fail \ + --header "Authorization: Bearer $TOKEN" "$API") + RULE_ID=$(printf '%s' "$RULES" | python3 -c ' +import base64 +import json +import os +import sys +body = json.load(sys.stdin) +rules = body.get("private_endpoint_rules", body.get("privateEndpointRules", [])) +target_resource_id = base64.b64decode(os.environ["TARGET_RESOURCE_ID_B64"]).decode() +target_group_id = base64.b64decode(os.environ["TARGET_GROUP_ID_B64"]).decode() +for rule in rules: + if (rule.get("resource_id") == target_resource_id and + rule.get("group_id") == target_group_id): + print(rule.get("private_endpoint_rule_id", rule.get("privateEndpoint_rule_id", rule.get("id", "")))) + break +') + if test -n "$RULE_ID"; then + curl --silent --show-error --fail --request DELETE \ + --header "Authorization: Bearer $TOKEN" "$API/$RULE_ID" + fi + EOT +} + +depends_on = [ + databricks_mws_ncc_binding.this, + azapi_resource.appgw, +] +} + +# Give the Databricks-created private endpoint time to appear in Azure before +# optionally attempting approval. +resource "time_sleep" "wait_for_private_endpoint" { + depends_on = [null_resource.ncc_private_endpoint_rule] + create_duration = "90s" +} + +resource "null_resource" "approve_private_endpoint" { + count = var.auto_approve_private_endpoint ? 1 : 0 + + triggers = { + appgw_name = var.appgw_name + rg_name = azurerm_resource_group.this.name + } + + provisioner "local-exec" { + interpreter = ["bash", "-c"] + command = <<-EOT + set -euo pipefail + for attempt in 1 2 3 4 5 6; do + CONNECTION_IDS=$(az network application-gateway show \ + --name "${var.appgw_name}" \ + --resource-group "${azurerm_resource_group.this.name}" \ + --query "privateLinkConfigurations[].privateEndpointConnections[?privateLinkServiceConnectionState.status=='Pending'].id" \ + --output tsv 2>/dev/null || true) + if test -n "$CONNECTION_IDS"; then + while IFS= read -r connection_id; do + test -n "$connection_id" || continue + az network private-endpoint-connection approve \ + --id "$connection_id" \ + --description "Approved for Databricks Serverless NCC" + done <<< "$CONNECTION_IDS" + exit 0 + fi + echo "No pending App Gateway private endpoint yet; retry $attempt/6." + sleep 30 + done + echo "ERROR: no pending connection appeared after six attempts; approve it manually in Azure." + exit 1 + EOT + } + + depends_on = [time_sleep.wait_for_private_endpoint] +} diff --git a/modules/adb-serverless-appgw-tls-transit/network.tf b/modules/adb-serverless-appgw-tls-transit/network.tf new file mode 100644 index 00000000..aa040ec8 --- /dev/null +++ b/modules/adb-serverless-appgw-tls-transit/network.tf @@ -0,0 +1,91 @@ +locals { + tags = merge( + { + ManagedBy = "terraform" + Module = "adb-serverless-appgw-tls-transit" + }, + var.tags, + ) +} + +resource "azurerm_resource_group" "this" { + name = var.rg_name + location = var.azure_region + tags = local.tags +} + +resource "azurerm_virtual_network" "this" { + name = "vnet-${var.appgw_name}" + location = azurerm_resource_group.this.location + resource_group_name = azurerm_resource_group.this.name + address_space = var.vnet_address_space + tags = local.tags +} + +resource "azurerm_subnet" "appgw" { + name = "snet-appgw" + resource_group_name = azurerm_resource_group.this.name + virtual_network_name = azurerm_virtual_network.this.name + address_prefixes = [var.appgw_subnet_prefix] +} + +resource "azurerm_subnet" "appgw_pls" { + name = "snet-appgw-pls" + resource_group_name = azurerm_resource_group.this.name + virtual_network_name = azurerm_virtual_network.this.name + address_prefixes = [var.appgw_pls_subnet_prefix] + + # Required for the Application Gateway Private Link configuration. + private_link_service_network_policies_enabled = false +} + +# Keep the public-facing management surface from accepting Internet traffic. +# GatewayManager and AzureLoadBalancer need the documented health/management +# ports for the Application Gateway service to operate. +resource "azurerm_network_security_group" "appgw" { + name = "nsg-${var.appgw_name}" + location = azurerm_resource_group.this.location + resource_group_name = azurerm_resource_group.this.name + tags = local.tags + + security_rule { + name = "allow-gateway-manager" + priority = 100 + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "65200-65535" + source_address_prefix = "GatewayManager" + destination_address_prefix = "*" + } + + security_rule { + name = "allow-azure-load-balancer" + priority = 110 + direction = "Inbound" + access = "Allow" + protocol = "Tcp" + source_port_range = "*" + destination_port_range = "65503-65534" + source_address_prefix = "AzureLoadBalancer" + destination_address_prefix = "*" + } + + security_rule { + name = "deny-internet" + priority = 4000 + direction = "Inbound" + access = "Deny" + protocol = "*" + source_port_range = "*" + destination_port_range = "*" + source_address_prefix = "Internet" + destination_address_prefix = "*" + } +} + +resource "azurerm_subnet_network_security_group_association" "appgw" { + subnet_id = azurerm_subnet.appgw.id + network_security_group_id = azurerm_network_security_group.appgw.id +} diff --git a/modules/adb-serverless-appgw-tls-transit/outputs.tf b/modules/adb-serverless-appgw-tls-transit/outputs.tf new file mode 100644 index 00000000..7d84dc6c --- /dev/null +++ b/modules/adb-serverless-appgw-tls-transit/outputs.tf @@ -0,0 +1,34 @@ +output "appgw_id" { + description = "Resource ID of the Application Gateway." + value = azapi_resource.appgw.id +} + +output "appgw_name" { + description = "Application Gateway name." + value = azapi_resource.appgw.name +} + +output "appgw_frontend_config_name" { + description = "Private frontend configuration name used as the NCC rule group_id." + value = local.frontend_name +} + +output "appgw_frontend_private_ip" { + description = "Private IP of the Application Gateway listener frontend." + value = local.appgw_frontend_private_ip +} + +output "ncc_id" { + description = "Databricks NCC ID." + value = databricks_mws_network_connectivity_config.this.network_connectivity_config_id +} + +output "transit_vnet_id" { + description = "Transit VNet ID." + value = azurerm_virtual_network.this.id +} + +output "serverless_domain_names" { + description = "FQDNs registered in the NCC private endpoint rule." + value = var.serverless_domain_names +} diff --git a/modules/adb-serverless-appgw-tls-transit/providers.tf b/modules/adb-serverless-appgw-tls-transit/providers.tf new file mode 100644 index 00000000..371f7a46 --- /dev/null +++ b/modules/adb-serverless-appgw-tls-transit/providers.tf @@ -0,0 +1,26 @@ +terraform { + required_version = ">= 1.9.0" + + required_providers { + azurerm = { + source = "hashicorp/azurerm" + version = ">=4.31.0" + } + azapi = { + source = "Azure/azapi" + version = "2.0.1" + } + databricks = { + source = "databricks/databricks" + version = ">=1.81.1" + } + null = { + source = "hashicorp/null" + version = ">=3.2.0" + } + time = { + source = "hashicorp/time" + version = ">=0.9.0" + } + } +} diff --git a/modules/adb-serverless-appgw-tls-transit/variables.tf b/modules/adb-serverless-appgw-tls-transit/variables.tf new file mode 100644 index 00000000..955da29f --- /dev/null +++ b/modules/adb-serverless-appgw-tls-transit/variables.tf @@ -0,0 +1,171 @@ +variable "azure_subscription_id" { + type = string + description = "Azure subscription ID to deploy into." + + validation { + condition = can(regex("^[0-9a-fA-F-]{36}$", var.azure_subscription_id)) + error_message = "azure_subscription_id must be a UUID." + } +} + +variable "azure_region" { + type = string + description = "Azure region short name. Must match the workspace and NCC region." +} + +variable "rg_name" { + type = string + description = "Resource group to create for the transit resources." + default = "rg-appgw-tls-transit" +} + +variable "appgw_name" { + type = string + description = "Application Gateway name." + default = "appgw-serverless-transit" +} + +variable "appgw_capacity" { + type = number + description = "Fixed Standard_v2 Application Gateway instance capacity." + default = 2 + + validation { + condition = var.appgw_capacity >= 1 && var.appgw_capacity <= 10 && floor(var.appgw_capacity) == var.appgw_capacity + error_message = "appgw_capacity must be a whole number between 1 and 10." + } +} + +variable "databricks_host" { + type = string + description = "Databricks account console host." + default = "https://accounts.azuredatabricks.net" + + validation { + condition = can(regex("^https://[a-zA-Z0-9.-]+/?$", var.databricks_host)) + error_message = "databricks_host must be an HTTPS URL with a hostname." + } +} + +variable "databricks_account_id" { + type = string + description = "Databricks account ID (UUID)." + + validation { + condition = can(regex("^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$", var.databricks_account_id)) + error_message = "databricks_account_id must be a UUID." + } +} + +variable "databricks_workspace_id" { + type = string + description = "Databricks workspace ID to bind to the NCC." + + validation { + condition = can(tonumber(var.databricks_workspace_id)) && tonumber(var.databricks_workspace_id) > 0 + error_message = "databricks_workspace_id must be a positive numeric workspace ID." + } +} + +variable "backend_addresses" { + type = list(string) + description = "IPv4 addresses of TLS backends reachable from the transit VNet." + default = [] + + validation { + condition = alltrue([ + for address in var.backend_addresses : can(cidrhost("${address}/32", 0)) + ]) + error_message = "backend_addresses must contain IPv4 addresses." + } +} + +variable "backend_fqdns" { + type = list(string) + description = "FQDNs of TLS backends reachable from the transit VNet." + default = [] + + validation { + condition = alltrue([for fqdn in var.backend_fqdns : length(trimspace(fqdn)) > 0]) + error_message = "backend_fqdns must not contain empty values." + } +} + +variable "serverless_domain_names" { + type = list(string) + description = "FQDNs that serverless clients dial. The NCC rule supports at most 10 names." + + validation { + condition = ( + length(var.serverless_domain_names) > 0 && + length(var.serverless_domain_names) <= 10 && + length(distinct(var.serverless_domain_names)) == length(var.serverless_domain_names) && + alltrue([ + for domain in var.serverless_domain_names : domain == trimspace(domain) && can(regex("^(\\*\\.)?([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\\.)+[a-zA-Z0-9]{2,63}$", domain)) + ]) + ) + error_message = "Provide 1 to 10 unique FQDNs; a leading wildcard such as *.example.com is allowed." + } +} + +variable "listener_port" { + type = number + description = "TCP/TLS port exposed by the Application Gateway." + default = 9092 + + validation { + condition = var.listener_port >= 1 && var.listener_port <= 65535 && floor(var.listener_port) == var.listener_port + error_message = "listener_port must be a whole number between 1 and 65535." + } +} + +variable "backend_port" { + type = number + description = "TCP/TLS port used by the backend. Defaults to listener_port." + default = null + + validation { + condition = var.backend_port == null ? true : (var.backend_port >= 1 && var.backend_port <= 65535 && floor(var.backend_port) == var.backend_port) + error_message = "backend_port must be null or a whole number between 1 and 65535." + } +} + +variable "vnet_address_space" { + type = list(string) + description = "Address space for the transit VNet." + default = ["10.230.0.0/16"] +} + +variable "appgw_subnet_prefix" { + type = string + description = "Address prefix for the Application Gateway subnet. The subnet must provide at least ten usable host addresses." + default = "10.230.1.0/24" + + validation { + condition = can(cidrhost(var.appgw_subnet_prefix, 10)) + error_message = "appgw_subnet_prefix must be a valid IPv4 subnet with at least ten usable host addresses." + } +} + +variable "appgw_pls_subnet_prefix" { + type = string + description = "Address prefix for the dedicated Application Gateway Private Link subnet." + default = "10.230.2.0/24" + + validation { + condition = can(cidrhost(var.appgw_pls_subnet_prefix, 1)) + error_message = "appgw_pls_subnet_prefix must be a valid IPv4 subnet." + } +} + +variable "auto_approve_private_endpoint" { + type = bool + description = "Attempt to approve the Databricks-created private endpoint with Azure CLI." + default = false +} + +variable "tags" { + type = map(string) + description = "Tags applied to created resources." + default = {} +}