6. Integrations
No database is an island, and a relational database is the most connected thing in most architectures. This page covers the services Azure SQL Database is almost always paired with, and — more usefully — the glue, because "how do these two talk to each other securely" is where the real work is.
The two glue mechanisms that recur everywhere
Learn these once and most Azure integration questions answer themselves.
1. Managed identity + a contained database user
The keyless way an Azure resource authenticates to the database. Note the shape carefully, because it is a two-sided operation and half of the failures come from doing only one side:
# Azure side: the app has an identity
resource "azurerm_user_assigned_identity" "api" {
name = "id-orders-api-prod"
resource_group_name = azurerm_resource_group.app.name
location = azurerm_resource_group.app.location
}
resource "azurerm_linux_web_app" "api" {
# ...
identity {
type = "UserAssigned"
identity_ids = [azurerm_user_assigned_identity.api.id]
}
app_settings = {
# No password. The driver acquires a token for this identity.
"ConnectionStrings__Orders" = "Server=tcp:${var.sql_fqdn},1433;Database=sqldb-orders-prod;Authentication=Active Directory Managed Identity;User Id=${azurerm_user_assigned_identity.api.client_id};Encrypt=True;"
}
}
-- Database side: the identity is a user with the narrowest useful role.
-- Run as the Entra admin, once per database, per identity.
CREATE USER [id-orders-api-prod] FROM EXTERNAL PROVIDER;
ALTER ROLE db_datareader ADD MEMBER [id-orders-api-prod];
ALTER ROLE db_datawriter ADD MEMBER [id-orders-api-prod];
-- Better still, for anything mature:
-- GRANT EXECUTE ON SCHEMA::app TO [id-orders-api-prod]; -- procedures only, no table access
Why this is not an Azure RBAC role assignment. There is no Azure SQL Data Reader built-in role,
and there never will be one that works this way — data-plane authorisation for SQL lives in SQL,
because the granularity (table, column, row, procedure) is far finer than ARM's model. This is the
control plane vs. data plane split made concrete, and it is the single most
common integration misunderstanding.
A prerequisite people miss: for the server to resolve group memberships and some principal types
when creating external users, its identity may need the Entra Directory Readers role. If
CREATE USER ... FROM EXTERNAL PROVIDER fails with a "Principal ... not found" error and the
principal plainly exists, this is usually why.
2. Private endpoint + Private DNS zone
The way a caller reaches the database without traversing the public internet.
- The private endpoint targets the
sqlServersub-resource (this is the string you pass; getting it wrong is a common Terraform error). - The Private DNS zone is
privatelink.database.windows.net, linked to every VNet that must resolve it. - The magic is in the CNAME chain:
myserver.database.windows.net→myserver.privatelink.database.windows.net→ the private IP. The connection string does not change — which is exactly why this works for applications that can't be modified. - Pair it with
public_network_access_enabled = false, or you have added a private path without removing the public one.
The failure signature to recognise: the app resolves the public IP and times out. Nine times in
ten the VNet isn't linked to the Private DNS zone, or the caller is resolving through a custom DNS
server that doesn't forward to Azure DNS (168.63.129.16). Check nslookup output before checking
anything else.
The integration map
| Pairs with | Why | The glue |
|---|---|---|
| Microsoft Entra ID | Who can administer it, who can read it, without passwords | Entra admin group on the logical server + contained users FROM EXTERNAL PROVIDER; azureADOnlyAuthentication = true to close the password path |
| Azure Key Vault | The customer-managed key for TDE; any legacy connection strings you haven't eliminated yet | Server system-assigned identity granted Key Vault Crypto Service Encryption User on the vault; for app settings, a Key Vault reference resolved via the app's managed identity |
| Azure Private Link | Reach the database without a public endpoint | Private endpoint on the sqlServer sub-resource + privatelink.database.windows.net zone |
| App Service / Functions / Container Apps | The thing that actually queries it | Managed identity + contained user (above) + VNet integration on the caller so it can reach the private endpoint |
| Azure Kubernetes Service | Same, for containerised callers | Workload identity federating a Kubernetes service account to an Entra app/identity, then the same contained user. Do not mount a connection string as a Secret |
| Azure Monitor / Log Analytics | See what it's doing | A diagnostic setting (off by default) routing SQLInsights, Errors, Timeouts, Blocks, Deadlocks, QueryStoreRuntimeStatistics and Basic metrics to a workspace; then KQL and alert rules |
| Azure Data Factory / Synapse pipelines | Load into it, extract out of it | A linked service authenticated by the data factory's managed identity, plus a managed private endpoint in the managed VNet so the runtime reaches a private database. A self-hosted integration runtime is the on-premises answer |
| Azure Databricks / Fabric | Analytics over operational data | JDBC via the Spark connector with Entra token auth; realistically, though, read from a replica or a copy — don't run analytical scans against the OLTP primary |
| Azure Logic Apps / Power Apps / Power BI | Low-code consumers | The SQL connector, authenticated by managed identity where supported; Power BI via DirectQuery (hits the primary — point it at a read replica) or Import (doesn't) |
| Azure Cache for Redis | Take read pressure off the database | Application-side cache-aside. The database integration is "there isn't one" — and that's the point; the app owns invalidation |
| Azure API Management | Never expose the database; expose an API | APIM in front of the service that owns the data. Listed here because "can APIM query SQL directly?" is asked often enough to answer: it can, and you shouldn't |
| Azure Backup / long-term retention | Compliance retention beyond the PITR window | LTR policy on the database itself — not Azure Backup, which does not manage Azure SQL Database. Knowing which service owns backup here is a common gap |
| Azure Elastic Jobs | The SQL Agent replacement | An Elastic Job agent (itself backed by a small database) running T-SQL on a schedule across one or many target databases. This is the answer to "where did SQL Agent go" |
[Image Prompt: 2D minimalistic hub-and-spoke diagram with an Azure SQL Database at the centre connected by labelled edges to Microsoft Entra ID, Azure Key Vault, Private Link, App Service, Azure Functions, Azure Kubernetes Service, Azure Data Factory, Azure Monitor, Azure Cache for Redis, and Power BI, flat design, clean vector art style, white background]
Three integration patterns worth knowing by name
Read replica routing
Business Critical gives you a readable secondary at no extra cost, and Hyperscale gives you named replicas. Routing to them is a connection-string change, not an architecture change:
Server=tcp:sql-orders-prod.database.windows.net,1433;Database=sqldb-orders-prod;
Authentication=Active Directory Managed Identity;ApplicationIntent=ReadOnly;Encrypt=True;
Point reporting queries, exports, and Power BI DirectQuery at that connection string. Teams pay for Business Critical and then send every query to the primary, which is buying a second server and leaving it switched off.
The catch: the secondary is asynchronously applied for read purposes, so a read-your-own-write immediately after a commit may not see it. Fine for a dashboard, wrong for a checkout confirmation.
Change feed out of the database
When something downstream needs to react to a row changing, the options in ascending order of robustness:
- Change Tracking — lightweight, tells you which rows changed since a version. Enough for a sync job. Built in.
- Change Data Capture (CDC) — the full before/after, read from change tables. Heavier, and worth checking tier support ⚠️ verify current CDC availability by tier against current Azure docs.
- Outbox pattern — the application writes a domain event to an outbox table in the same transaction as the business change, and a separate process publishes it to Service Bus or Event Grid. More code, but it is the only option that gives you transactional consistency between "the row changed" and "the event was emitted". For anything that matters, this is the answer.
Note what isn't on the list: there is no native "Azure SQL → Event Grid" trigger comparable to Blob Storage's. Don't design as though there is.
Failover group as the application's endpoint
If you deploy a failover group, applications should connect to the listener name
(<fog-name>.database.windows.net), not the server name. The listener follows the primary, so a
regional failover requires zero configuration change on the caller. Connecting to the server name
directly is the mistake that turns a two-minute failover into a two-hour redeployment.
Read-only workloads can use the group's read-only listener, which follows the secondary.
What you should be able to do now
Wire an application to this database with no password anywhere, over a private network path, with
telemetry flowing — and explain why granting the app Contributor in Azure RBAC would not have
helped it read a single row.
Next: Production →
← Back to the Azure SQL Database overview · ← Previous: Deployment