Skip to content

Latest commit

 

History

1 Commit

Folders and files

Repository files navigation

☁️ Azure SQL Managed Instance Entra Administrator Terraform Module

Names the Entra ID administrator of an Azure SQL Managed Instance and decides whether SQL authentication survives alongside it — targeting hashicorp/azurerm ~> 4.0.

Terraform azurerm Module Type Resources Caveat


🧩 Overview

  • 🔐 Names the Entra ID (Azure AD) administrator of a SQL Managed Instance — a user, group or service principal.
  • 🚪 Decides whether SQL authentication survives alongside it.
  • 🧬 One resource, two ARM records. The administrator and the Entra-only setting are separate sub-resources; both IDs are emitted.
  • 🔀 azuread_authentication_only defaults to true here and to false in the provider — and the provider's zero value actively turns the setting off.
  • ⏳ Its delete timeout is three hours, six times the create.

💡 Why it matters: the default is not a no-op either way. A bare resource block sends azuread_authentication_only = false unconditionally, so it disables Entra-only authentication on an instance that may have had it on. This module sends true instead — which is the secure value and a disruptive one, because it stops every SQL login on the instance working. Both directions deserve a decision, so this README states the cost of its own default as plainly as the risk of the alternative.


❤️ Support this project

If this module saved you time:


🗺️ Where this fits in the family

flowchart TB
    MI["terraform-azurerm-mssql-managed-instance"]
    THIS["terraform-azurerm-mssql-managed-instance-active-directory-administrator"]
    TDE["terraform-azurerm-mssql-managed-instance-transparent-data-encryption"]
    SAP["terraform-azurerm-mssql-managed-instance-security-alert-policy"]
    KV["terraform-azurerm-key-vault"]
    SRV["terraform-azurerm-mssql-server"]

    MI -->|"id"| THIS
    MI -->|"id"| TDE
    MI -->|"name and resource_group_name"| SAP
    KV -->|"key_ids or key_versionless_ids"| TDE
    MI -->|"identity_principal_id wraps the key"| KV
    THIS -.->|"Entra-only auth is a separate ARM record"| MI
    SRV -.->|"the logical server sets its admin inline"| THIS

    style THIS fill:#0078D4,stroke:#004578,color:#ffffff
    style MI fill:#004578,stroke:#004578,color:#ffffff
    style TDE fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style SAP fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style KV fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style SRV fill:#F3F2F1,stroke:#8A8886,color:#201F1E
Loading

The managed instance feeds its id here and to the encryption-protector module; the security alert policy takes the parent as a name instead, which is why that node's edge is labelled differently. The dotted edge back to the instance is the one to read carefully: Entra-only authentication is a separate ARM record under the instance, written by this same resource. The other dotted edge is a reminder that a logical SQL server sets its administrator inline on the server resource — there is no separate module for it.


🧬 What this module builds

flowchart TB
    MID["managed_instance_id (required, force-new)"]
    IDENT["login_username plus object_id plus tenant_id"]
    AAD["azuread_authentication_only (module TRUE, provider false)"]

    ADM["azurerm_mssql_managed_instance_active_directory_administrator.this"]

    OID["id"]
    OAAD["azuread_only_authentication_id"]
    OSQL["sql_authentication_still_permitted"]
    ODES["destroying_this_resource_re_enables_sql_authentication"]

    MID --> ADM
    IDENT --> ADM
    AAD --> ADM
    ADM --> OID
    ADM --> OAAD
    ADM --> OSQL
    ADM --> ODES

    style ADM fill:#0078D4,stroke:#004578,color:#ffffff
    style MID fill:#004578,stroke:#004578,color:#ffffff
    style IDENT fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style AAD fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style OID fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style OAAD fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style OSQL fill:#F3F2F1,stroke:#8A8886,color:#201F1E
    style ODES fill:#F3F2F1,stroke:#8A8886,color:#201F1E
Loading
Resource Count Notes
azurerm_mssql_managed_instance_active_directory_administrator.this 1 One per instance; writes two ARM records — /administrators/ActiveDirectory and /azureADOnlyAuthentications/Default

✅ Provider / Versions

Requirement Value
Terraform >= 1.12.0
hashicorp/azurerm ~> 4.0
Provider block None in this module — the caller configures the provider, its auth, and the mandatory features {} block

Schema notes that bite:

  • 🔴 One resource, two ARM APIs. The administrator record and the Entra-only authentication setting are separate sub-resources, written by two different clients in the same create and read back from two different APIs.
  • 🔴 azuread_authentication_only has NO schema default and is sent unconditionally, so its zero value of false means a bare resource block turns Entra-only authentication off rather than leaving it alone. This module defaults it to true.
  • 🔴 terraform destroy re-enables SQL authentication. The service refuses to remove an Entra administrator while Entra-only authentication is on, so the provider disables it first as a required step.
  • 🔴 object_id is not force-new. Handing administration of the instance to a different principal is an ordinary in-place attribute change, rendered no more prominently in a plan than a display-name correction.
  • ⚠️ The delete timeout is 180 minutes, against 30 for create and update and 5 for read. That is the provider's own value, and it is a signal about how long the service takes.
  • ⚠️ login_username carries only a non-empty check and is never reconciled with object_id. A display name left stale after a group rename is accepted and simply misleads the next reader.
  • ℹ️ There IS a requires-import guard on create — unlike the managed instance's security alert policy and vulnerability assessment, which silently overwrite. A clean apply here is evidence that no administrator existed.
  • ℹ️ tenant_id is required here, while the logical server's azuread_administrator block defaults it to the provider's tenant. Same idea, two conventions.
  • ℹ️ No tags, no location. Tag the managed instance instead.

🔑 Required Azure RBAC Roles / Permissions

Permission Scope Why
Microsoft.Sql/managedInstances/administrators/read and /write the managed instance create, update and remove the administrator
Microsoft.Sql/managedInstances/azureADOnlyAuthentications/read and /write the managed instance the second ARM record this resource writes
SQL Security Manager the managed instance the role Microsoft documents for enabling and disabling Entra-only authentication, and the least-privilege choice that covers the second ARM record above. Broad roles carrying Microsoft.Sql/*/write (Owner, Contributor) also suffice and grant far more
SQL Managed Instance Contributor / SQL Server Contributor the managed instance NOT sufficient. Microsoft documents that these can check Entra-only authentication but cannot change it — the administrator write succeeds and the Entra-only write is refused, leaving the half-finished apply described in a_failed_entra_only_write_leaves_the_administrator_in_state

🔴 Whoever can write this resource can hand instance administration to any principal, in place, with no replacement shown in the plan. That is a higher-privilege operation than the resource's size suggests.

⚠️ No directory permission is needed to name an administrator. Azure accepts an object ID without the Terraform principal being able to read it in Entra, so a typo in object_id is caught by nothing — it produces an administrator nobody can use.

🔒 No secret passes through this module. An administrator is identified here, never authenticated.


Azure Prerequisites

  • The Microsoft.Sql resource provider registered on the subscription.
  • An existing Azure SQL Managed Instance.
  • An existing Entra ID user, group or service principal, and its object ID and tenant ID.
  • 🔴 A cutover plan for SQL logins, if azuread_authentication_only stays at this module's default of true — they stop working at apply.
  • The caller configures provider "azurerm" { features {} }, auth and subscription.

📁 Module Structure

terraform-azurerm-mssql-managed-instance-active-directory-administrator/
├── providers.tf    # required_version + pinned azurerm; no provider block
├── variables.tf    # 6 inputs, deeply typed, with the identity and Entra-only rules
├── main.tf         # the single keystone `this`
├── outputs.tf      # id first, then both ARM record IDs, then the posture facts
├── README.md       # this file
├── SCOPE.md        # the cross-module contract
├── LICENSE         # MIT
└── .gitignore

⚙️ Quick Start

provider "azurerm" {
  features {}
}

module "mi_entra_admin" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance-active-directory-administrator.git?ref=v1.0.0"

  managed_instance_id = module.sqlmi.id
  login_username      = "sql-mi-admins"
  object_id           = var.sql_admin_group_object_id
  tenant_id           = var.tenant_id
}

Read example 2 before applying this. The default disables SQL authentication for every login on the instance.


🔌 Cross-Module Contract

Consumes

Input Type Source
managed_instance_id Resource ID terraform-azurerm-mssql-managed-instance → id
login_username display name the caller
object_id GUID the caller
tenant_id GUID the caller

Emits

Output Description
id Resource ID of the administrator record (first)
azuread_only_authentication_id the second ARM record this module writes
sql_authentication_still_permitted true when the flag was set to false
object_id / login_username / tenant_id who administers the instance
destroying_this_resource_re_enables_sql_authentication constant true

📚 Example Library

1 · The secure call, with a group
module "mi_entra_admin" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance-active-directory-administrator.git?ref=v1.0.0"

  managed_instance_id = module.sqlmi.id
  login_username      = "sql-mi-admins"
  object_id           = var.sql_admin_group_object_id
  tenant_id           = var.tenant_id
}

🔒 azuread_authentication_only defaults to true. Entra ID becomes the only way in.

💡 Naming a GROUP rather than a person is the difference between rotating an administrator and rebuilding a configuration. Membership changes without a Terraform apply, and it survives someone leaving. This module cannot tell which kind of principal an object ID names, so it recommends rather than enforces.

2 · What the default actually does to SQL logins
module "mi_entra_admin" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance-active-directory-administrator.git?ref=v1.0.0"

  managed_instance_id = module.sqlmi.id
  login_username      = "sql-mi-admins"
  object_id           = var.sql_admin_group_object_id
  tenant_id           = var.tenant_id

  # The default, stated explicitly so the plan reads as a decision.
  azuread_authentication_only = true
}

🔴 Every SQL-authenticated login on the instance stops working at apply — including the built-in administrator login created with the instance. Applications connecting with a SQL username and password fail immediately.

⚠️ It is reversible by setting the flag back to false, but it is not a quiet change. Schedule it rather than discovering it.

3 · Keeping SQL authentication during a migration
module "mi_entra_admin" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance-active-directory-administrator.git?ref=v1.0.0"

  managed_instance_id = module.sqlmi.id
  login_username      = "sql-mi-admins"
  object_id           = var.sql_admin_group_object_id
  tenant_id           = var.tenant_id

  azuread_authentication_only = false
}

output "sql_logins_still_work" {
  value = module.mi_entra_admin.sql_authentication_still_permitted # true
}

⚠️ This is the opt-out, and the module makes you type it. It is the right setting while applications are still migrating off SQL logins — and the output exists so "which instances are still like this?" is one expression rather than an audit.

4 · The second ARM record
output "the_two_records_this_writes" {
  value = {
    administrator = module.mi_entra_admin.id
    entra_only    = module.mi_entra_admin.azuread_only_authentication_id
  }
}

🔴 This one Terraform resource writes two ARM records. The administrator lives under /administrators/ActiveDirectory; Entra-only authentication is a separate sub-resource under /azureADOnlyAuthentications/Default, written by a second client in the same create and read back from a different API.

💡 Both IDs are emitted because a policy exemption, an access review or an incident note needs to name the record that actually changed — and only one of them is this resource's id.

5 · What a destroy quietly does
# Removing this module block does more than remove an administrator.

🔴 terraform destroy re-enables SQL authentication. The service refuses to remove an Entra administrator while Entra-only authentication is on, so the provider disables that setting first, as a required step, and then removes the administrator.

⚠️ Nothing in the plan says so. If SQL authentication must stay off, revoke the SQL logins themselves rather than relying on this resource's continued existence.

6 · Handing administration to a different principal
module "mi_entra_admin" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance-active-directory-administrator.git?ref=v1.0.0"

  managed_instance_id = module.sqlmi.id
  login_username      = "sql-mi-admins-v2"
  object_id           = var.new_admin_group_object_id # <-- in-place change
  tenant_id           = var.tenant_id
}

🔴 Only managed_instance_id is force-new. Changing object_id hands administration of the instance to a different Entra principal as an ordinary in-place attribute change — rendered in a plan no more prominently than the display-name correction beside it.

⚠️ Review changes to this argument the way you would review a role assignment, not the way you would review a tag.

7 · The display name is only a label
module "mi_entra_admin" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance-active-directory-administrator.git?ref=v1.0.0"

  managed_instance_id = module.sqlmi.id
  login_username      = "SQL MI Admins" # a label; spaces are fine
  object_id           = var.sql_admin_group_object_id
  tenant_id           = var.tenant_id
}

ℹ️ object_id grants access; login_username is what the portal shows. Azure does not reconcile them, so a name left stale after a group rename is accepted and simply misleads. Neither the provider nor this module can detect that.

⚠️ This module refuses only the probable mistake — a GUID or a Resource ID pasted where a display name belongs. It imposes no other shape, because a real display name has none.

8 · A service principal for automation
module "mi_entra_admin" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance-active-directory-administrator.git?ref=v1.0.0"

  managed_instance_id = module.sqlmi.id
  login_username      = "sp-sqlmi-automation"
  object_id           = var.automation_sp_object_id
  tenant_id           = var.tenant_id
}

💡 A service principal works, and with azuread_authentication_only = true it connects with a token rather than a password — which is the point: Entra-only authentication removes the credential rather than protecting it.

⚠️ Only one administrator exists per instance, so this replaces a group administrator rather than joining it. Nesting the service principal inside an admin group is usually better.

9 · An explicit tenant, because it is required here
data "azurerm_client_config" "current" {}

module "mi_entra_admin" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance-active-directory-administrator.git?ref=v1.0.0"

  managed_instance_id = module.sqlmi.id
  login_username      = "sql-mi-admins"
  object_id           = var.sql_admin_group_object_id
  tenant_id           = data.azurerm_client_config.current.tenant_id
}

ℹ️ tenant_id is required on this resource, unlike the logical server's azuread_administrator block, which falls back to the provider's tenant. Reading it from the provider's own configuration is the closest equivalent.

10 · A three-hour delete deadline
module "mi_entra_admin" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance-active-directory-administrator.git?ref=v1.0.0"

  managed_instance_id = module.sqlmi.id
  login_username      = "sql-mi-admins"
  object_id           = var.sql_admin_group_object_id
  tenant_id           = var.tenant_id

  timeouts = {
    create = "45m"
    delete = "180m" # the provider's own default -- do not shorten without a reason
  }
}

⚠️ The provider defaults delete to 180 minutes against 30 for create. That six-fold gap is a signal about how long the service can take to disable Entra-only authentication and then remove the administrator.

ℹ️ All four deadlines are honoured on this resource — which is not true of every neighbour in this family.

11 · Several instances from one map
module "mi_entra_admin" {
  source   = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance-active-directory-administrator.git?ref=v1.0.0"
  for_each = module.managed_instances

  managed_instance_id = each.value.id
  login_username      = "sql-mi-admins"
  object_id           = var.sql_admin_group_object_id
  tenant_id           = var.tenant_id
}

output "instances_where_sql_logins_still_work" {
  value = [for k, m in module.mi_entra_admin : k if m.sql_authentication_still_permitted]
}

💡 Keying by a stable name means adding or removing an instance never re-indexes the rest, and the output turns "which instances still allow SQL authentication?" into one expression.

12 · Adopting an instance that already has an administrator
module "mi_entra_admin" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance-active-directory-administrator.git?ref=v1.0.0"

  managed_instance_id = module.sqlmi.id
  login_username      = "sql-mi-admins"
  object_id           = var.sql_admin_group_object_id
  tenant_id           = var.tenant_id
}

✅ This apply will FAIL rather than overwrite. The provider's create checks for an existing administrator and returns a requires-import error, so an instance already configured in the portal is not silently replaced.

💡 That is worth calling out because two of this module's closest neighbours — the managed instance's security alert policy and vulnerability assessment — have no such guard and do overwrite. Run terraform import to adopt the existing record.

13 · 🏗️ End-to-end composition
provider "azurerm" {
  features {}
}

data "azurerm_client_config" "current" {}

module "sqlmi" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance.git?ref=v1.0.0"

  name                = "sqlmi-platform-eus2"
  resource_group_name = "rg-data-platform"
  location            = "eastus2"
  sku_name            = "GP_Gen5"
  vcores              = 8
  storage_size_in_gb  = 512
  subnet_id           = var.managed_instance_subnet_id
  license_type        = "BasePrice"
  # Both are required through this module: the provider pairs each with an
  # inline Entra administrator block that this module does not expose.
  administrator_login          = "sqladmin"
  administrator_login_password = var.mi_admin_password # out of band; never committed
}

module "mi_entra_admin" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance-active-directory-administrator.git?ref=v1.0.0"

  managed_instance_id = module.sqlmi.id
  login_username      = "sql-mi-admins"
  object_id           = var.sql_admin_group_object_id
  tenant_id           = data.azurerm_client_config.current.tenant_id

  azuread_authentication_only = true
}

# The other identity/encryption control on the same instance.
module "mi_tde" {
  source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance-transparent-data-encryption.git?ref=v1.0.0"

  managed_instance_id = module.sqlmi.id
  # No key -- service-managed encryption, which is already secure.
}

output "identity_posture" {
  value = {
    administrator     = module.mi_entra_admin.object_id
    entra_only_record = module.mi_entra_admin.azuread_only_authentication_id
    sql_logins_work   = module.mi_entra_admin.sql_authentication_still_permitted
    encryption_key    = module.mi_tde.key_kind
  }
}

🔒 No credential anywhere in this composition. Entra-only authentication removes the SQL password rather than protecting it, and the encryption protector references a Microsoft-managed key.

🔴 Applying this stops SQL logins on sqlmi-platform-eus2 working. That is the intended posture; it is not a quiet one.


📥 Inputs

Identity of the instance — managed_instance_id (required, force-new). Identity of the administrator — login_username, object_id, tenant_id (all required, none force-new). Authentication — azuread_authentication_only. Tail — timeouts. (There is no tags: the resource exposes none.)

Full schemas
Name Type Default Notes
managed_instance_id string — Required, force-new. Anchored; a logical server ID is refused.
login_username string — Required. A label, not an identity. A GUID or Resource ID is refused.
object_id string — Required. GUID. Not force-new — changing it hands over administration.
tenant_id string — Required. GUID. No fallback to the provider's tenant.
azuread_authentication_only bool true Diverges from the provider's false. Disables all SQL logins.
timeouts object({create, read, update, delete}) null All four real; delete defaults to 180m.

🧾 Outputs

Output Description
id Resource ID of the administrator record (first)
azuread_only_authentication_id the second ARM record this module writes
managed_instance_id the parent, as configured
managed_instance_name / resource_group_name / subscription_id parsed from the parent ID
login_username / object_id / tenant_id who administers the instance
azuread_authentication_only whether SQL authentication is disabled
sql_authentication_still_permitted true when the flag was set to false
force_new_fields / fields_that_can_change_after_creation lifecycle
fields_azure_returns_on_read where drift is detectable
one_terraform_resource_writes_two_arm_records constant true
module_default_diverges_from_provider_default constant true
enabling_entra_only_breaks_every_sql_login constant true
destroying_this_resource_re_enables_sql_authentication constant true
the_delete_timeout_is_three_hours constant true
an_existing_administrator_blocks_creation true BY DEFAULT — read with the next row
the_import_guard_can_be_switched_off_by_the_caller constant true — the guard is a provider feature flag
the_entra_administrator_must_exist_before_entra_only_can_be_enabled constant true — Microsoft's ordering requirement
a_failed_entra_only_write_leaves_the_administrator_in_state constant true — the half-finished apply
sql_managed_instance_contributor_cannot_change_entra_only_authentication constant true — needs SQL Security Manager
changing_the_administrator_is_an_in_place_update constant true
login_username_is_a_label_not_an_identity constant true
a_group_is_a_better_administrator_than_a_person constant true
the_tenant_is_required_here_unlike_on_the_logical_server constant true
one_administrator_per_managed_instance constant true
no_secret_is_accepted_or_emitted_by_this_module constant true
this_resource_supports_no_azure_resource_tags constant true

🔒 No secret is accepted or emitted. An administrator is identified here, never authenticated.


🧠 Architecture Notes

One Terraform resource, two ARM records. The administrator lives under /administrators/ActiveDirectory; Entra-only authentication is a separate sub-resource under /azureADOnlyAuthentications/Default, written by a second client in the same create and read back from a different API. They can drift independently. Both Resource IDs are emitted, because only one of them is this resource's id and a review needs to name the other.

The divergence here prevents an active weakening, not just a no-op. The provider declares azuread_authentication_only with no default, so its zero value is false — and the create sends that value unconditionally. A bare resource block therefore disables Entra-only authentication on an instance that may have had it on. This module defaults to true, matching this suite's Entra-first convention for SQL and the sibling logical-server module.

And that default is disruptive, which is not a reason to hide it. Enabling Entra-only authentication stops every SQL-authenticated login working, including the built-in administrator created with the instance. The README, the variable description and an output all say so, and azuread_authentication_only = false remains a first-class choice for a migration period.

Destroy is a security change. Because the service will not remove an Entra administrator while Entra-only authentication is enabled, the provider disables it first as a required step. A terraform destroy therefore hands SQL authentication back. If that matters, revoke the SQL logins themselves — this resource's absence guarantees nothing.

Handing over administration is an in-place update. Only managed_instance_id is force-new, so a changed object_id reads in a plan like any other attribute change while being the most consequential edit this module allows.

Lifecycle. A lifecycle block is not valid inside a module block, so a caller who wants deletion protection wants a CanNotDelete management lock on the instance — which prevents deletion, not replacement.


🧱 Design Principles

Concern Secure default (empty call) Opt-out (caller must type it)
Authentication azuread_authentication_only = true — diverging from the provider's false set to false
Administrator principal none assumed — required input —
Directory object shape a group is recommended and reported, never enforced name a user or service principal
Display-name mistakes a GUID or Resource ID is refused —
Secret handling none — no credential passes through this module not available
Existing administrator refused with a requires-import error terraform import

🚀 Runbook

terraform init -backend=false
terraform validate
terraform fmt -check

Pin the module with ?ref=v1.0.0 — never a branch. This module is plan-only in this repository; a human applies from CI.


🧪 Testing

terraform validate proves, offline and with no credentials: the type schemas, the anchored managed_instance_id pattern (a logical server ID and a child ID are both refused), the non-empty login_username check, the GUID-or-Resource-ID-as-display-name refusal, and the UUID shape of object_id and tenant_id.

Only terraform plan — which needs credentials — exercises: whether the instance exists, whether an administrator is already configured (the requires-import guard), and whether the object ID names anything real in the tenant.

Nothing at either stage catches an object ID that is well-formed and wrong. Azure accepts it without the Terraform principal being able to read it in Entra, and the result is an administrator nobody can use.

Note the asymmetry: a validation failure blocks terraform destroy as well as apply, which is why this module refuses only what the provider itself would refuse, plus the one display-name mistake.


💬 Example Output

id                                 = "/subscriptions/.../resourceGroups/rg-data-platform/providers/Microsoft.Sql/managedInstances/sqlmi-platform-eus2/administrators/ActiveDirectory"
azuread_only_authentication_id     = "/subscriptions/.../managedInstances/sqlmi-platform-eus2/azureADOnlyAuthentications/Default"
managed_instance_name              = "sqlmi-platform-eus2"
login_username                     = "sql-mi-admins"
object_id                          = "1a2b3c4d-5e6f-7081-92a3-b4c5d6e7f809"
azuread_authentication_only        = true
sql_authentication_still_permitted = false
force_new_fields                   = ["managed_instance_id"]

🔍 Troubleshooting

Symptom Cause Fix
managed_instance_id must be a SQL MANAGED INSTANCE Resource ID a logical server ID, or a child of the instance, was passed Use the managed instance module's id. A logical server sets its administrator inline on the server resource
login_username looks like a GUID or a Resource ID the object ID was pasted into the display-name field Put the GUID in object_id; login_username is the portal label
object_id must be a UUID / tenant_id must be a UUID a display name or a Resource ID was passed Pass bare GUIDs
login_username is empty or whitespace blank display name Supply the principal's display name
Applications suddenly cannot connect after an apply azuread_authentication_only defaults to true here and disables SQL logins Expected. Set it to false while migrating, or move the applications to Entra authentication
A requires-import error on first apply an administrator already exists on the instance Expected, and better than overwriting. terraform import the existing record
terraform destroy left SQL authentication working again the provider must disable Entra-only authentication before removing the administrator Expected. Revoke the SQL logins separately
A destroy timed out the provider's own delete deadline is 180 minutes for a reason Do not shorten timeouts.delete; raise it if needed
The portal shows an administrator name that no longer matches the group login_username is a label Azure never reconciles with object_id Update login_username; nothing detects this for you
The administrator was set but nobody can sign in the object ID is well-formed but names nothing, or the wrong tenant was supplied Verify the object in Entra; no permission or validation check catches this

🔗 Related Docs


💙 "Infrastructure as Code should be standardized, consistent, and secure."