A standalone, secure-by-default Terraform module for a single Azure SQL Managed Instance, targeting
hashicorp/azurerm ~> 4.0.
This module manages one Azure SQL Managed Instance and nothing else β it owns the keystone
azurerm_mssql_managed_instance and its two inline blocks (identity and timeouts).
- ποΈ Provisions a SQL Managed Instance with an explicit SKU, vCore count, and storage size.
- π Ships a private, TLS-1.2 posture: the public data endpoint is off and the transport floor is 1.2 on an empty call.
- π Attaches a system-assigned managed identity by default, so the instance has a Microsoft Entra identity without embedded credentials.
- π Joins a caller-supplied subnet that must be delegated to
Microsoft.Sql/managedInstances. - πΎ Defaults backups to geo-redundant (
GRS) storage for cross-region durability. - π Accepts a SQL administrator password only as a
sensitiveinput to be provisioned out of band, and documents Microsoft Entra-only authentication as the preferred path.
π‘ Why it matters: A SQL Managed Instance is a long-lived, network-anchored data platform whose defaults govern how exposed and how durable your database estate is. This module makes the hardened instance the default and forces every relaxation β public access, weaker TLS, no managed identity, local-only backups β to be typed out explicitly and reviewed.
If this module saves you time, please consider supporting its ongoing maintenance:
- β Star the repository to help others find it.
- π€ Connect on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
flowchart LR
rg["terraform-azurerm-resource-group"]
vnet["terraform-azurerm-virtual-network (delegated subnet)"]
nsg["terraform-azurerm-network-security-group"]
rt["terraform-azurerm-route-table"]
mi["terraform-azurerm-mssql-managed-instance"]
aad["terraform-azurerm-mssql-managed-instance-active-directory-administrator"]
tde["terraform-azurerm-mssql-managed-instance-transparent-data-encryption"]
fog["terraform-azurerm-mssql-managed-instance-failover-group"]
diag["terraform-azurerm-monitor-diagnostic-setting"]
rg -->|"resource_group_name"| mi
vnet -->|"subnet_id"| mi
nsg -->|"secures subnet"| vnet
rt -->|"routes subnet"| vnet
mi -->|"id"| aad
mi -->|"id"| tde
mi -->|"dns_zone"| fog
mi -->|"id"| diag
classDef self fill:#0078D4,stroke:#004578,color:#ffffff;
classDef keystone fill:#004578,stroke:#002240,color:#ffffff;
classDef ext fill:#f1f3f5,stroke:#adb5bd,color:#212529;
class mi self;
class aad,tde,fog keystone;
class rg,vnet,nsg,rt,diag ext;
flowchart TB
subgraph inputs["Inputs"]
rgn["resource_group_name"]
loc["location"]
sub["subnet_id (delegated)"]
sku["sku_name / vcores / storage_size_in_gb"]
idn["identity (SystemAssigned default)"]
end
mi["azurerm_mssql_managed_instance.this"]
idblk["identity {} (inline)"]
toblk["timeouts {} (inline)"]
subgraph outputs["Outputs"]
oid["id"]
onm["name"]
ofq["fqdn"]
odns["dns_zone"]
opid["identity_principal_id"]
end
rgn -->|"places"| mi
loc -->|"places"| mi
sub -->|"joins"| mi
sku -->|"sizes"| mi
idn -->|"renders"| idblk
mi --- idblk
mi --- toblk
mi -->|"emits"| oid
mi -->|"emits"| onm
mi -->|"emits"| ofq
mi -->|"emits"| odns
idblk -->|"emits"| opid
classDef self fill:#0078D4,stroke:#004578,color:#ffffff;
classDef keystone fill:#004578,stroke:#002240,color:#ffffff;
classDef ext fill:#f1f3f5,stroke:#adb5bd,color:#212529;
class mi keystone;
class idblk,toblk self;
class rgn,loc,sub,sku,idn,oid,onm,ofq,odns,opid ext;
Resource inventory
| Resource | Count | Role |
|---|---|---|
azurerm_mssql_managed_instance.this |
1 | The keystone managed instance |
identity {} (inline dynamic) |
0β1 | System- or user-assigned managed identity |
timeouts {} (inline dynamic) |
0β1 | Optional operation timeouts |
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
hashicorp/azurerm |
~> 4.0 |
| Provider block | None in this module β the caller configures provider "azurerm" { features {} }, auth, and subscription |
Schema notes that bite (verified against the live provider schema):
name,resource_group_name,location,collation, andtimezone_idare immutable β changing any forces replacement.locationandresource_group_nametake theirForceNewfrom the provider's shared schema helpers, so they do not appear in a plain search of the resource's own source.administrator_loginis immutable too, and this is the costliest surprise here. Renaming the SQL administrator destroys the instance and builds a new one, with a 24-hour timeout allowed for each half.administrator_login_passwordis not immutable β rotating the password is an ordinary in-place update.- Three fields are force-new in one direction only, each by way of the provider's
CustomizeDiffrather than a schema marker, so nothing but the plan warns:dns_zone_partner_idβ replaces only on the empty β set transition. Changing it between two partner IDs, or clearing it, updates in place.identityβ replaces only on removal of the last identity. Adding one is in-place. The provider cites an open Azure REST API defect.database_formatβ replaces only on theAlwaysUpToDateβSQLServer2022downgrade. The upgrade is in-place.
subnet_idis updatable in the current provider line, but the subnet must be delegated toMicrosoft.Sql/managedInstancesand dedicated to managed instances; a non-conforming subnet fails at apply, not at plan.minimum_tls_versionaccepts1.0,1.1and1.2on the pinned~> 4.0line, andproxy_overrideacceptsDefault,ProxyandRedirect. The registry documentation states narrower sets for both β it describes the forthcoming 5.0 provider, whose schema block 4.x replaces with a wider override. This module accepts the real 4.x sets, defaults to the locked-down value, and reports a weakened TLS floor through an output rather than refusing it.proxy_overridehas no schema default on 4.x (it is Optional + Computed); the provider substitutesDefaultat create time when the argument is omitted. An empty call therefore yieldsDefault, notRedirect.storage_iopsapplies only whengeneral_purpose_v2_enabled = trueand is refused on anyBC_SKU;general_purpose_v2_enabledcannot betrueon aBC_SKU. All three rules live inCustomizeDiffand are mirrored by this module'svalidation {}blocks so they fail offline instead.zone_redundant_enabledis available on both General Purpose and Business Critical β it is only the Next-gen General Purpose tier that excludes it. Microsoft additionally requiresstorage_account_typeofZRSorGZRSbefore zone redundancy can be enabled; the provider does not check that pairing, so this module does.administrator_login_passwordanddns_zone_partner_idare never returned by the Azure API β the provider's read sets both back from prior state, so drift in either is permanently invisible toterraform plan.- Provisioning is slow: create, update, and delete each default to a 24-hour service timeout. Deleting the last instance in a subnet also tears down the virtual cluster, which Microsoft documents at around 90 minutes, against about a minute for any other instance.
SQL Managed Instance Contributoron the target resource group (create/update/delete the instance), or a custom role grantingMicrosoft.Sql/managedInstances/*at that scope.Network Contributor(orMicrosoft.Network/virtualNetworks/subnets/join/action) on the delegated subnet so the instance can join it.Managed Identity Operatoron any user-assigned identity you attach.- Least-privilege: scope each role to the smallest resource group / subnet that works, not the subscription.
- The
Microsoft.Sqlresource provider registered on the target subscription. - An existing resource group in a supported US Azure region.
- A dedicated subnet delegated to
Microsoft.Sql/managedInstances, with an associated network security group and route table (user-defined routes), not shared with other resources. - Sufficient regional vCore quota for the chosen SKU and vCore count.
- For customer-managed transparent data encryption: an existing Key Vault and key, with the instance identity granted access (wired via the sibling modules).
- The caller configures the
provider "azurerm" { features {} }block, auth, and subscription; this module declares none of these.
terraform-azurerm-mssql-managed-instance/
βββ providers.tf # required_version >= 1.12.0; azurerm ~> 4.0; no provider block
βββ variables.tf # deeply-typed object() schemas; secure defaults; tags + timeouts tail
βββ main.tf # keystone azurerm_mssql_managed_instance.this; dynamic identity + timeouts
βββ outputs.tf # id first, then name, fqdn, dns_zone, identity principal/tenant IDs
βββ README.md # this document
βββ SCOPE.md # the cross-module contract
βββ LICENSE # MIT
βββ .gitignore # canonical library ignore set
The smallest real call. The caller configures the provider, auth, and the mandatory features {} block.
provider "azurerm" {
features {}
}
module "sql_managed_instance" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance.git?ref=v1.0.0"
name = "mi-platform-eastus2"
resource_group_name = "rg-data-platform"
location = "eastus2"
sku_name = "GP_Gen5"
vcores = 8
storage_size_in_gb = 256
subnet_id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-network/providers/Microsoft.Network/virtualNetworks/vnet-core/subnets/snet-sqlmi"
license_type = "LicenseIncluded"
administrator_login = "sqladmin"
administrator_login_password = var.mi_admin_password # provisioned out of band; never committed
tags = {
environment = "production"
workload = "data-platform"
}
}π On an empty call the instance is private (
public_data_endpoint_enabled = false), enforcesminimum_tls_version = "1.2", carries a system-assigned identity, and uses geo-redundant backups.
Consumes
| Input | Type | From |
|---|---|---|
resource_group_name |
string |
resource-group module output name |
location |
string |
caller / resource group location |
subnet_id |
string |
virtual-network module subnet_ids (a subnet delegated to Microsoft.Sql/managedInstances) |
identity.identity_ids |
set(string) |
user-assigned-identity module id (user-assigned identity only) |
dns_zone_partner_id |
string |
a partner mssql-managed-instance module id (failover-group topologies) |
Emits
| Output | Description | Consumed by |
|---|---|---|
id |
SQL Managed Instance Resource ID | Entra admin / TDE / failover group / diagnostics / role assignments |
name |
Instance name | diagnostics / tagging conventions |
fqdn |
Fully qualified domain name | application connection strings |
dns_zone |
DNS zone the instance lives in | a partner instance's dns_zone_partner_id |
identity_principal_id |
Principal ID of the managed identity (or null) | Key Vault / RBAC grants for customer-managed TDE |
identity_tenant_id |
Tenant ID of the managed identity (or null) | Entra-scoped grants |
1 Β· Minimal secure General Purpose instance
The empty-ish call: private, TLS 1.2, system-assigned identity, geo-redundant backups.
module "mi" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance.git?ref=v1.0.0"
name = "mi-app-eastus2"
resource_group_name = "rg-data"
location = "eastus2"
sku_name = "GP_Gen5"
vcores = 4
storage_size_in_gb = 128
subnet_id = var.sqlmi_subnet_id
license_type = "LicenseIncluded"
administrator_login = "sqladmin"
administrator_login_password = var.mi_admin_password
}π‘ Everything not set here inherits the module's secure defaults β you type only what the workload needs.
2 Β· Business Critical, zone-redundant
Business Critical adds local SSD, higher IOPS, and read replicas; zone redundancy spreads them across availability zones.
module "mi" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance.git?ref=v1.0.0"
name = "mi-core-eastus2"
resource_group_name = "rg-data"
location = "eastus2"
sku_name = "BC_Gen5"
vcores = 16
storage_size_in_gb = 512
subnet_id = var.sqlmi_subnet_id
license_type = "LicenseIncluded"
zone_redundant_enabled = true
storage_account_type = "GZRS" # required: zone redundancy needs zonal backup storage
administrator_login = "sqladmin"
administrator_login_password = var.mi_admin_password
}
β οΈ Zone redundancy requiresstorage_account_typeofZRSorGZRS. The module default isGRS, so leaving it at the default here would produce a configuration Azure refuses at apply β this module refuses it at validate instead. Zone redundancy is available on General Purpose as well as Business Critical; only the Next-gen General Purpose tier excludes it. It is also region-limited, costs more for both compute and backups, and Microsoft documents enabling it as taking around four hours.
3 Β· Bring-your-own-license (Azure Hybrid Benefit)
Use existing SQL Server licenses by setting the base-price license model.
module "mi" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance.git?ref=v1.0.0"
name = "mi-byol-eastus2"
resource_group_name = "rg-data"
location = "eastus2"
sku_name = "GP_Gen5"
vcores = 8
storage_size_in_gb = 256
subnet_id = var.sqlmi_subnet_id
license_type = "BasePrice"
administrator_login = "sqladmin"
administrator_login_password = var.mi_admin_password
}βΉοΈ
license_type = "LicenseIncluded"(the other legal value) bills the SQL license with the instance.
4 Β· Explicit TLS 1.2 floor
The secure default made explicit for auditability.
module "mi" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance.git?ref=v1.0.0"
name = "mi-tls-eastus2"
resource_group_name = "rg-data"
location = "eastus2"
sku_name = "GP_Gen5"
vcores = 4
storage_size_in_gb = 128
subnet_id = var.sqlmi_subnet_id
license_type = "LicenseIncluded"
minimum_tls_version = "1.2"
administrator_login = "sqladmin"
administrator_login_password = var.mi_admin_password
}π Legal values are
1.0,1.1,1.2. Lowering the floor weakens transport security and is generally rejected by the service.
5 Β· Connection policy β proxy override Redirect
Redirect gives lower latency after the handshake but requires additional ports.
module "mi" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance.git?ref=v1.0.0"
name = "mi-redirect-eastus2"
resource_group_name = "rg-data"
location = "eastus2"
sku_name = "GP_Gen5"
vcores = 4
storage_size_in_gb = 128
subnet_id = var.sqlmi_subnet_id
license_type = "LicenseIncluded"
proxy_override = "Redirect"
administrator_login = "sqladmin"
administrator_login_password = var.mi_admin_password
}
β οΈ Redirectrequires ports1433and11000-11999open in the subnet's network security group. Legal values areDefault,Proxy,Redirect.
6 Β· Public data endpoint (explicit opt-in)
Exposing the public endpoint is a deliberate relaxation of the secure default.
module "mi" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance.git?ref=v1.0.0"
name = "mi-public-eastus2"
resource_group_name = "rg-data"
location = "eastus2"
sku_name = "GP_Gen5"
vcores = 4
storage_size_in_gb = 128
subnet_id = var.sqlmi_subnet_id
license_type = "LicenseIncluded"
public_data_endpoint_enabled = true
proxy_override = "Proxy"
administrator_login = "sqladmin"
administrator_login_password = var.mi_admin_password
}
β οΈ Enabling the public data endpoint accepts traffic from outside the VNet on port3342. Pair it with tight network-security-group rules and keep it off unless a public consumer truly requires it.
7 Β· User-assigned managed identity
Attach one or more user-assigned identities instead of the default system-assigned one.
module "mi" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance.git?ref=v1.0.0"
name = "mi-uai-eastus2"
resource_group_name = "rg-data"
location = "eastus2"
sku_name = "GP_Gen5"
vcores = 4
storage_size_in_gb = 128
subnet_id = var.sqlmi_subnet_id
license_type = "LicenseIncluded"
identity = {
type = "UserAssigned"
identity_ids = [var.uai_id]
}
administrator_login = "sqladmin"
administrator_login_password = var.mi_admin_password
}βΉοΈ
identity_idsis required whenevertypeincludesUserAssigned.
8 Β· Both system- and user-assigned identity
Combine the platform identity with one or more caller-owned identities.
module "mi" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance.git?ref=v1.0.0"
name = "mi-dualid-eastus2"
resource_group_name = "rg-data"
location = "eastus2"
sku_name = "GP_Gen5"
vcores = 8
storage_size_in_gb = 256
subnet_id = var.sqlmi_subnet_id
license_type = "LicenseIncluded"
identity = {
type = "SystemAssigned, UserAssigned"
identity_ids = [var.uai_id]
}
service_principal_type = "SystemAssigned"
administrator_login = "sqladmin"
administrator_login_password = var.mi_admin_password
}π‘ Set
identity = nullto opt out of a managed identity entirely β but prefer an identity over embedded credentials.
9 Β· Next-gen General Purpose with provisioned IOPS
The Next-gen General Purpose tier exposes provisioned storage IOPS.
module "mi" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance.git?ref=v1.0.0"
name = "mi-gpv2-eastus2"
resource_group_name = "rg-data"
location = "eastus2"
sku_name = "GP_Gen5"
vcores = 8
storage_size_in_gb = 256
subnet_id = var.sqlmi_subnet_id
license_type = "LicenseIncluded"
general_purpose_v2_enabled = true
storage_iops = 4000
administrator_login = "sqladmin"
administrator_login_password = var.mi_admin_password
}
β οΈ storage_iops(range 300β80000) applies only whengeneral_purpose_v2_enabled = true, which is incompatible withzone_redundant_enabled.
10 Β· Custom collation and timezone (immutable)
Both fields are force-new; set them at creation and treat them as fixed.
module "mi" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance.git?ref=v1.0.0"
name = "mi-collation-centralus"
resource_group_name = "rg-data"
location = "centralus"
sku_name = "GP_Gen5"
vcores = 4
storage_size_in_gb = 128
subnet_id = var.sqlmi_subnet_id
license_type = "LicenseIncluded"
collation = "Latin1_General_100_CI_AS_SC"
timezone_id = "UTC"
administrator_login = "sqladmin"
administrator_login_password = var.mi_admin_password
}
β οΈ Changingcollation,timezone_id, ordns_zone_partner_idafter creation forces the instance to be replaced.
11 Β· Backup redundancy β zone-redundant backup storage
Tune the backup storage account type independently of the compute tier.
module "mi" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance.git?ref=v1.0.0"
name = "mi-gzrs-eastus2"
resource_group_name = "rg-data"
location = "eastus2"
sku_name = "GP_Gen5"
vcores = 8
storage_size_in_gb = 256
subnet_id = var.sqlmi_subnet_id
license_type = "LicenseIncluded"
storage_account_type = "GZRS"
administrator_login = "sqladmin"
administrator_login_password = var.mi_admin_password
}βΉοΈ Legal values are
GRS(default),GZRS,LRS,ZRS. Prefer a geo- or zone-redundant option for regulated data.
12 Β· Custom operation timeouts
Managed instance operations are slow; the service defaults are 24 hours. Override deliberately.
module "mi" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance.git?ref=v1.0.0"
name = "mi-timeouts-eastus2"
resource_group_name = "rg-data"
location = "eastus2"
sku_name = "GP_Gen5"
vcores = 4
storage_size_in_gb = 128
subnet_id = var.sqlmi_subnet_id
license_type = "LicenseIncluded"
timeouts = {
create = "24h"
update = "24h"
delete = "24h"
read = "5m"
}
administrator_login = "sqladmin"
administrator_login_password = var.mi_admin_password
}
β οΈ Do not shorten the create timeout below what your region typically needs β a truncated timeout fails the apply mid-provision.
13 Β· Failover-group-ready pair (shared DNS zone)
Create a primary, then a secondary that shares the primary's DNS zone via dns_zone_partner_id.
module "mi_primary" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance.git?ref=v1.0.0"
name = "mi-primary-eastus2"
resource_group_name = "rg-data"
location = "eastus2"
sku_name = "BC_Gen5"
vcores = 8
storage_size_in_gb = 256
subnet_id = var.sqlmi_subnet_primary
license_type = "LicenseIncluded"
administrator_login = "sqladmin"
administrator_login_password = var.mi_admin_password
}
module "mi_secondary" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance.git?ref=v1.0.0"
name = "mi-secondary-westus2"
resource_group_name = "rg-data-dr"
location = "westus2"
sku_name = "BC_Gen5"
vcores = 8
storage_size_in_gb = 256
subnet_id = var.sqlmi_subnet_secondary
license_type = "LicenseIncluded"
dns_zone_partner_id = module.mi_primary.id
administrator_login = "sqladmin"
administrator_login_password = var.mi_admin_password
}
# The failover group itself is a sibling module wired against both instance ids.
β οΈ dns_zone_partner_idis immutable β set it at creation of the secondary.
14 Β· for_each at scale β a map of instances
Instantiate many instances from a keyed map so adding or removing one never re-indexes the rest.
locals {
instances = {
app = { sku_name = "GP_Gen5", vcores = 4, storage_size_in_gb = 128 }
core = { sku_name = "BC_Gen5", vcores = 16, storage_size_in_gb = 512 }
}
}
module "mi" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance.git?ref=v1.0.0"
for_each = local.instances
name = "mi-${each.key}-eastus2"
resource_group_name = "rg-data"
location = "eastus2"
sku_name = each.value.sku_name
vcores = each.value.vcores
storage_size_in_gb = each.value.storage_size_in_gb
subnet_id = var.sqlmi_subnet_id
license_type = "LicenseIncluded"
administrator_login = "sqladmin"
administrator_login_password = var.mi_admin_password
}π‘ Keys (
app,core) are the stable identity β renaming a key replaces that instance, so keep keys meaningful and fixed.
15 Β· ποΈ End-to-end composition
Wire a resource group, a virtual network with a delegated subnet, a network security group, a route table, and this managed instance into one topology. Sibling outputs feed this module's inputs.
provider "azurerm" {
features {}
}
module "rg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-resource-group.git?ref=v1.0.0"
name = "rg-data-platform"
location = "eastus2"
}
module "nsg" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-network-security-group.git?ref=v1.0.0"
name = "nsg-sqlmi"
resource_group_name = module.rg.name
location = module.rg.location
}
module "route_table" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-route-table.git?ref=v1.0.0"
name = "rt-sqlmi"
resource_group_name = module.rg.name
location = module.rg.location
}
module "vnet" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-virtual-network.git?ref=v1.0.0"
name = "vnet-core"
resource_group_name = module.rg.name
location = module.rg.location
address_space = ["10.20.0.0/16"]
subnets = {
sqlmi = {
name = "snet-sqlmi"
address_prefixes = ["10.20.1.0/27"]
network_security_group_id = module.nsg.id
route_table_id = module.route_table.id
delegations = {
managedinstance = {
service_delegation_name = "Microsoft.Sql/managedInstances"
service_delegation_actions = ["Microsoft.Network/virtualNetworks/subnets/join/action"]
}
}
}
}
}
module "sql_managed_instance" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance.git?ref=v1.0.0"
name = "mi-platform-eastus2"
resource_group_name = module.rg.name
location = module.rg.location
sku_name = "GP_Gen5"
vcores = 8
storage_size_in_gb = 256
subnet_id = module.vnet.subnet_ids["sqlmi"]
license_type = "LicenseIncluded"
administrator_login = "sqladmin"
administrator_login_password = var.mi_admin_password
tags = {
environment = "production"
workload = "data-platform"
}
}βΉοΈ The Entra administrator, transparent data encryption, and failover group are separate sibling modules; wire them against
module.sql_managed_instance.id(and.dns_zonefor failover groups). Subnet argument names follow the virtual-network module's own schema.
Required
| Name | Type | Description |
|---|---|---|
name |
string |
Globally unique instance name (immutable) |
resource_group_name |
string |
Existing resource group (immutable) |
location |
string |
Azure region (immutable) |
sku_name |
string |
Service tier + hardware generation (validated enum) |
vcores |
number |
vCore count for the SKU |
storage_size_in_gb |
number |
Storage size in GB; 32β32768, capped at 16384 unless general_purpose_v2_enabled |
subnet_id |
string |
Delegated subnet resource ID (anchored pattern) |
license_type |
string |
LicenseIncluded or BasePrice β compared case-insensitively, as the provider does |
administrator_login |
string |
SQL administrator login. Required through this module (see below), and immutable |
administrator_login_password |
string |
SQL administrator password. Required through this module, sensitive, 8β128 characters |
β οΈ The two administrator inputs are required, and that is a deliberate departure from this suite's usual preference for optional inputs. The provider marks bothOptional, but pairs each β throughAtLeastOneOfβ with the inlineazure_active_directory_administratorblock. This module does not expose that block (the Entra administrator is a sibling module with its own lifecycle), and becauseAtLeastOneOfnames arguments of the same resource, a separate Entra administrator resource cannot satisfy it. Omitting them fails at plan with a provider error, so the module declares them required and the failure lands at parse time instead. Provision the password out of band and pass a reference.
Common optional (secure defaults)
| Name | Type | Default | Description |
|---|---|---|---|
minimum_tls_version |
string |
"1.2" |
Transport floor. 1.0 / 1.1 / 1.2 are all accepted on ~> 4.0; the module defaults to the floor and reports a weakened value rather than refusing it |
public_data_endpoint_enabled |
bool |
false |
Public endpoint off by default (port 3342 when enabled) |
storage_account_type |
string |
"GRS" |
Backup redundancy, not data storage. ZRS/GZRS required for zone redundancy |
identity |
object |
{ type = "SystemAssigned" } |
Managed identity; type closed set, identity_ids required when type includes UserAssigned and each entry pattern-checked |
proxy_override |
string |
null |
Connection policy. Default / Proxy / Redirect on ~> 4.0; omitting it yields Default |
zone_redundant_enabled |
bool |
false |
Requires ZRS/GZRS backups; excluded on Next-gen General Purpose |
general_purpose_v2_enabled |
bool |
null |
Next-gen General Purpose. Refused on BC_ SKUs; the only tier that bills IOPS |
storage_iops |
number |
null |
300β80000; requires general_purpose_v2_enabled, refused on BC_ SKUs |
tags |
map(string) |
{} |
Resource tags; at most 50, names β€512 and values β€256 characters |
timeouts |
object |
null |
Operation timeouts; Go duration strings, provider defaults are 24h/5m/24h/24h |
Full input schemas
variable "identity" {
type = object({
type = string # SystemAssigned | UserAssigned | "SystemAssigned, UserAssigned"
identity_ids = optional(set(string), []) # required when type includes UserAssigned
})
default = { type = "SystemAssigned" }
}
variable "timeouts" {
type = object({
create = optional(string)
read = optional(string)
update = optional(string)
delete = optional(string)
})
default = null
}Other optional inputs: collation (string, null, immutable), timezone_id (string, null,
immutable), maintenance_configuration_name (string, null, validated SQL_Default or
SQL_{Location}_MI_{Number}), hybrid_secondary_usage (string, null, validated Active/Passive),
database_format (string, null, validated AlwaysUpToDate/SQLServer2022; the downgrade forces
replacement), dns_zone_partner_id (string, null, anchored managed-instance ID pattern; force-new only
on empty β set), service_principal_type (string, null, validated SystemAssigned).
collation and timezone_id are checked only for non-emptiness, matching the provider β SQL Server ships
thousands of collations and the Windows time zone set varies by build, so a list short enough to maintain
would refuse legal values. A misspelling in either passes validate and plan and is refused by Azure at
apply.
| Output | Description | Notes |
|---|---|---|
id |
SQL Managed Instance Resource ID | Emitted first; the reference every sibling module consumes |
name |
Instance name | |
resource_group_name |
Resource group containing the instance | |
location |
Azure region, in the canonical form Azure uses | Read from the resource, not var.location |
fqdn |
Fully qualified domain name | For connection strings; public only when the public endpoint is on, and then on port 3342 |
dns_zone |
DNS zone the instance lives in | Confirms two instances share a zone. The partner argument takes an instance ID, not this string |
subnet_id |
Delegated subnet hosting the instance | |
identity_principal_id |
Managed identity principal ID | null when no identity is configured |
identity_tenant_id |
Managed identity tenant ID | null when no identity is configured |
user_assigned_identity_ids |
User-assigned identities attached | Empty set for a purely system-assigned identity |
primary_user_assigned_identity_is_nondeterministic |
More than one user-assigned identity is attached | The provider picks the primary from an unordered map; the choice is not stable across runs |
sku_name |
SKU as Azure reports it | Normalised by the provider β Azure returns BC_G8IH / MIBC64G8IH for BC_Gen8IH |
vcores |
vCore count | |
storage_size_in_gb |
Allocated storage in GB | |
public_data_endpoint_enabled |
Public endpoint posture | For a policy assertion without re-reading the input |
backup_storage_is_geo_redundant |
Backups replicate cross-region (GRS/GZRS) |
False means a geo-restore has nothing to restore from |
tls_floor_is_weakened |
minimum_tls_version is not 1.2 |
Reported, not refused β refusing would block destroy |
proxy_override_effective |
Connection policy actually in force | An omitted value yields Default on ~> 4.0, not Redirect |
admin_credentials_are_mandatory_through_this_module |
Always true |
AtLeastOneOf cannot be satisfied by the sibling Entra module |
changing_admin_login_replaces_this_instance |
Always true |
The login is force-new; the password is not |
credential_and_partner_drift_is_invisible |
Always true |
The API returns neither the password nor the partner ID |
database_format_downgrade_replaces_this_instance |
Always true |
The upgrade is in-place; only the downgrade rebuilds |
removing_identity_replaces_this_instance |
Always true |
Adding an identity is in-place; removing the last one rebuilds |
iops_is_separately_billed |
Next-gen General Purpose is selected | The only tier that bills IOPS, above a free quota |
storage_size_is_multiple_of_32 |
Size matches Microsoft's documented increment | Reported, not enforced |
deleting_the_last_instance_in_a_subnet_is_slow |
Always true |
~90 minutes, against ~1 minute for any other instance |
No output carries a secret. administrator_login_password is deliberately not re-emitted: it is already in
this module's state, and emitting it would copy it into every consuming configuration's state as well, for
no benefit.
π The administrator password is an input only and is never emitted as an output.
- Immutability is broad.
name,resource_group_name,location,collation,timezone_id, anddns_zone_partner_idare all force-new β treat them as fixed at creation. Plan a new instance rather than an in-place change when any of these must move. - The subnet is a hard contract.
subnet_idmust reference a subnet delegated toMicrosoft.Sql/managedInstances, dedicated to managed instances, and paired with a network security group and a route table. This is not enforced atplan; a non-conforming subnet fails during the (long) apply. - Provisioning is measured in hours. The service defaults create/update/delete to 24 hours. CI pipelines
and human runbooks must budget for this; the
timeoutsinput exists to raise, not casually lower, these. - Identity by default.
identitydefaults to a system-assigned identity soidentity_principal_idis available to grant Key Vault or RBAC access (for example for customer-managed TDE via the sibling module). Setidentity = nullonly when you deliberately want no managed identity. - Authentication path. SQL authentication needs
administrator_login+administrator_login_passwordat creation. The password is asensitiveinput to be provisioned out of band. Microsoft Entra-only authentication β the preferred path β is configured via the sibling active-directory-administrator module and lets you omit the SQL password entirely. features {}dependence. The provider will not initialize without a caller-sideprovider "azurerm" { features {} }block. That is expected; this module intentionally declares no provider block.
| Concern | Secure default (empty call) | Opt-out (caller types it) |
|---|---|---|
| Public data endpoint | public_data_endpoint_enabled = false |
set to true |
| Transport security | minimum_tls_version = "1.2" |
set 1.1 / 1.0 |
| Managed identity | identity = { type = "SystemAssigned" } |
set a different type or null |
| Backup redundancy | storage_account_type = "GRS" (geo-redundant) |
set LRS / ZRS / GZRS |
| Secrets | password is sensitive, provisioned out of band; prefer Entra-only auth |
pass a plaintext value (discouraged) |
Rules of thumb: a boolean that gates exposure defaults to the closed value; an enum that includes a
"public"/"allow-all" member defaults to the locked-down member and is enforced with a validation {} block;
the module never emits a secret.
terraform init -backend=false
terraform validate
terraform fmt -check- Pin the module with
?ref=v1.0.0β never track a branch. - This module is plan-only during authoring and review; a human runs
terraform applyfrom CI against real credentials. - Because apply can run for hours, schedule managed-instance changes in a window that tolerates long operations.
The offline proof gate covers what can be verified without touching Azure:
terraform init -backend=falseresolves and installs the pinnedazurerm ~> 4.0provider.terraform validateproves the configuration is internally consistent and type-correct against the pinned provider schema β everyobject()schema andvalidation {}block is exercised here.terraform fmt -checkenforces canonical formatting.
What only a human-run terraform plan/apply from CI can exercise: subnet delegation conformance, regional
vCore quota, SKU/vCore/storage combinations, and the multi-hour provisioning itself. This library never runs
plan or apply during authoring.
Outputs:
id = "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/rg-data-platform/providers/Microsoft.Sql/managedInstances/mi-platform-eastus2"
name = "mi-platform-eastus2"
fqdn = "mi-platform-eastus2.a1b2c3d4e5f6.database.windows.net"
dns_zone = "a1b2c3d4e5f6"
identity_principal_id = "11111111-2222-3333-4444-555555555555"
identity_tenant_id = "99999999-8888-7777-6666-555555555555"
| Symptom | Cause | Fix |
|---|---|---|
SubnetIsNotDelegatedToProvider at apply |
Subnet is not delegated to Microsoft.Sql/managedInstances |
Delegate the subnet (see the end-to-end composition) and re-apply |
VnetSubnetConflictWithIntendedPolicy / subnet in use |
Subnet is shared with other resources | Use a dedicated subnet for the managed instance |
| Apply times out during create | Default provisioning can take hours | Keep the 24-hour create timeout; do not shorten it |
provider not initialized / features error |
Missing caller-side features {} block |
Add provider "azurerm" { features {} } in the root module |
| Plan shows full replacement on a small edit | Changed an immutable field (name, location, collation, timezone_id, dns_zone_partner_id) |
Revert the field or accept replacement deliberately |
identity_ids error on user-assigned identity |
type includes UserAssigned but identity_ids is empty |
Supply at least one user-assigned identity ID |
storage_iops rejected |
Set without general_purpose_v2_enabled = true |
Enable Next-gen General Purpose or drop storage_iops |
| Cannot omit administrator password | Instance created with SQL authentication | Configure Microsoft Entra-only auth via the sibling administrator module |
- Provider resource:
azurerm_mssql_managed_instance - Azure SQL Managed Instance concepts: Microsoft Learn β SQL Managed Instance
- Subnet delegation and connectivity: Microsoft Learn β SQL Managed Instance VNet architecture
- Sibling modules:
terraform-azurerm-resource-group,terraform-azurerm-virtual-network,terraform-azurerm-network-security-group,terraform-azurerm-route-table,terraform-azurerm-mssql-managed-instance-active-directory-administrator,terraform-azurerm-mssql-managed-instance-transparent-data-encryption,terraform-azurerm-mssql-managed-instance-failover-group,terraform-azurerm-monitor-diagnostic-setting - This module's
SCOPE.mdfor the full cross-module contract.
π "Infrastructure as Code should be standardized, consistent, and secure."