Chooses the key that encrypts a SQL Managed Instance's data at rest β Microsoft-managed, Key Vault, or Managed HSM. Targets
hashicorp/azurerm ~> 4.0.
- π Sets the encryption protector for a SQL Managed Instance: a service-managed key, a Key Vault key, or a Managed HSM key.
- β The empty call is already encrypted. Supplying no key leaves transparent data encryption on with a Microsoft-managed key β so nothing here is inverted.
- β±οΈ Moves the provider's apply-time versioned-key rule to validate, where it costs nothing.
- π§Ύ Emits the untracked ARM record the provider creates alongside β
{vaultName}_{keyName}_{keyVersion}. - π Destroy reverts to a service-managed key; it deletes nothing and decrypts nothing.
π‘ Why it matters: a customer-managed key is often reached for as though it were strictly stronger. The data is encrypted either way. What changes is who holds the key β and therefore who can render the instance unreadable by deleting, disabling or expiring it. This module treats that as a control transfer with a real operational cost, and says so.
If this module saved you time:
- β Star the repository
- πΌ Connect on LinkedIn
- β Buy me a coffee
flowchart TB
MI["terraform-azurerm-mssql-managed-instance"]
THIS["terraform-azurerm-mssql-managed-instance-transparent-data-encryption"]
ADM["terraform-azurerm-mssql-managed-instance-active-directory-administrator"]
SAP["terraform-azurerm-mssql-managed-instance-security-alert-policy"]
KV["terraform-azurerm-key-vault"]
SRV["terraform-azurerm-mssql-server"]
MI -->|"id"| THIS
MI -->|"id"| ADM
MI -->|"name and resource_group_name"| SAP
KV -->|"key_ids or key_versionless_ids"| THIS
MI -->|"identity_principal_id wraps the key"| KV
THIS -.->|"also creates an untracked managed instance key record"| MI
SRV -.->|"the logical server has its own protector resource"| THIS
style THIS fill:#0078D4,stroke:#004578,color:#ffffff
style MI fill:#004578,stroke:#004578,color:#ffffff
style ADM 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
The managed instance feeds its id here and to the Entra administrator module; the security alert policy
takes the parent as a name instead, which is why that edge is labelled differently. The key vault feeds
either a versioned key_ids entry or a versionless key_versionless_ids one β and which you pick is
governed by auto_rotation_enabled. The dotted edge back to the instance is the record this module creates
without Terraform tracking it: a managed instance key, named after the vault, key and version.
flowchart TB
MID["managed_instance_id (required, force-new)"]
KEY["key_vault_key_id XOR managed_hsm_key_id, or neither"]
ROT["auto_rotation_enabled (false demands a VERSIONED key)"]
TDE["azurerm_mssql_managed_instance_transparent_data_encryption.this"]
OID["id"]
OKIND["key_kind"]
OMIK["managed_instance_key_id"]
OREV["destroying_this_resource_reverts_to_a_service_managed_key"]
MID --> TDE
KEY --> TDE
ROT --> TDE
TDE --> OID
TDE --> OKIND
TDE --> OMIK
TDE --> OREV
style TDE fill:#0078D4,stroke:#004578,color:#ffffff
style MID fill:#004578,stroke:#004578,color:#ffffff
style KEY fill:#F3F2F1,stroke:#8A8886,color:#201F1E
style ROT fill:#F3F2F1,stroke:#8A8886,color:#201F1E
style OID fill:#F3F2F1,stroke:#8A8886,color:#201F1E
style OKIND fill:#F3F2F1,stroke:#8A8886,color:#201F1E
style OMIK fill:#F3F2F1,stroke:#8A8886,color:#201F1E
style OREV fill:#F3F2F1,stroke:#8A8886,color:#201F1E
| Resource | Count | Notes |
|---|---|---|
azurerm_mssql_managed_instance_transparent_data_encryption.this |
1 | One per instance; the ID always ends /encryptionProtector/current. With a customer-managed key the provider also creates an untracked managed instance key record |
| 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:
- π΄
key_vault_key_idandmanaged_hsm_key_idare mutually exclusive. The conflict is enforced by the provider and is invisible in the binary schema. - π΄ A versionless key requires
auto_rotation_enabled = true, and the provider raises that during apply β it only discovers the problem while resolving the key. This module raises the equivalent atvalidate. - π΄ The provider creates a second ARM record Terraform does not track:
.../managedInstances/<instance>/keys/{vaultName}_{keyName}_{keyVersion}. It appears in no plan and is destroyed by nothing. - π΄ Destroy does not delete. An encryption protector cannot be removed, only switched; the provider's delete reverts the instance to a service-managed key, which its own source explains is to prevent an accidental lockout.
- π΄
timeouts.deleteis accepted and ignored. The delete function reads the create/update deadline β consistent with the delete really being an update. - π΄ There is no requires-import guard, and the provider documents why: the service always creates a protector with the instance, so an already-exists check would always fire. An apply silently replaces whatever was configured.
β οΈ State may show both key fields for an HSM key. A Managed HSM key identifier is also a valid key identifier, so the read populateskey_vault_key_idtoo and suppresses the diff. That is not drift.- π΄ The exclusion between the two key arguments is decided by an argument's PRESENCE, not by its value β and the two disagree about what empty means. An empty string in the unused slot is refused exactly as a second real key would be:
key_vault_key_id = ""beside a Managed HSM key failsterraform validatewith"key_vault_key_id": conflicts with managed_hsm_key_id.managed_hsm_key_idis worse, because the provider's create tests it for an explicit null rather than for emptiness β an empty string there reaches a key-identifier parser and the apply fails, with nothing visible offline. That is not a theoretical value: an empty string is exactly what this resource's read returns for whichever key argument is unused, so a composition wiring one instance's key outputs into another's inputs supplies one. This module renders both arguments through a normalizing local so a blank arrives as an absent argument, and its own mutual-exclusion check counts a blank as unset for the same reason. Reported asan_empty_string_is_not_the_same_as_an_unset_key. β οΈ The versionless-key diff suppression only applies whenauto_rotation_enabledis true.- βΉοΈ
managed_hsm_key_iddoes not exist on the provider's 5.0 line, wherekey_vault_key_idcarries both kinds of identifier. This library pins~> 4.0deliberately. - βΉοΈ No
tags, nolocation. Tag the managed instance instead.
| Permission | Scope | Why |
|---|---|---|
Microsoft.Sql/managedInstances/encryptionProtector/read and /write |
the managed instance | set the protector |
Microsoft.Sql/managedInstances/keys/read and /write |
the managed instance | the side-effect key record the provider creates |
SQL Security Manager (or Contributor) |
the managed instance | the built-in role carrying the above |
get, wrapKey, unwrapKey on the key |
the key vault or Managed HSM | granted to the managed instance's identity, not the Terraform principal |
get on the key |
the key vault or Managed HSM | granted to the Terraform principal, and needed only for a VERSIONLESS key identifier β see below |
π The key permissions belong to the instance's identity β with one exception, and it is on the path this module recommends. The instance's own identity is what wraps and unwraps the database encryption key. But when the configured identifier is versionless, the provider has to discover the current version before it can name the key record, and it does that by reading the key from the vault or Managed HSM as the Terraform principal, during apply. That principal therefore needs a data-plane
geton the key: on a vault, the Key Vault Crypto User role or an access policy grantingget; on a Managed HSM, the equivalent role assignment on the key. A versioned identifier needs none of this, because there is nothing to resolve.resolving_a_versionless_key_reads_the_key_as_youreports which case a configuration is in.
β οΈ The failure is late and reads as unrelated: the apply fails with a key-vault authorization error while RESOLVING the key -- before the key record is created and before the protector is set -- not with a plan-time permission message.π΄ Whoever controls the key controls the instance's readability. Vault delete rights are, in effect, instance-destroy rights β and no Azure role on the SQL side limits that. Review who can delete or disable the key with the same seriousness as who can delete the instance.
- The
Microsoft.Sqlresource provider registered on the subscription. - An existing Azure SQL Managed Instance with a system-assigned identity.
- For a customer-managed key: a Key Vault or Managed HSM with purge protection enabled, a supported key, and
get/wrapKey/unwrapKeygranted to the instance's identity. - A versioned key identifier unless
auto_rotation_enabledistrue. - The caller configures
provider "azurerm" { features {} }, auth and subscription.
terraform-azurerm-mssql-managed-instance-transparent-data-encryption/
βββ providers.tf # required_version + pinned azurerm; no provider block
βββ variables.tf # 5 inputs, deeply typed, with the key-pairing and version rules
βββ main.tf # the single keystone `this`
βββ outputs.tf # id first, then the derived key facts, then the posture facts
βββ README.md # this file
βββ SCOPE.md # the cross-module contract
βββ LICENSE # MIT
βββ .gitignore
provider "azurerm" {
features {}
}
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
}That is service-managed encryption, and it is already secure β see example 1. Reach for a customer-managed key only when you want the control, and read example 3 before you do.
Consumes
| Input | Type | Source |
|---|---|---|
managed_instance_id |
Resource ID | terraform-azurerm-mssql-managed-instance β id |
key_vault_key_id |
key identifier | terraform-azurerm-key-vault β key_ids[β¦] or key_versionless_ids[β¦] |
managed_hsm_key_id |
key identifier | a Managed HSM; mutually exclusive with the above |
Emits
| Output | Description |
|---|---|
id |
Resource ID of the encryption protector (first) |
key_kind |
service_managed, key_vault or managed_hsm |
managed_instance_key_id |
the untracked side-effect record |
vault_name / key_name / key_version |
parsed from the key identifier |
the_configured_key_is_versioned |
the pairing check against auto-rotation |
losing_the_key_makes_the_instance_unreadable |
constant true |
1 Β· The empty call is already encrypted
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
}
output "key" {
value = module.mi_tde.key_kind # "service_managed"
}β Omitting a key does not turn encryption off. Transparent data encryption stays on with a Microsoft-managed key. That is why nothing in this module inverts a provider default β unlike its neighbours in this family, the safe configuration is already the empty one.
2 Β· A Key Vault key, versioned
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
key_vault_key_id = module.tde_vault.key_ids["tde"]
}βΉοΈ
key_idsfrom the key-vault module are versioned identifiers, which is exactly whatauto_rotation_enabled = falseβ the default β requires.
β οΈ Rotating the key in the vault now means editing this configuration. Example 4 avoids that.
3 Β· What a customer-managed key actually buys, and costs
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
key_vault_key_id = module.tde_vault.key_ids["tde"]
}
output "risk" {
value = {
cmk_in_use = module.mi_tde.this_instance_uses_a_customer_managed_key
vault_to_watch = module.mi_tde.vault_name
}
}π΄ The data was already encrypted before this. What changes is who holds the key β and therefore who can render the instance unreadable by deleting it, disabling it, letting it expire, or removing the instance's access to the vault. No Terraform plan predicts any of that.
π Purge protection on the vault is not optional here. It, and a soft-delete retention long enough to notice, are what stand between an accident and a permanent one.
4 Β· Versionless plus auto-rotation, so rotation is not a change request
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
key_vault_key_id = module.tde_vault.key_versionless_ids["tde"]
auto_rotation_enabled = true
}π‘ This is usually the better posture. The instance follows the key as it rotates in the vault, so a rotation stops being a Terraform apply.
βΉοΈ The provider resolves the current version at apply and suppresses the resulting diff β but only when
auto_rotation_enabledis true, which is why the two must be set together.
5 Β· The rule this module moves from apply to validate
# REFUSED at plan:
# key_vault_key_id = module.tde_vault.key_versionless_ids["tde"]
# auto_rotation_enabled = false # <-- versionless key, rotation offπ΄ The provider raises this during APPLY, because it only discovers the problem while resolving the key against the vault: "
key_vault_key_idmust be a versioned Key Vault Key ID whenauto_rotation_enabledis false".β This module enforces the same rule offline, at
terraform planβ offline, in seconds, before anything is touched. Either add the version or setauto_rotation_enabled = true.
6 Β· A Managed HSM key
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
managed_hsm_key_id = var.hsm_tde_key_id
}
output "kind" {
value = module.mi_tde.key_kind # "managed_hsm"
}
β οΈ State will showkey_vault_key_idpopulated too, with the same value. A Managed HSM key identifier is also a valid key identifier, so the provider's read sets both and suppresses the diff. That is expected, not drift.βΉοΈ This argument does not exist on the provider's 5.0 line, where
key_vault_key_idis expected to carry both kinds. This library pins~> 4.0deliberately, so it works today and will need revisiting later.
7 Β· The two key arguments conflict
# REFUSED at plan by this module, and by the provider:
# key_vault_key_id = module.tde_vault.key_ids["tde"]
# managed_hsm_key_id = var.hsm_tde_key_id
β οΈ The conflict is invisible in the binary schema β it lives only in the provider's Go source. This module refuses the combination at plan with a message naming which one to keep, rather than deferring to a generic conflict error.
8 Β· The record Terraform creates and does not track
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
key_vault_key_id = module.tde_vault.key_ids["tde"]
}
output "untracked_record" {
value = {
name = module.mi_tde.managed_instance_key_name # "kv-tde_tde-key_abc123..."
id = module.mi_tde.managed_instance_key_id
}
}π΄ When a key is supplied the provider creates a managed instance KEY record β
{vaultName}_{keyName}_{keyVersion}β and points the protector at it. Terraform does not manage that record as a separate resource, so it appears in no plan and is destroyed by nothing.π‘ Its name and ID are recomputed here so that something names it. They are
nullwhen the key is versionless, because the version is only resolved at apply and cannot honestly be known offline.
9 Β· Auto-rotation with no key governs nothing
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
auto_rotation_enabled = true
# no key -- so this setting applies to nothing
}
output "inert" {
value = module.mi_tde.auto_rotation_setting_governs_nothing_here # true
}βΉοΈ Reported, not refused. A service-managed key is rotated by Microsoft on its own schedule, so the setting is accepted and simply does not apply. Setting it ahead of supplying a key is harmless, and refusing it would reject a legal configuration.
10 Β· Moving from service-managed to customer-managed, and back
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
# Add a key to take control; remove it to hand control back.
key_vault_key_id = var.take_key_control ? module.tde_vault.key_ids["tde"] : null
}βΉοΈ Only
managed_instance_idis force-new, so switching between a service-managed and a customer-managed key is an in-place update, not a replacement. Nothing is re-encrypted from scratch β the protector changes, not the data.π΄ And removing the module entirely does the same thing as setting the key to null. Destroy reverts to a service-managed key; it does not decrypt anything and does not leave the instance unprotected.
11 Β· The timeout that does nothing
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
key_vault_key_id = module.tde_vault.key_ids["tde"]
timeouts = {
create = "45m" # governs create, update AND the so-called delete
delete = "45m" # accepted, stored, never consulted
}
}π΄ The provider's delete function reads the create/update deadline, which is consistent with what it does β the delete is really an update back to a service-managed key.
timeouts.deleteis therefore accepted and has no effect. Settimeouts.create.
12 Β· Several instances sharing one key
module "mi_tde" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-mssql-managed-instance-transparent-data-encryption.git?ref=v1.0.0"
for_each = module.managed_instances
managed_instance_id = each.value.id
key_vault_key_id = module.tde_vault.key_versionless_ids["tde"]
auto_rotation_enabled = true
}
output "instances_on_a_customer_managed_key" {
value = [
for k, m in module.mi_tde : k
if m.this_instance_uses_a_customer_managed_key
]
}π΄ One key protecting several instances means one deletion can lock all of them. That is a legitimate design β it centralises key control β but the blast radius should be a decision, not an accident.
π‘ Each instance still needs its own identity granted
wrapKeyandunwrapKeyon that key.
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 "tde_vault" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-key-vault.git?ref=v1.0.0"
name = "kv-sqlmi-tde"
resource_group_name = "rg-data-platform"
location = "eastus2"
tenant_id = data.azurerm_client_config.current.tenant_id
keys = {
tde = {
key_type = "RSA"
key_size = 2048
key_opts = ["unwrapKey", "wrapKey"]
}
}
}
# The instance's own identity is what wraps and unwraps -- not Terraform's.
resource "azurerm_role_assignment" "mi_uses_tde_key" {
scope = module.tde_vault.id
role_definition_name = "Key Vault Crypto Service Encryption User"
principal_id = module.sqlmi.identity_principal_id
}
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
key_vault_key_id = module.tde_vault.key_versionless_ids["tde"]
auto_rotation_enabled = true
depends_on = [azurerm_role_assignment.mi_uses_tde_key]
}
# The other identity control on the same instance.
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
}
output "encryption_posture" {
value = {
key_kind = module.mi_tde.key_kind
vault = module.mi_tde.vault_name
key = module.mi_tde.key_name
follows_rotation = module.mi_tde.auto_rotation_enabled
untracked_record = module.mi_tde.managed_instance_key_id
}
}π A versionless key with auto-rotation, so key rotation never becomes a Terraform change.
β οΈ Thedepends_onis deliberate: nothing in the protector references the role assignment, so without it Terraform may set the protector before the instance can wrap the key.π΄
kv-sqlmi-tdeis now load-bearing. Deleting that vault or that key makessqlmi-platform-eus2unreadable, and purge protection is what makes the mistake recoverable.
Identity β managed_instance_id (required, force-new).
Key β key_vault_key_id, managed_hsm_key_id (at most one; neither means service-managed).
Rotation β auto_rotation_enabled.
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. |
key_vault_key_id |
string |
null |
A key identifier. Conflicts with managed_hsm_key_id. Versioned unless rotation is on. |
managed_hsm_key_id |
string |
null |
Same rules. Absent on the provider's 5.0 line. |
auto_rotation_enabled |
bool |
false |
The provider's default, kept β see Architecture Notes. |
timeouts |
object({create, read, update, delete}) |
null |
30/5/30/30. delete is ignored by the provider. |
| Output | Description |
|---|---|
id |
Resource ID of the encryption protector (first) |
managed_instance_id |
the parent, as configured |
managed_instance_name / resource_group_name / subscription_id |
parsed from the parent ID |
key_vault_key_id / managed_hsm_key_id / auto_rotation_enabled |
as the provider reports them |
key_kind |
service_managed, key_vault or managed_hsm |
vault_name / key_name / key_version |
parsed from the key identifier |
managed_instance_key_name / managed_instance_key_id |
the untracked side-effect record |
this_instance_uses_a_customer_managed_key |
true for CMK |
the_configured_key_is_versioned |
pairing check against auto-rotation |
auto_rotation_setting_governs_nothing_here |
true when set with no key |
the_empty_call_is_already_encrypted |
constant true |
a_customer_managed_key_is_a_control_transfer_not_an_upgrade |
constant true |
losing_the_key_makes_the_instance_unreadable |
constant true |
the_instance_needs_wrap_and_unwrap_on_the_vault |
constant true |
a_versionless_key_requires_auto_rotation |
constant true |
the_provider_creates_a_second_arm_record_it_does_not_track |
constant true |
destroying_this_resource_reverts_to_a_service_managed_key |
constant true |
timeouts_delete_is_accepted_and_ignored |
constant true |
creating_this_resource_overwrites_the_existing_protector |
constant true |
an_empty_string_is_not_the_same_as_an_unset_key |
constant true β why both key arguments are rendered through a normalizing local |
resolving_a_versionless_key_reads_the_key_as_you |
true for a versionless key: the apply reads it as your principal |
the_two_key_arguments_are_mutually_exclusive |
constant true |
state_may_show_both_key_fields_for_an_hsm_key |
constant true |
managed_hsm_key_id_does_not_exist_on_the_provider_five_zero_line |
constant true |
one_protector_per_managed_instance |
constant true |
force_new_fields / fields_that_can_change_after_creation |
lifecycle |
fields_azure_returns_on_read |
where drift is detectable |
no_secret_is_accepted_or_emitted_by_this_module |
constant true |
this_resource_supports_no_azure_resource_tags |
constant true |
π A key is referenced here by identifier, never carried. Nothing is marked sensitive, and that is correct β a key identifier is not a secret.
Nothing is inverted here, and the reasoning is worth stating. Two neighbouring modules in this family
diverge from provider defaults because those defaults make the feature do nothing. This one does not, for two
reasons. The empty call already produces the safe resource β no key means service-managed encryption, not
absent encryption. And auto_rotation_enabled cannot be flipped safely, because true and false demand
different key identifiers: defaulting it to true would silently change what counts as a legal key ID for
every caller, a worse trap than the one it would fix.
A customer-managed key is a control transfer. The data is encrypted either way. What changes is who holds the key, and therefore who can render the instance unreadable. That is a genuine control and a genuine operational responsibility, not a security upgrade on its own β and it makes vault delete rights into instance-destroy rights, which no Azure role on the SQL side limits.
One rule is moved forward. The provider raises "must be a versioned Key Vault Key ID when
auto_rotation_enabled is false" during apply, because it only discovers the problem while resolving the key
against the vault. The same rule is a pure shape check β does the identifier have a version segment? β so
this module enforces it at terraform plan, offline and in seconds. The error message says the module is mirroring
the provider rather than inventing a rule.
A second ARM record is created and never tracked. With a customer-managed key the provider creates a
managed instance key named {vaultName}_{keyName}_{keyVersion} and points the protector at it. It appears in
no plan and is destroyed by nothing. Its name and ID are recomputed and emitted β and are null under a
versionless identifier, because the version is only resolved at apply and guessing would be worse than
saying so.
Destroy reverts rather than deletes. An encryption protector cannot be removed, only switched between
customer-managed and service-managed. The provider's delete issues an update back to service-managed, which
its own source explains is to avoid an accidental lockout if someone removed the vault keys. So a
terraform destroy here hands key control back to Microsoft; it decrypts nothing and leaves no instance
unprotected. Consistent with that, timeouts.delete is ignored in favour of the create/update deadline.
Nothing is marked sensitive, deliberately. A key identifier is a URL, not key material. Marking it would break plan review while protecting nothing β and the honest control here is who can use the key, not who can read its name.
| Concern | Secure default (empty call) | Opt-out (caller must type it) |
|---|---|---|
| Encryption at rest | on, with a service-managed key β the empty call | supply a key to take control |
| Key control | Microsoft's | key_vault_key_id or managed_hsm_key_id |
| Two keys at once | refused at plan | not available |
| Versionless key without rotation | refused at plan β the provider's own rule, moved forward | not available |
| Rotation | false β the provider's default, kept because it decides what key IDs are legal |
set to true with a versionless key |
| Secret handling | none β a key is referenced, never carried | not available |
| Key lifecycle | not controlled by this module β purge protection is the caller's job | β |
terraform init -backend=false
terraform validate
terraform fmt -checkPin the module with ?ref=v1.0.0 β never a branch. This module is plan-only in this repository; a human
applies from CI.
terraform validate proves, offline and with no credentials: the type schemas, the anchored
managed_instance_id pattern (a logical server ID is refused), the key-identifier shape for both key
arguments (a secret or certificate identifier is refused), the mutual exclusion of the two key arguments,
and β the substantive one β the versioned-key rule that the provider only raises at apply.
Only terraform plan and apply β which need credentials β exercise: whether the key exists, whether the
instance's identity can wrap and unwrap it, whether the vault has purge protection, and what the current
version of a versionless key resolves to.
Nothing at either stage reports that destroying this resource reverts to a service-managed key, that a second
ARM record was created, or that timeouts.delete is ignored. That is what the constant outputs are for.
Note the asymmetry: a validation failure blocks terraform destroy as well as apply, which is why the
key checks mirror provider rules rather than inventing stricter ones.
id = "/subscriptions/.../resourceGroups/rg-data-platform/providers/Microsoft.Sql/managedInstances/sqlmi-platform-eus2/encryptionProtector/current"
key_kind = "key_vault"
vault_name = "kv-sqlmi-tde"
key_name = "tde"
key_version = null
auto_rotation_enabled = true
managed_instance_key_name = null
this_instance_uses_a_customer_managed_key = true
the_configured_key_is_versioned = false
force_new_fields = ["managed_instance_id"]
βΉοΈ
key_versionandmanaged_instance_key_namearenullhere because the configured identifier is versionless β the version is resolved at apply. With a versioned identifier both are populated.
| Symptom | Cause | Fix |
|---|---|---|
must be a VERSIONED key ID when auto_rotation_enabled is false |
a versionless identifier with rotation off | Add the version, or set auto_rotation_enabled = true. This is the provider's own rule, enforced earlier |
both key_vault_key_id and managed_hsm_key_id are set |
two key arguments | Keep the one matching where the key lives |
must be a Key Vault KEY identifier |
a secret or certificate identifier, or a Resource ID | Use the key URL β https://<vault>/keys/<name>[/<version>] |
managed_instance_id must be a SQL MANAGED INSTANCE Resource ID |
a logical server ID was passed | A logical server has its own encryption-protector resource |
| Apply fails saying the key cannot be accessed | the instance's identity lacks get/wrapKey/unwrapKey, or the vault lacks purge protection |
Grant the role to identity_principal_id and enable purge protection β example 13 |
State shows key_vault_key_id set when only managed_hsm_key_id was configured |
an HSM key identifier is also a valid key identifier; the read sets both | Expected, not drift |
| A plan wants to change the key every run | a versionless identifier with auto_rotation_enabled = false β the diff suppression only applies when rotation is on |
Set auto_rotation_enabled = true, or use a versioned identifier |
timeouts.delete had no effect |
the provider's delete reads the create/update deadline | Set timeouts.create |
terraform destroy left the instance encrypted |
a protector cannot be deleted, only switched back to service-managed | Expected, and deliberate β it prevents lockout |
| An existing protector configuration was replaced without warning | there is no requires-import guard, because the service always creates a protector | Expected. Reconcile the portal configuration into this module's inputs |
| Databases became inaccessible | the key was deleted, disabled, expired, or access was revoked | Restore the key from soft delete if purge protection allowed it; otherwise this is not recoverable from Terraform |
azurerm_mssql_managed_instance_transparent_data_encryption- Transparent data encryption with customer-managed keys
- Managed HSM keys for TDE
- Sibling modules:
terraform-azurerm-mssql-managed-instance,terraform-azurerm-mssql-managed-instance-active-directory-administrator,terraform-azurerm-mssql-managed-instance-security-alert-policy,terraform-azurerm-key-vault,terraform-azurerm-mssql-server - This module's
SCOPE.md
π "Infrastructure as Code should be standardized, consistent, and secure."