Configures Advanced Threat Protection for an Azure SQL Managed Instance: which detections run, who is told when one fires, and whether a copy of the threat-detection records is written to storage you own.
The module's enabled default diverges from the provider's, deliberately. The provider declares
enabled with no default, so its zero value is false and a bare resource block creates a disabled alert
policy — a threat-detection record that detects nothing while appearing in state and in the portal. A module
named for a security alert policy should not ship one that is switched off, so true is the default here and
the caller types enabled = false to opt out.
That default has a bill attached, and the module says so rather than leaving it to an invoice: Advanced Threat Protection is part of Microsoft Defender for SQL, priced per node — the whole managed instance, not per database.
azurerm_mssql_managed_instance_security_alert_policy— a single resource namedthis.
- The managed instance itself — consumed as a name plus resource group, not an ID.
- The threat-detection storage account — consumed as a blob endpoint; the storage module owns it.
- The vulnerability assessment, which is the other half of Microsoft Defender for SQL and is owned by the sibling module. Both capabilities sit inside the same per-node purchase.
- The managed instance's identity and any role assignment on the storage account — not visible from this resource and not created here.
| Input | Type | Source module |
|---|---|---|
managed_instance_name |
string (name) |
terraform-azurerm-mssql-managed-instance → name |
resource_group_name |
string (name) |
the caller |
storage_endpoint |
string (https URL) |
terraform-azurerm-storage-account → primary_blob_endpoint |
storage_account_access_key |
string (sensitive) |
out of band |
| Permission | Scope | Why |
|---|---|---|
Microsoft.Sql/managedInstances/securityAlertPolicies/read |
the managed instance | plan and refresh |
Microsoft.Sql/managedInstances/securityAlertPolicies/write |
the managed instance | create, update and disable the policy |
SQL Security Manager |
the managed instance | Microsoft names this as the role for enabling and managing Defender for SQL settings |
- The Terraform principal needs no storage permission. It supplies an endpoint and optionally a key; the service does the writing.
- Reading this policy does not read a credential — the provider's read never queries Azure for the access key, so plan access here is not credential access.
- The
Microsoft.Sqlresource provider registered on the subscription. - An existing Azure SQL Managed Instance.
- A Microsoft Defender for SQL entitlement, billed per node. Enabling this policy is a purchase.
- For storage-backed records: an existing storage account whose blob endpoint is passed here, reachable from the instance, plus a credential or an identity grant.
- The caller configures the
provider "azurerm" { features {} }block, auth and subscription.
| Output | Description | Consumed by |
|---|---|---|
id |
Resource ID of the alert policy (first) | downstream references |
managed_instance_id |
the parent ID, derived by trimming this policy's own ID | terraform-azurerm-mssql-managed-instance-vulnerability-assessment → managed_instance_id |
enabled_alert_types |
the deny list stated positively | security review |
all_alert_types |
the provider's closed set of six | building a deny list against a checked reference |
this_policy_detects_nothing |
true when disabled or when every type is denied | security review |
alerts_reach_nobody_by_email |
true when no recipient is configured | operational review |
an_access_key_is_configured |
presence flag only — never the key | security review |
module_default_diverges_from_provider_default |
constant true | reading the module honestly |
advanced_threat_protection_is_billed_per_node |
constant true | cost review |
force_new_fields |
the parent reference | lifecycle review |
No plaintext secret is emitted.
Verified against the pinned provider schema and the provider's source at the pinned tag:
- 🔴 The provider's
enabledhas no default, so its zero value isfalse. This module defaults it totrue; the divergence is emitted as an output. - 🔴
disabled_alertsis a DENY list. An entry switches that detection off. The legal values are a closed, case-sensitive set of six enforced by the provider. - 🔴
terraform destroydoes not delete this policy — it disables it. The provider's delete sends a policy carrying only the disabled state, so the endpoint, recipients and retention it had are not preserved by that call. - 🔴 There is no import-already-exists guard on create. Applying over an existing policy silently overwrites it; a clean apply is not evidence that nothing was configured before.
- 🔴 The access key is never read back from Azure. The provider's read re-sets the field from state rather than querying the service, so drift on it is undetectable by construction.
⚠️ storage_endpointcarries only a non-empty check in the provider — no URL validation at all. The https and not-a-Resource-ID checks are this module's.⚠️ The parent is a name and a resource group here, while the sibling vulnerability assessment takes a singlemanaged_instance_id. Both come from the same provider release.⚠️ managed_instance_nameis validated with the logical SERVER name rule, borrowed from the mssql package: lowercase letters, digits and hyphens, no leading or trailing hyphen, at most 63 characters.- ℹ️
retention_daysgoverns storage only, and governs nothing when no endpoint is set. The provider's only check is that it is not negative; there is no documented upper bound. - ℹ️
resource_group_namecomes from the provider's shared schema, which carries force-new and normalisation. - ℹ️ No
tags, nolocation. Tag the managed instance instead. - All four timeouts are honoured here (30/5/30/30) — which is not true of the sibling.
- Per this module suite's single-primary-resource convention, the resource is named
this. enableddefaults totrue, diverging from the provider, because the empty call must produce the safe resource and a disabled alert policy is not it. The divergence and its cost are both emitted.email_account_admins_enabledkeeps the provider'sfalse, because who should receive a security alert is an organisational decision rather than a safe-versus-unsafe one.- The deny list's argument name is mirrored from the provider — a wrong value fails loudly against a
closed enum — and its complement is emitted positively as
enabled_alert_types, so a reviewer never has to invert a list. email_addressesrefuses only the obvious mistake (no@); no email regex is imposed, because one would reject legal addresses.- The parent ID is derived by trimming this policy's own Resource ID rather than assembled from a subscription looked up at plan time.