I've worked a lot with Microsoft SQL Server and Azure SQL on Azure over the years. In most projects, setting up the infrastructure with Terraform is pretty straightforward: you define an azurerm_mssql_server, add an azurerm_mssql_database, configure firewall rules, and you're good to go.

The real pain starts right after that: how do you actually manage what lives inside the database?

Managing database objects with existing Terraform providers was honestly frustrating. Most of the community providers either had very limited features—especially for modern Azure capabilities like Azure AD / Entra ID, Managed Identities, and Workload Identity—or they had lots of bugs, ranging from authentication token issues to broken state drift detection and crashes during terraform plan.

Because of those bugs and missing features, many engineers gave up and resorted to ugly workarounds like running sqlcmd inside null_resource bash scripts.

That is why I wrote my own Terraform provider: muecahit94/mssql.

1. Why I Built muecahit94/mssql

In a modern cloud-native setup, you don't want to create old-school SQL logins with static passwords and pass them around as Kubernetes secrets. If you have an application running on Azure Kubernetes Service (AKS) that needs a database, the clean approach is Azure Workload Identity:

The application pod simply uses the ServiceAccount, exchanges its Kubernetes token for an Azure Entra ID token, and logs into SQL Server—without any stored password, connection string secret, or rotation pipeline.

To make this work seamlessly in Terraform without buggy workarounds, I built muecahit94/mssql in Go using Microsoft's official go-mssqldb driver and the Azure SDK. It's published directly on the official Terraform Registry and provides:

2. How the Zero-Secret Authentication Flow Works

To understand why this architecture is so clean, let's look at what actually happens under the hood when your containerized app on AKS talks to Azure SQL:

1
Token Projection in AKS

Your application Pod runs using a Kubernetes ServiceAccount annotated with the Azure Client ID and Tenant ID (azure.workload.identity/client-id and azure.workload.identity/tenant-id). AKS automatically injects a cryptographically signed, short-lived OIDC token directly into the pod volume.

2
Federated Identity Exchange

When the app initiates a database connection, the Azure SDK inside your app presents that projected Kubernetes OIDC token to Azure Entra ID (login.microsoftonline.com).

3
OIDC Trust Validation

Entra ID verifies the token against the Federated Identity Credential linked to the AKS OIDC Issuer URL. Once verified, Entra ID issues a temporary Azure SQL OAuth2 access token for the User-Assigned Managed Identity.

4
Database Login & Role Check

The application connects to Azure SQL on port 1433 using this access token. Azure SQL verifies the token, matches the identity's Object ID (SID) provisioned by muecahit94/mssql, and grants access without any password.

[ STEP 1: AKS CLUSTER ]
  Application Pod (Container)
     │ 
     ├── Attached to: ServiceAccount (azure.workload.identity/client-id & tenant-id)
     └── Injected Volume: /var/run/secrets/azure/tokens/azure-identity-token (Signed OIDC Token)
     │
     ▼
[ STEP 2 & 3: AZURE ENTRA ID ]
  Azure SDK requests SQL Token using the projected OIDC JWT
     │
     ├── Entra ID validates: OIDC Issuer URL == AKS Cluster OIDC Issuer
     ├── Entra ID validates: Subject == system:serviceaccount:<namespace>:<serviceaccount-name>
     │
     └── Issued: OAuth2 Access Token for Azure SQL Database (Audience: https://database.windows.net/)
     │
     ▼
[ STEP 4: AZURE SQL DATABASE (Provisioned via Terraform) ]
  Application connects over TDS (Port 1433) using Access Token
     │
     ├── Azure SQL maps Token Object ID ──▶ mssql_azuread_user [uai-order-service-prod]
     └── Enforces Assigned Roles: [db_datareader, db_datawriter]
     │
  ✅ Connection established securely with ZERO secrets stored in Kubernetes or Git!

3. Step-by-Step Terraform Implementation

Let's look at the complete, working Terraform configuration to build this setup.

Step 1: Configure the Providers

We declare azurerm, kubernetes, and muecahit94/mssql. The MSSQL provider uses azure_auth {} to log in with your current Azure credentials (CLI, Service Principal, or Managed Identity):

versions.tf
terraform {
  required_version = ">= 1.5.0"
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.100"
    }
    mssql = {
      source  = "muecahit94/mssql"
      version = "~> 1.0"
    }
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = "~> 2.30"
    }
  }
}

provider "azurerm" {
  features {}
}

# The MSSQL provider connects to Azure SQL using Azure authentication
provider "mssql" {
  hostname = azurerm_mssql_server.sql.fully_qualified_domain_name
  port     = 1433
  azure_auth {}
}

Step 2: Create the Azure Managed Identity & Federated Credential

Next, create the User-Assigned Managed Identity for the application and link it to the AKS OIDC issuer URL. This tells Azure Entra ID to trust tokens issued by AKS for our specific ServiceAccount:

identity.tf
# 1. Managed Identity for the service
resource "azurerm_user_assigned_identity" "order_service" {
  name                = "uai-order-service-prod"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
}

# 2. Federated Identity Credential linking AKS OIDC to the Managed Identity
resource "azurerm_federated_identity_credential" "order_service_fic" {
  name                = "fic-order-service"
  resource_group_name = azurerm_resource_group.rg.name
  audience            = ["api://AzureADTokenExchange"]
  issuer              = azurerm_kubernetes_cluster.aks.oidc_issuer_url
  parent_id           = azurerm_user_assigned_identity.order_service.id
  subject             = "system:serviceaccount:production:order-service-sa"
}

Step 3: Create the ServiceAccount in Kubernetes

In the Kubernetes namespace where your pods will run, create the ServiceAccount annotated with both the Client ID and Tenant ID of the Managed Identity. The AKS Workload Identity mutating webhook reads these annotations to inject the projected token and Azure environment variables into your pods:

kubernetes.tf
# Fetch current Azure tenant and subscription context
data "azurerm_client_config" "current" {}

resource "kubernetes_namespace" "prod" {
  metadata {
    name = "production"
  }
}

resource "kubernetes_service_account" "order_service" {
  metadata {
    name      = "order-service-sa"
    namespace = kubernetes_namespace.prod.metadata[0].name
    annotations = {
      "azure.workload.identity/client-id" = azurerm_user_assigned_identity.order_service.client_id
      "azure.workload.identity/tenant-id" = data.azurerm_client_config.current.tenant_id
    }
    labels = {
      "azure.workload.identity/use" = "true"
    }
  }
}

Step 4: Create the Database User with muecahit94/mssql

Now comes the core step. Using the mssql_azuread_user resource, we map the Managed Identity directly into the database using its principal_id (Object ID) and assign its roles in one clean block:

database_users.tf
# Create the Azure SQL Database User mapped to the Managed Identity
resource "mssql_azuread_user" "order_service" {
  database_name  = azurerm_mssql_database.app_db.name
  name           = azurerm_user_assigned_identity.order_service.name
  object_id      = azurerm_user_assigned_identity.order_service.principal_id
  default_schema = "dbo"

  # Assign standard database roles
  roles = [
    "db_datareader",
    "db_datawriter"
  ]
}
Why this workflow is so clean:

• Predictable State: terraform plan shows you exact diffs. If someone changes a role manually, Terraform detects and fixes the drift.
• No Stored Passwords: You don't have to generate, store, or rotate database credentials anywhere in Key Vault or Kubernetes secrets.
• Clean Teardown: Running terraform destroy properly cleans up database principals and role bindings.

4. Managing Schemas and Granular Permissions

If your application needs custom schemas or more fine-grained permissions (such as granting SELECT and INSERT on a specific schema rather than whole-database access), the provider handles that declaratively as well:

granular_permissions.tf
# 1. Create a dedicated schema
resource "mssql_schema" "orders" {
  database_name = azurerm_mssql_database.app_db.name
  name          = "orders"
  owner_name    = "dbo"
}

# 2. Create a custom database role
resource "mssql_database_role" "order_processor" {
  database_name = azurerm_mssql_database.app_db.name
  name          = "role_order_processor"
}

# 3. Grant SELECT and INSERT on the 'orders' schema to the role
resource "mssql_schema_permission" "orders_select" {
  database_name  = azurerm_mssql_database.app_db.name
  schema_name    = mssql_schema.orders.name
  principal_name = mssql_database_role.order_processor.name
  permission     = "SELECT"
}

resource "mssql_schema_permission" "orders_insert" {
  database_name  = azurerm_mssql_database.app_db.name
  schema_name    = mssql_schema.orders.name
  principal_name = mssql_database_role.order_processor.name
  permission     = "INSERT"
}

# 4. Add the Managed Identity user to the custom role
resource "mssql_database_role_member" "order_service_membership" {
  database_name = azurerm_mssql_database.app_db.name
  role_name     = mssql_database_role.order_processor.name
  member_name   = mssql_azuread_user.order_service.name
}

5. Summary & Getting Started

Key Takeaways

  • Secretless Databases on Azure: Using AKS Workload Identity with Azure SQL means zero passwords in Git, state files, or Kubernetes secrets.
  • Fully Declarative Lifecycle: Managing database users, roles, and schemas with a native provider gives you full drift detection and clean automation.
  • Simple Integration: You can drop muecahit94/mssql right into your existing Terraform modules without changing your pipeline tooling.

The provider is open source and available directly on the Terraform Registry:

View on Terraform Registry Star on GitHub