AzurePostgreSQLDevOpsTerraform

Migrating PostgreSQL from Azure Single Server to Flexible Server: What Actually Broke

·4 min read

When Microsoft announced the deprecation of Azure Database for PostgreSQL Single Server, we had no choice but to migrate. Our production system at TomTom runs multiple regional PostgreSQL instances with strict data residency requirements — not exactly a simple lift-and-shift.

Here's what actually happened.

The Setup

We were running PostgreSQL 11 on Azure Single Server across three regions. The stack:

  • Backend: Java Spring Boot microservices connecting via JDBC
  • IaC: Terraform managing the database resources, VNet rules, and DNS
  • Networking: Private endpoints with custom DNS zones

The official migration guide makes it sound straightforward. It is not.

What the Docs Don't Tell You

1. SSL enforcement behaves differently

Single Server defaults to SSL enforcement on. Flexible Server defaults to SSL enforcement on too — but the certificate chain changed. Our Spring Boot services were using the old BaltimoreCyberTrustRoot certificate bundled in the JDBC driver.

After migration, connections failed silently in staging. Not a handshake error, not a clear SSL rejection — just connection timeouts. It took an hour of Wireshark captures to trace it back to a cert mismatch.

Fix: Update your JDBC connection string to use DigiCertGlobalRootG2.crt and configure sslmode=require explicitly.

2. Terraform resource types are completely different

Single Server uses azurerm_postgresql_server. Flexible Server uses azurerm_postgresql_flexible_server. These are not compatible resources. You can't just rename them.

This means Terraform wants to destroy the old server and create a new one. The state migration is manual:

terraform state rm azurerm_postgresql_server.main
terraform import azurerm_postgresql_flexible_server.main /subscriptions/.../flexibleServers/...

We also had to re-import all firewall rules, VNet integrations, and database configs as new resource types.

3. Private DNS zones are different

Single Server uses privatelink.postgres.database.azure.com. Flexible Server uses a custom private DNS zone that you define. If you're using the same VNet, you need to either reuse or create a separate zone.

We accidentally ended up with two DNS zones for a period, which caused intermittent connection failures depending on which resolver responded first.

Fix: Explicitly set private_dns_zone_id in your Terraform config and make sure your VNet link is pointing to the correct zone.

4. Extensions need to be re-enabled

Extensions enabled on Single Server are not automatically migrated. We use uuid-ossp and pg_trgm — both had to be re-enabled on the Flexible Server via:

CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS pg_trgm;

Some extensions also require the azure_pg_admin role on Flexible Server which is slightly different from Single Server permissions.

5. Connection pooling defaults changed

Single Server had a max connections limit based on vCores. Flexible Server also ties max connections to compute tier, but the numbers are different. We went from a 4-vCore Single Server (max ~250 connections) to a comparable Flexible Server tier and our HikariCP pool started hitting connection limits under load.

Check your pool sizing against the new compute tier before go-live.

What Went Smoothly

Not everything was painful. The actual data migration via pg_dump / pg_restore was clean. The Azure Database Migration Service also worked well for the initial data sync, which let us keep the old server running while we validated the new one.

The Flexible Server also has better observability — query performance insights are much more detailed than what Single Server offered.

The Terraform Module After

For anyone migrating, here's the rough shape of what the Flexible Server Terraform resource looks like compared to Single Server:

resource "azurerm_postgresql_flexible_server" "main" {
  name                   = var.server_name
  resource_group_name    = var.resource_group
  location               = var.location
  version                = "14"
  administrator_login    = var.admin_user
  administrator_password = var.admin_password
  
  private_dns_zone_id = azurerm_private_dns_zone.postgres.id
  delegated_subnet_id = azurerm_subnet.postgres.id

  storage_mb   = 32768
  sku_name     = "GP_Standard_D4s_v3"
  
  depends_on = [azurerm_private_dns_zone_virtual_network_link.postgres]
}

The key addition is delegated_subnet_id — Flexible Server uses VNet injection instead of private endpoints, which is actually cleaner but requires a dedicated subnet.

Summary

| Issue | Single Server | Flexible Server | |---|---|---| | SSL cert | BaltimoreCyberTrustRoot | DigiCertGlobalRootG2 | | Terraform resource | azurerm_postgresql_server | azurerm_postgresql_flexible_server | | Private DNS zone | privatelink.postgres.database.azure.com | Custom zone | | Networking | Private endpoints | VNet injection (delegated subnet) | | Extensions | Auto-migrated? No | Must re-enable manually |

The migration took us about two weeks from planning to production cutover, most of it spent on the Terraform refactor and networking validation. The actual downtime was under 10 minutes.

If you're doing this, budget more time than you think for the Terraform state migration and DNS validation. Those are the parts that will surprise you.