Sets the Microsoft Defender for Cloud pricing tier for one resource type on the active subscription β targets
hashicorp/azurerm ~> 4.0, secure by default (Defender on).
This module manages a single Microsoft Defender for Cloud pricing record for one resource type on whichever subscription your provider is pointed at.
- π‘οΈ Selects the
Standard(paid Defender) orFreetier for a chosenresource_type. - ποΈ Optionally pins a plan
subplan(for example ServersP2or StorageDefenderForStorageV2). - π§© Optionally enables one or more
extensionblocks (agentless scanning, malware scanning, sensitive-data discovery, and similar), keyed by extension name. - π Operates at subscription scope β there is exactly one pricing record per resource type, so this resource carries no name, location, resource group, or tags.
π‘ Why it matters: Defender for Cloud is a subscription-wide security posture control. Enabling the right plans is the difference between an audited, threat-monitored subscription and a blind one. This module makes the enabled state explicit, reviewable, and version-controlled, and it defaults to protection on so an empty call never silently leaves a subscription unprotected.
If this module saves you time, please consider supporting its continued development:
- β Star the repository on GitHub
- π€ Connect on LinkedIn: linkedin.com/in/microsoftexpert
- β Buy me a coffee: buymeacoffee.com/microsoftexpert
flowchart TD
ra["terraform-azurerm-role-assignments"]
law["terraform-azurerm-log-analytics-workspace"]
mod["terraform-azurerm-security-center-subscription-pricing"]
sub["Azure Subscription (provider target)"]
vm["Protected: Virtual Machines / Servers"]
st["Protected: Storage Accounts"]
sql["Protected: SQL Servers"]
kv["Protected: Key Vaults"]
ra -->|"grants Security Admin at scope"| mod
law -->|"workspace id for extension properties"| mod
mod -->|"sets Defender plan per resource_type on"| sub
sub -->|"protects"| vm
sub -->|"protects"| st
sub -->|"protects"| sql
sub -->|"protects"| kv
style mod fill:#0078D4,color:#fff,stroke:#004578,stroke-width:2px
style sub fill:#004578,color:#fff,stroke:#002d4d,stroke-width:2px
style ra fill:#F5F7FA,color:#1b1f23,stroke:#c5ccd6
style law fill:#F5F7FA,color:#1b1f23,stroke:#c5ccd6
style vm fill:#F5F7FA,color:#1b1f23,stroke:#c5ccd6
style st fill:#F5F7FA,color:#1b1f23,stroke:#c5ccd6
style sql fill:#F5F7FA,color:#1b1f23,stroke:#c5ccd6
style kv fill:#F5F7FA,color:#1b1f23,stroke:#c5ccd6
This module sits directly on the subscription. A role-assignments sibling grants the identity the Security Admin role it needs; a log-analytics-workspace sibling can feed a workspace id into an extension property; every workload resource type on the subscription inherits the plan this module sets.
flowchart LR
rt["resource_type (plan selector)"]
tier["tier (Standard = Defender ON)"]
sp["subplan (force-new)"]
ext["extensions map"]
this["azurerm_security_center_subscription_pricing.this"]
oid["output: id"]
ort["output: resource_type"]
otier["output: tier"]
rt -->|"which plan"| this
tier -->|"on or off"| this
sp -->|"plan detail"| this
ext -->|"dynamic extension blocks"| this
this -->|"resource id first"| oid
this -->|"plan selector"| ort
this -->|"active tier"| otier
style this fill:#004578,color:#fff,stroke:#002d4d,stroke-width:2px
style rt fill:#0078D4,color:#fff,stroke:#004578
style tier fill:#0078D4,color:#fff,stroke:#004578
style sp fill:#0078D4,color:#fff,stroke:#004578
style ext fill:#0078D4,color:#fff,stroke:#004578
style oid fill:#F5F7FA,color:#1b1f23,stroke:#c5ccd6
style ort fill:#F5F7FA,color:#1b1f23,stroke:#c5ccd6
style otier fill:#F5F7FA,color:#1b1f23,stroke:#c5ccd6
Resource inventory
| Resource | Count | Role |
|---|---|---|
azurerm_security_center_subscription_pricing.this |
1 | The keystone Defender for Cloud pricing record for one resource type. |
extension (dynamic, nested) |
0..N | Optional plan extensions, rendered from the extensions map. |
timeouts (dynamic, nested) |
0..1 | Optional operation timeouts. |
| Requirement | Value |
|---|---|
| Terraform | >= 1.12.0 |
| azurerm provider | ~> 4.0 |
| Provider block | None in this module β the caller configures provider "azurerm" { features {} }, authentication, and the target subscription. |
Schema notes that bite (verified against the live provider schema):
subplanis force-new. Changing it destroys and recreates the pricing record.- Deleting the resource resets the tier to
Free. Removing this module from a configuration turns paid protection off for that resource type β it does not leave the previous plan in place. - One record per resource type per subscription (singleton). The resource ID ends in
/pricings/<resource_type>. Managing the same resource type both here and elsewhere (portal, policy, a second module instance) produces perpetual diffs. extensionrequires theStandardtier. Extensions listed against aFreeplan are ignored.- No
tags,name,location, orresource_group_name. The resource is identified only byresource_typeon the active subscription. - Legacy resource types.
ContainerRegistryandKubernetesServiceare superseded byContainers; prefer the current value.
Security Adminon the target subscription β grantsMicrosoft.Security/pricings/readandMicrosoft.Security/pricings/write, which is exactly what this module needs, or- a custom role scoped to the subscription carrying only
Microsoft.Security/pricings/readandMicrosoft.Security/pricings/write(least privilege).Owner/Contributorwork but are broader than necessary.
- The
Microsoft.Securityresource provider registered on the target subscription. - The caller's
provider "azurerm"is pointed at the subscription whose plans are being managed; multi-subscription topologies use aliased provider configurations, not a module variable. - The caller configures
provider "azurerm" { features {} }, authentication, and the subscription β the module declares none of these. - Any resource referenced by an extension (for example a Log Analytics workspace id passed through
additional_extension_properties) already exists and is consumed by id.
terraform-azurerm-security-center-subscription-pricing/
βββ providers.tf # required_version >= 1.12.0; azurerm ~> 4.0; no provider block
βββ variables.tf # resource_type, tier, subplan, extensions, timeouts (no tags)
βββ main.tf # keystone azurerm_security_center_subscription_pricing.this + dynamic blocks
βββ outputs.tf # id first, then resource_type, tier, subplan, extension_names
βββ README.md # this document
βββ SCOPE.md # the cross-module contract
βββ LICENSE # MIT
βββ .gitignore # canonical library ignore set
# The caller configures the provider, auth, and the target subscription.
provider "azurerm" {
features {}
# subscription_id / auth come from the environment or this block.
}
module "defender_servers" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-subscription-pricing.git?ref=v1.0.0"
# Empty-call equivalent: resource_type defaults to "VirtualMachines",
# tier defaults to "Standard" (Defender enabled).
}π The empty call enables the Standard (paid Defender) plan for VirtualMachines. To manage a different plan, set
resource_type. To turn protection off, you must explicitly settier = "Free".
Consumes
| Input | Type | Typical source |
|---|---|---|
resource_type |
string |
caller (which Defender plan to manage) |
tier |
string |
caller (Standard on / Free off) |
subplan |
string |
caller (Microsoft-supplied subplan value) |
extensions[*].additional_extension_properties |
map(string) |
terraform-azurerm-log-analytics-workspace (id), caller config |
Emits
| Output | Description |
|---|---|
id |
The pricing record Resource ID (/subscriptions/<sub>/providers/Microsoft.Security/pricings/<resource_type>). |
resource_type |
The resource type whose plan this instance manages. |
tier |
The active tier (Standard = enabled, Free = disabled). |
subplan |
The active subplan, or null for the plan default. |
extension_names |
The set of enabled extension names (empty when none). |
1 Β· Minimal call (secure default)
module "defender_servers" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-subscription-pricing.git?ref=v1.0.0"
}π With no inputs,
resource_type = "VirtualMachines"andtier = "Standard"β Defender for Servers is enabled.
2 Β· Defender for Servers, Plan 2, with extensions
module "defender_servers_p2" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-subscription-pricing.git?ref=v1.0.0"
resource_type = "VirtualMachines"
tier = "Standard"
subplan = "P2"
extensions = {
MdeDesignatedSubscription = {}
AgentlessVmScanning = {}
}
}π‘ Plan 2 (
P2) adds agentless scanning and Microsoft Defender for Endpoint integration. Extensions are only honored on theStandardtier.β οΈ Changingsubplanlater forces the pricing record to be recreated.
3 Β· Defender for Storage (V2) with malware scanning
module "defender_storage" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-subscription-pricing.git?ref=v1.0.0"
resource_type = "StorageAccounts"
tier = "Standard"
subplan = "DefenderForStorageV2"
extensions = {
OnUploadMalwareScanning = {
additional_extension_properties = {
CapGBPerMonthPerStorageAccount = "5000"
}
}
SensitiveDataDiscovery = {}
}
}π‘
additional_extension_propertiescarries the per-account monthly malware-scanning cap in GB.
4 Β· Defender for SQL Servers
module "defender_sql" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-subscription-pricing.git?ref=v1.0.0"
resource_type = "SqlServers"
tier = "Standard"
}βΉοΈ Covers Azure SQL logical servers. Use
SqlServerVirtualMachinesfor SQL running on VMs.
5 Β· Defender for Containers
module "defender_containers" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-subscription-pricing.git?ref=v1.0.0"
resource_type = "Containers"
tier = "Standard"
extensions = {
AgentlessDiscoveryForKubernetes = {}
ContainerRegistriesVulnerabilityAssessments = {}
}
}
β οΈ Containerssupersedes the legacyKubernetesServiceandContainerRegistryresource types β do not manage all three for the same workloads.
6 Β· Defender for Key Vault
module "defender_key_vault" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-subscription-pricing.git?ref=v1.0.0"
resource_type = "KeyVaults"
tier = "Standard"
}π Threat protection for Key Vault data-plane access, enabled subscription-wide.
7 Β· Defender for App Service
module "defender_app_service" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-subscription-pricing.git?ref=v1.0.0"
resource_type = "AppServices"
tier = "Standard"
}βΉοΈ Billed per App Service plan. This is a common place protection stays enabled after a subscription-level opt-out β manage it explicitly.
8 Β· Defender for Resource Manager (Arm)
module "defender_arm" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-subscription-pricing.git?ref=v1.0.0"
resource_type = "Arm"
tier = "Standard"
}π‘ Monitors control-plane operations against Azure Resource Manager for the subscription.
9 Β· Defender for DNS
module "defender_dns" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-subscription-pricing.git?ref=v1.0.0"
resource_type = "Dns"
tier = "Standard"
}βΉοΈ Detects suspicious DNS activity such as data exfiltration and communication with malicious domains.
10 Β· Defender for APIs
module "defender_apis" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-subscription-pricing.git?ref=v1.0.0"
resource_type = "Api"
tier = "Standard"
subplan = "P1"
}
β οΈ Subplan values are supplied by Microsoft and are plan-specific; confirm the current value for your tenant before pinning it.
11 Β· Defender CSPM (cloud security posture management)
module "defender_cspm" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-subscription-pricing.git?ref=v1.0.0"
resource_type = "CloudPosture"
tier = "Standard"
extensions = {
SensitiveDataDiscovery = {}
AgentlessDiscoveryForKubernetes = {}
}
}π‘
CloudPostureis the Defender CSPM plan; its value comes largely from the extensions it enables.
12 Β· Turn protection OFF (the explicit opt-out)
module "defender_servers_off" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-subscription-pricing.git?ref=v1.0.0"
resource_type = "VirtualMachines"
tier = "Free"
}
β οΈ tier = "Free"disables the paid Defender plan for VirtualMachines. This is the deliberate opt-out β the caller must type it. Deleting the resource entirely has the same effect (the tier resets toFree).
13 Β· Many plans at once via for_each
locals {
defender_plans = {
VirtualMachines = { tier = "Standard", subplan = "P2" }
StorageAccounts = { tier = "Standard", subplan = "DefenderForStorageV2" }
SqlServers = { tier = "Standard", subplan = null }
KeyVaults = { tier = "Standard", subplan = null }
Containers = { tier = "Standard", subplan = null }
Arm = { tier = "Standard", subplan = null }
Dns = { tier = "Standard", subplan = null }
AppServices = { tier = "Standard", subplan = null }
}
}
module "defender" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-subscription-pricing.git?ref=v1.0.0"
for_each = local.defender_plans
resource_type = each.key
tier = each.value.tier
subplan = each.value.subplan
}π‘ One module instance per resource type, keyed by the resource type name β adding or removing a plan never re-indexes the rest.
14 Β· ποΈ End-to-end composition
Enable Defender across the key plans on a subscription and wire a Log Analytics workspace id from a sibling module into a Storage malware-scanning extension property.
provider "azurerm" {
features {}
}
# Sibling: a Log Analytics workspace whose id feeds an extension property.
module "law" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-log-analytics-workspace.git?ref=v1.0.0"
name = "law-security-eastus"
resource_group_name = "rg-security"
location = "eastus"
}
locals {
# Plans that take no special extensions.
defender_simple = {
SqlServers = { subplan = null }
KeyVaults = { subplan = null }
Arm = { subplan = null }
Dns = { subplan = null }
AppServices = { subplan = null }
Containers = { subplan = null }
}
}
# Defender for Servers (P2) with agentless scanning.
module "defender_servers" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-subscription-pricing.git?ref=v1.0.0"
resource_type = "VirtualMachines"
tier = "Standard"
subplan = "P2"
extensions = {
AgentlessVmScanning = {}
MdeDesignatedSubscription = {}
}
}
# Defender for Storage (V2) β malware-scan results routed via the workspace id.
module "defender_storage" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-subscription-pricing.git?ref=v1.0.0"
resource_type = "StorageAccounts"
tier = "Standard"
subplan = "DefenderForStorageV2"
extensions = {
OnUploadMalwareScanning = {
additional_extension_properties = {
CapGBPerMonthPerStorageAccount = "5000"
}
}
SensitiveDataDiscovery = {}
}
}
# The remaining plans, one instance per resource type.
module "defender_simple" {
source = "git::https://github.com/microsoftexpert/terraform-azurerm-security-center-subscription-pricing.git?ref=v1.0.0"
for_each = local.defender_simple
resource_type = each.key
tier = "Standard"
subplan = each.value.subplan
}
output "storage_plan_id" {
value = module.defender_storage.id
}
output "workspace_id" {
value = module.law.id
}π Every key plan on the subscription is enabled explicitly. The workspace lives in its own module and is consumed by id β this module never creates cross-cutting infrastructure of its own.
Grouped summary
- Plan selection:
resource_type(default"VirtualMachines"),tier(default"Standard"). - Plan detail:
subplan(defaultnull, force-new). - Extensions:
extensions(default{}, keyed by extension name). - Universal tail:
timeouts(defaultnull). Notagsβ the resource type does not support it.
Full variable schemas
variable "resource_type" {
type = string
default = "VirtualMachines"
# One of: AI, Api, AppServices, ContainerRegistry, KeyVaults, KubernetesService,
# SqlServers, SqlServerVirtualMachines, StorageAccounts, VirtualMachines, Arm, Dns,
# OpenSourceRelationalDatabases, Containers, CosmosDbs, CloudPosture.
}
variable "tier" {
type = string
default = "Standard" # Standard = Defender enabled (secure default); Free = disabled.
# One of: Free, Standard.
}
variable "subplan" {
type = string
default = null # Microsoft-supplied, plan-specific. Changing it forces a new resource.
}
variable "extensions" {
type = map(object({
additional_extension_properties = optional(map(string))
}))
default = {}
# Map key = extension name. Only honored on the Standard tier.
}
variable "timeouts" {
type = object({
create = optional(string)
read = optional(string)
update = optional(string)
delete = optional(string)
})
default = null
}| Output | Description | Kind |
|---|---|---|
id |
The Azure Resource ID of the Defender for Cloud pricing record (/subscriptions//providers/Microsoft.Security/pricings/<resource_type>) | Passthrough |
resource_type |
The resource type whose Defender for Cloud plan is managed by this instance | Passthrough |
tier |
The active pricing tier (Standard = Defender enabled, Free = disabled) | Passthrough |
subplan |
The active pricing subplan, or null when the plan default is used | Passthrough |
extension_names |
The set of Defender extension names enabled on this plan (empty when none are configured) | Passthrough |
is_subscription_wide |
Constant true, and the first thing to understand here | Constant |
billed_whether_used |
Constant true | Constant |
enables_paid_protection |
True when tier is Standard, meaning Defender protection is ON and billing for this resource type across the subscription | Derived |
destroy_disables_protection |
Constant true, and it is the reverse of what a destroy usually means | Constant |
resource_type_is_part_of_the_id_but_not_force_new |
Constant true, and the sharpest trap on this resource | Constant |
is_a_singleton_per_resource_type |
Constant true | Constant |
applies_serially_across_the_subscription |
Constant true | Constant |
protected_resource_type |
The resource type this setting governs | Passthrough |
extension_count |
How many Defender extensions this setting enables | Passthrough |
unlisted_extensions_are_disabled |
Constant true | Constant |
subplan_change_replaces_the_setting |
Constant true | Constant |
- Subscription-scoped singleton. The resource has no name/location/resource group/tags; it is identified solely by
resource_typeon the active subscription. There is one record per resource type β model the subscription's full posture as one module instance per plan (see thefor_eachexample). - The keystone is named
this. Per this module suite's single-primary-resource convention. - Secure by default.
tierdefaults toStandard, so an empty call enables Defender for Cloud. Turning protection off is an explicittier = "Free". subplanis force-new. A change destroys and recreates the record; plan appropriately so a change does not leave a coverage gap.- Deletion resets to
Free. Removing the module disables the paid plan for that resource type rather than preserving the last state. - Extensions are a keyed map. The map key is used verbatim as the extension
name; each entry renders onedynamic "extension"block, withadditional_extension_propertiesapplied throughtry(..., null)so an omitted map renders as absent. Extensions are only meaningful on theStandardtier. features {}dependence. The provider will not initialize without a caller-sideprovider "azurerm" { features {} }block; that belongs to the root module, not here.
| Concern | Secure default (empty call) | Opt-out (caller must type it) |
|---|---|---|
| Defender for Cloud plan | tier = "Standard" (protection enabled) |
tier = "Free" (protection disabled) |
| Extensions | extensions = {} β none enabled unless requested |
list extensions explicitly |
| Subplan | subplan = null β plan default |
pin a Microsoft-supplied value |
| Secrets | none accepted or emitted | β |
A boolean- or enum-gated exposure defaults to the protected member; the caller must type the relaxation.
terraform init -backend=false
terraform validate
terraform fmt -check- Pin the module with
?ref=v1.0.0β never a moving branch. - This is plan-only during authoring; a human runs
terraform plan/applyfrom CI against real credentials. - Ensure the provider targets the intended subscription before applying β this resource changes subscription-wide security posture.
The offline proof gate is what CI runs before any human applies:
terraform init -backend=falseβ resolves the pinnedazurerm ~> 4.0provider without a backend.terraform validateβ proves the configuration is internally consistent and type-correct against the pinned provider schema, catching every typing mistake theobject()schemas andvalidation {}blocks are designed to surface (illegaltier, unknownresource_type).terraform fmt -checkβ enforces canonical formatting.
Neither validate nor fmt calls Azure. Only terraform plan (run by a human, against real credentials, from CI) exercises the Microsoft.Security API.
$ terraform output
id = "/subscriptions/00000000-0000-0000-0000-000000000000/providers/Microsoft.Security/pricings/StorageAccounts"
resource_type = "StorageAccounts"
tier = "Standard"
subplan = "DefenderForStorageV2"
extension_names = [
"OnUploadMalwareScanning",
"SensitiveDataDiscovery",
]| Symptom | Cause | Fix |
|---|---|---|
tier must be one of: Free, Standard. |
An unsupported tier value. |
Use "Standard" or "Free". |
resource_type must be one of: ... |
A typo or a value outside the legal set. | Use a current value from the list (prefer Containers over KubernetesService/ContainerRegistry). |
| Plan wants to replace the record | subplan changed (it is force-new). |
Expected; schedule the change to avoid a coverage gap. |
| Extensions appear to do nothing | The plan is on the Free tier. |
Extensions require tier = "Standard". |
| Perpetual diff on the same plan | The resource type is also managed elsewhere (portal, policy, another instance). | Manage each resource type in exactly one place. |
Authorization failed on apply |
The identity lacks Microsoft.Security/pricings/write. |
Assign Security Admin (or a custom role with the pricing actions) at the subscription. |
| Protection unexpectedly off after a refactor | The module was removed; deletion resets the tier to Free. |
Keep the module in the configuration to keep the plan enabled. |
- azurerm provider β
azurerm_security_center_subscription_pricing - Microsoft Defender for Cloud documentation
- Sibling modules:
terraform-azurerm-log-analytics-workspace,terraform-azurerm-role-assignments,terraform-azurerm-resource-group - This module's
SCOPE.md
π "Infrastructure as Code should be standardized, consistent, and secure."