How can I create a subnet with delegation when my organization has defined a Deny-Subnet-Without-Nsg?

I would like to do this:

resource "azurerm_subnet" "database" {
  name                                          = "DatabaseSubnet"
  resource_group_name                           = var.resource_group_name
  virtual_network_name                          = azurerm_virtual_network.vnet_hub.name
  # this ranges from 10.0.3.1 through 10.0.3.254
  address_prefixes                              = ["10.0.3.0/24"]
  service_endpoints                             = ["Microsoft.Storage"]
  private_endpoint_network_policies_enabled     = false
  private_link_service_network_policies_enabled = false

  delegation {
    name = "fs"
    service_delegation {
      name    = "Microsoft.DBforPostgreSQL/flexibleServers"
      actions = [
        "Microsoft.Network/virtualNetworks/subnets/join/action",
      ]
    }
  }
}

resource "azurerm_subnet_network_security_group_association" "private_database" {
  subnet_id                 = azurerm_subnet.database.id
  network_security_group_id = var.private_subnet_nsg_id
}

However, I get a RequestDisallowedByPolicy error, more specifically

Subnets should have a Network Security Group

I know I can create subnets directly within an azurerm_virtual_network resource, like this:

resource "azurerm_virtual_network" "vnet_hub" {
  name                = "HubVnet"
  location            = var.location
  resource_group_name = var.resource_group_name
  # this ranges from 10.0.0.1 through 10.0.3.254
  address_space       = ["10.0.0.0/22"]

  tags = var.tags

  subnet {
    name           = "AzureFirewallSubnet"
    address_prefix = "10.0.0.0/24"
  }

  subnet {
    name           = "JumpboxSubnet"
    address_prefix = "10.0.1.0/24"
    security_group = var.public_subnet_nsg_id
  }

  subnet {
    name           = "ResourcesSubnet"
    address_prefix = "10.0.2.0/24"
    security_group = var.private_subnet_nsg_id
  }
}

That includes a network security group straight away. However, I really need the delegation feature for my postgres server, and I have not been able to find a way to make that happen without defining a subnet outside of the azurerm_virtual_network.

It seems like azure doesn’t like the idea to create the subnet without network security group, and then associating that subnet with an actual network security group.

What can I do?

After some try / error attempts, I was able to come up with a way to make it work. However, it’s much more complicated than what I expected for such a simple task. The idea consists of calling the azure cli directly. Instead of my azurerm_subnet.database resource above, define

resource "null_resource" "database_subnet" {
  depends_on = [azurerm_virtual_network.vnet_hub]

  provisioner "local-exec" {
    command     = format("%s/%s", path.module, "add_database_subnet.sh")
    interpreter = ["sh"]
    environment = {
      RESOURCE_GROUP_NAME       = var.resource_group_name
      SUBNET_NAME               = local.database_subnet_name
      VNET_NAME                 = azurerm_virtual_network.vnet_hub.name
      # this ranges from 10.0.3.1 through 10.0.3.254
      ADDRESS_PREFIX            = "10.0.3.0/24"
      NETWORK_SECURITY_GROUP_ID = var.private_subnet_nsg_id
    }
  }
}

data "external" "get_database_subnet_id" {
  program = [
    "sh", format("%s/%s", path.module, "get_database_subnet_id.sh")
  ]

  query = {
    RESOURCE_GROUP_NAME    = var.resource_group_name
    SUBNET_NAME = local.database_subnet_name
    VNET_NAME = azurerm_virtual_network.vnet_hub.name
  }
}

together with the following scripts:

# add_database_subnet.sh
#! /bin/sh

az network vnet subnet create \
  --name "${SUBNET_NAME}" \
  --vnet-name "${VNET_NAME}" \
  --resource-group "${RESOURCE_GROUP_NAME}" \
  --address-prefixes "${ADDRESS_PREFIX}" \
  --network-security-group "${NETWORK_SECURITY_GROUP_ID}" \
  --service-endpoints "Microsoft.Storage" \
  --delegations "Microsoft.DBforPostgreSQL/flexibleServers"

and

#! /bin/sh

eval "$(jq -r '@sh "RESOURCE_GROUP_NAME=\(.RESOURCE_GROUP_NAME) SUBNET_NAME=\(.SUBNET_NAME) VNET_NAME=\(.VNET_NAME)"')"

id=$(az network vnet subnet show --name $SUBNET_NAME --vnet-name $VNET_NAME --resource-group $RESOURCE_GROUP_NAME -o json | jq -r ".id")

# returned json values need to be base64-encoded
encoded_id=$(echo "$id" | base64 -w 0)
jq -n --arg encoded_id "$encoded_id" '{"encoded_id":$encoded_id}'

Because the creation of the actual postgres flexible server needs a delegated subnet id, we need to expose the database subnet id, like this:

output "database_snet_id" {
  value = base64decode(data.external.get_database_subnet_id.result.encoded_id)
}

That’s a little nightmare, I see no point using terraform to do such things. I had to experiment the whole day to make it work, especially the exposition of the subnet id. Terraform is great for simple stuff, but the smallest obstacle seems to make it very painful to use.

Or maybe there is a simpler way?