I am encountering challenges when attempting to create a PostgreSQL flexible server with a private endpoint in Azure using Terraform. Additionally, I have attached a Network Security Group (NSG) to the delegated private subnet associated with the PostgreSQL flexible server. Using location as
Central India and B_Standard_B1ms for db_sku.
Tried with different SKUs and locations as well but the same error.
Below, I have included the Terraform script and the error message I am receiving.
"azurerm_private_dns_zone" "postgres_dns_zone" {
name = "${var.project_name}${var.environment}.postgres.database.azure.com"
resource_group_name = var.resource_group
tags = var.tags
}
resource "azurerm_postgresql_flexible_server" "db" {
name = "${var.project_name}${var.environment}postgres"
resource_group_name = var.resource_group
location = var.location
administrator_login = var.db_admin
administrator_password = random_password.postgres_password.result
sku_name = var.db_sku
zone = var.zone
version = var.db_version
delegated_subnet_id = var.subnet_id
private_dns_zone_id = azurerm_private_dns_zone.postgres_dns_zone.id
storage_mb = var.db_storage
backup_retention_days = 7
geo_redundant_backup_enabled = false
public_network_access_enabled = false
dynamic "high_availability" {
for_each = var.enable_ha ? [1] : []
content {
mode = "ZoneRedundant"
standby_availability_zone = var.ha_zone
}
}
depends_on = [azurerm_private_dns_zone.postgres_dns_zone]
tags = var.tags
}
resource "azurerm_postgresql_flexible_server_configuration" "postgis" {
name = "azure.extensions"
server_id = azurerm_postgresql_flexible_server.db.id
value = "POSTGIS"
}
# Link private DNS zone with the VNet for PostgreSQL
resource "azurerm_private_dns_zone_virtual_network_link" "pg_dns_vnet_link" {
name = "${azurerm_postgresql_flexible_server.db.name}-vnetlink.com"
resource_group_name = var.resource_group
private_dns_zone_name = azurerm_private_dns_zone.postgres_dns_zone.name
virtual_network_id = var.vnet_id
tags = var.tags
depends_on = [ azurerm_postgresql_flexible_server.db ]
}
# Private endpoint for PostgreSQL
resource "azurerm_private_endpoint" "postgres_private_endpoint" {
name = "${azurerm_postgresql_flexible_server.db.name}-private-endpoint"
resource_group_name = var.resource_group
location = var.location
subnet_id = var.subnet_id
tags = var.tags
private_service_connection {
name = "${azurerm_postgresql_flexible_server.db.name}-Connection"
is_manual_connection = false
private_connection_resource_id = azurerm_postgresql_flexible_server.db.id
subresource_names = ["postgresqlServer"]
}
private_dns_zone_group {
name = "${azurerm_postgresql_flexible_server.db.name}-private-dns-zone-group"
private_dns_zone_ids = [
azurerm_private_dns_zone.postgres_dns_zone.id
]
}
depends_on = [ azurerm_postgresql_flexible_server.db, azurerm_private_dns_zone.postgres_dns_zone ]
}
The Vnet code is as below:
############
## VNET ##
############
resource "azurerm_virtual_network" "controltower_vnet" {
name = "${var.project_name}-${var.environment}-vnet"
address_space = var.cidr_range
location = var.location
resource_group_name = var.resource_group
tags = var.tags
lifecycle {
ignore_changes = [tags]
}
}
############
## SUBNET ##
############
resource "azurerm_subnet" "controltower_public" {
count = length(var.public_subnet_cidrs)
name = "${var.project_name}-${var.environment}-public-subnet-${count.index + 1}"
resource_group_name = var.resource_group
virtual_network_name = azurerm_virtual_network.controltower_vnet.name
address_prefixes = [var.public_subnet_cidrs[count.index]]
}
resource "azurerm_subnet" "controltower_private" {
count = length(var.private_subnet_cidrs)
name = "${var.project_name}-${var.environment}-private-subnet-${count.index + 1}"
resource_group_name = var.resource_group
virtual_network_name = azurerm_virtual_network.controltower_vnet.name
address_prefixes = [var.private_subnet_cidrs[count.index]]
default_outbound_access_enabled = false
dynamic "delegation" {
for_each = count.index == 0 ? [1] : []
content {
name = "postgresql-delegation"
service_delegation {
name = "Microsoft.DBforPostgreSQL/flexibleServers"
actions = [
#"Microsoft.Network/networkinterfaces/*",
#"Microsoft.Network/publicIPAddresses/join/action",
#"Microsoft.Network/publicIPAddresses/read",
#"Microsoft.Network/virtualNetworks/read",
"Microsoft.Network/virtualNetworks/subnets/action",
#"Microsoft.Network/virtualNetworks/subnets/join/action",
#"Microsoft.Network/virtualNetworks/subnets/prepareNetworkPolicies/action",
#"Microsoft.Network/virtualNetworks/subnets/unprepareNetworkPolicies/action"
]
}
}
}
service_endpoints = count.index == 0 ? ["Microsoft.Sql" , "Microsoft.Storage"] : []
private_endpoint_network_policies = "Enabled"
}
#########
## NAT ##
#########
resource "azurerm_public_ip" "controltower_nat_ip" {
name = "${var.project_name}-${var.environment}-nat-ip"
location = var.location
resource_group_name = var.resource_group
allocation_method = "Static"
sku = var.publicip_sku
tags = var.tags
lifecycle {
ignore_changes = [tags]
}
}
resource "azurerm_nat_gateway" "controltower_nat" {
name = "${var.project_name}-${var.environment}-ngw"
resource_group_name = var.resource_group
location = var.location
sku_name = var.nat_sku
tags = var.tags
lifecycle {
ignore_changes = [tags]
}
}
resource "azurerm_nat_gateway_public_ip_association" "controltower_nat_association" {
nat_gateway_id = azurerm_nat_gateway.controltower_nat.id
public_ip_address_id = azurerm_public_ip.controltower_nat_ip.id
}
# Associate NAT Gateway with private subnets
resource "azurerm_subnet_nat_gateway_association" "controltower_private_nat_association" {
count = length(var.private_subnet_cidrs)
subnet_id = azurerm_subnet.controltower_private[count.index].id
nat_gateway_id = azurerm_nat_gateway.controltower_nat.id
}
#############
## ROUTING ##
#############
# Route table for private subnets
resource "azurerm_route_table" "controltower_private_rt" {
name = "${var.project_name}-private-rt"
location = var.location
resource_group_name = var.resource_group
tags = var.tags
lifecycle {
ignore_changes = [tags]
}
}
# Route table for public subnets
resource "azurerm_route_table" "controltower_public_rt" {
name = "${var.project_name}-public-rt"
location = var.location
resource_group_name = var.resource_group
tags = var.tags
lifecycle {
ignore_changes = [tags]
}
}
# **Single Route for Private Subnets**: This will apply to all private subnets via the NAT gateway
resource "azurerm_route" "controltower_private_route" {
name = "route-private"
resource_group_name = var.resource_group
route_table_name = azurerm_route_table.controltower_private_rt.name
address_prefix = "0.0.0.0/0" # Route all outbound traffic
next_hop_type = "Internet" # Azure handles NAT gateway routing automatically
}
# Route for public subnets (directly to Internet)
resource "azurerm_route" "controltower_public_route" {
count = length(var.public_subnet_cidrs)
name = "route-public-${count.index + 1}"
resource_group_name = var.resource_group
route_table_name = azurerm_route_table.controltower_public_rt.name
address_prefix = "0.0.0.0/0" # Route all outbound traffic to the Internet
next_hop_type = "Internet"
}
##################
## ASSOCIATIONS ##
##################
# Associate private subnets with private route table
resource "azurerm_subnet_route_table_association" "controltower_private_association" {
count = length(var.private_subnet_cidrs)
subnet_id = azurerm_subnet.controltower_private[count.index].id
route_table_id = azurerm_route_table.controltower_private_rt.id
}
# Associate public subnets with public route table
resource "azurerm_subnet_route_table_association" "controltower_public_association" {
count = length(var.public_subnet_cidrs)
subnet_id = azurerm_subnet.controltower_public[count.index].id
route_table_id = azurerm_route_table.controltower_public_rt.id
}
#########
## NSG ##
#########
resource "azurerm_network_security_group" "controltower_nsg" {
name = "${var.project_name}-nsg"
location = var.location
resource_group_name = var.resource_group
tags = var.tags
lifecycle {
ignore_changes = [tags]
}
}
locals {
port_rules = {
for port in var.ports : port => {
priority = 100 + index(var.ports, port)
name = "allow-${port}"
}
}
}
# Create security rules for each port
resource "azurerm_network_security_rule" "allow_inbound" {
for_each = local.port_rules
name = each.value.name
priority = each.value.priority
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_address_prefixes = var.inbound_ips
source_port_range = "*"
destination_address_prefix = "*"
destination_port_ranges = [each.key]
network_security_group_name = azurerm_network_security_group.controltower_nsg.name
resource_group_name = var.resource_group
}
######################
## NSG ASSOCIATIONS ##
######################
# Associate NSG with public subnets
resource "azurerm_subnet_network_security_group_association" "controltower_public_nsg_association" {
count = length(var.public_subnet_cidrs)
subnet_id = azurerm_subnet.controltower_public[count.index].id
network_security_group_id = azurerm_network_security_group.controltower_nsg.id
}
# Associate NSG with private subnets
resource "azurerm_subnet_network_security_group_association" "controltower_private_nsg_association" {
count = length(var.private_subnet_cidrs)
subnet_id = azurerm_subnet.controltower_private[count.index].id
network_security_group_id = azurerm_network_security_group.controltower_nsg.id
}
But getting the below error:
╷
│ Error: creating Private Endpoint (Subscription: "XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
│ Resource Group Name: "test-dev-rg"
│ Private Endpoint Name: "testdevdb-private-endpoint"): performing CreateOrUpdate: unexpected status 400 (400 Bad Request) with error: PrivateEndpointFeatureNotSupportedOnServer: Call to Microsoft.DBforPostgreSQL/flexibleServers failed. Error message: The given server testdevdb does not support private endpoint feature. Please create a new server that is private endpoint capable. Refer to https://aka.ms/pgflex-pepreview for more details.
│
│ with module.DB.azurerm_private_endpoint.postgres_private_endpoint,
│ on ..modulesdatabasedb.tf line 68, in resource "azurerm_private_endpoint" "postgres_private_endpoint":
│ 68: resource "azurerm_private_endpoint" "postgres_private_endpoint" {
│
2
Create Azure Postgres Server with private endpoint using terraform
While creating a Postgres Server with private endpoint we need to make sure that the SKU and region we select show support the feature requirement.
Before running the deployment, we can make sure the configuration of PostgreSQL should be in confirmed by the commands given below.
az postgres flexible-server list-skus --location <your location> --output table
I tried a demo configuration with the configuration that suitable to achieve this requirement as mentioned below.
Configuration:
resource "azurerm_resource_group" "main" {
name = "vinay-pg-rg"
location = "westeurope"
}
resource "azurerm_virtual_network" "main" {
name = "vksb-pg-vnet"
address_space = ["10.88.0.0/16"]
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
}
resource "azurerm_subnet" "main" {
name = "vksb-pg-main-subnet"
resource_group_name = azurerm_resource_group.main.name
virtual_network_name = azurerm_virtual_network.main.name
address_prefixes = ["10.88.2.0/24"]
private_endpoint_network_policies = "Enabled"
service_endpoints = ["Microsoft.Sql"]
}
resource "azurerm_network_security_group" "main" {
name = "vksb-pg-main-nsg"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
security_rule {
name = "PostgreSQL"
priority = 100
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "5432"
source_address_prefix = "0.0.0.0/0"
destination_address_prefix = "*"
}
security_rule {
name = "HTTPS"
priority = 110
direction = "Inbound"
access = "Allow"
protocol = "Tcp"
source_port_range = "*"
destination_port_range = "443"
source_address_prefix = "0.0.0.0/0"
destination_address_prefix = "*"
}
}
resource "azurerm_subnet_network_security_group_association" "main" {
subnet_id = azurerm_subnet.main.id
network_security_group_id = azurerm_network_security_group.main.id
}
resource "azurerm_private_dns_zone" "postgres_dns_zone" {
name = "privatelink.postgres.database.azure.com"
resource_group_name = azurerm_resource_group.main.name
}
resource "azurerm_postgresql_flexible_server" "main" {
name = "vksb-postgresql"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
version = "16"
sku_name = "B_Standard_B2s"
administrator_login = "sqladmin"
administrator_password = "yourpassword"
storage_mb = 32768
storage_tier = "P4"
public_network_access_enabled = false
}
resource "azurerm_private_dns_zone_virtual_network_link" "main" {
name = "vksb-postgresql-main-vnet-link"
resource_group_name = azurerm_resource_group.main.name
private_dns_zone_name = azurerm_private_dns_zone.postgres_dns_zone.name
virtual_network_id = azurerm_virtual_network.main.id
depends_on = [azurerm_subnet.main, azurerm_virtual_network.main]
}
resource "azurerm_private_endpoint" "main" {
name = "vksb-postgresql-main"
location = azurerm_resource_group.main.location
resource_group_name = azurerm_resource_group.main.name
subnet_id = azurerm_subnet.main.id
private_service_connection {
name = "vksb-postgresql-psc"
private_connection_resource_id = azurerm_postgresql_flexible_server.main.id
subresource_names = ["postgresqlServer"]
is_manual_connection = false
}
private_dns_zone_group {
name = azurerm_postgresql_flexible_server.main.name
private_dns_zone_ids = [azurerm_private_dns_zone.postgres_dns_zone.id]
}
depends_on = [azurerm_postgresql_flexible_server.main, azurerm_subnet.main]
}
resource "azurerm_postgresql_flexible_server_database" "db" {
name = "vksb-pg-main-db"
server_id = azurerm_postgresql_flexible_server.main.id
charset = "UTF8"
collation = "en_US.utf8"
lifecycle {
prevent_destroy = false
}
depends_on = [azurerm_postgresql_flexible_server.main]
}
Deployment:
Refer:
https://azure.microsoft.com/en-us/explore/global-infrastructure/products-by-region/
https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/concepts-networking-private
https://learn.microsoft.com/en-us/azure/postgresql/flexible-server/concepts-networking-private-link
8