Skip to content

Latest commit

Β 

History

1 Commit

Folders and files

NameName
Last commit message
Last commit date
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

☁️ Azure Security Center Subscription Pricing Terraform Module

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).

Terraform azurerm Module Version Type Resources


🧩 Overview

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) or Free tier for a chosen resource_type.
  • πŸŽ›οΈ Optionally pins a plan subplan (for example Servers P2 or Storage DefenderForStorageV2).
  • 🧩 Optionally enables one or more extension blocks (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.

❀️ Support this project

If this module saves you time, please consider supporting its continued development:


πŸ—ΊοΈ Where this fits in the family

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
Loading

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.

🧬 What this module builds

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
Loading

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.

βœ… Provider / Versions

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):

  • subplan is 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.
  • extension requires the Standard tier. Extensions listed against a Free plan are ignored.
  • No tags, name, location, or resource_group_name. The resource is identified only by resource_type on the active subscription.
  • Legacy resource types. ContainerRegistry and KubernetesService are superseded by Containers; prefer the current value.

πŸ”‘ Required Azure RBAC Roles / Permissions

  • Security Admin on the target subscription β€” grants Microsoft.Security/pricings/read and Microsoft.Security/pricings/write, which is exactly what this module needs, or
  • a custom role scoped to the subscription carrying only Microsoft.Security/pricings/read and Microsoft.Security/pricings/write (least privilege). Owner/Contributor work but are broader than necessary.

Azure Prerequisites

  • The Microsoft.Security resource 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.

πŸ“ Module Structure

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

βš™οΈ Quick Start

# 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 set tier = "Free".


πŸ”Œ Cross-Module Contract

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).

πŸ“š Example Library

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" and tier = "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 the Standard tier. ⚠️ Changing subplan later 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_properties carries 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 SqlServerVirtualMachines for 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 = {}
  }
}

⚠️ Containers supersedes the legacy KubernetesService and ContainerRegistry resource 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 = {}
  }
}

πŸ’‘ CloudPosture is 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 to Free).

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.


πŸ“₯ Inputs

Grouped summary

  • Plan selection: resource_type (default "VirtualMachines"), tier (default "Standard").
  • Plan detail: subplan (default null, force-new).
  • Extensions: extensions (default {}, keyed by extension name).
  • Universal tail: timeouts (default null). No tags β€” 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
}

🧾 Outputs

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

🧠 Architecture Notes

  • Subscription-scoped singleton. The resource has no name/location/resource group/tags; it is identified solely by resource_type on the active subscription. There is one record per resource type β€” model the subscription's full posture as one module instance per plan (see the for_each example).
  • The keystone is named this. Per this module suite's single-primary-resource convention.
  • Secure by default. tier defaults to Standard, so an empty call enables Defender for Cloud. Turning protection off is an explicit tier = "Free".
  • subplan is 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 one dynamic "extension" block, with additional_extension_properties applied through try(..., null) so an omitted map renders as absent. Extensions are only meaningful on the Standard tier.
  • features {} dependence. The provider will not initialize without a caller-side provider "azurerm" { features {} } block; that belongs to the root module, not here.

🧱 Design Principles

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.

πŸš€ Runbook

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 / apply from CI against real credentials.
  • Ensure the provider targets the intended subscription before applying β€” this resource changes subscription-wide security posture.

πŸ§ͺ Testing

The offline proof gate is what CI runs before any human applies:

  • terraform init -backend=false β€” resolves the pinned azurerm ~> 4.0 provider without a backend.
  • terraform validate β€” proves the configuration is internally consistent and type-correct against the pinned provider schema, catching every typing mistake the object() schemas and validation {} blocks are designed to surface (illegal tier, unknown resource_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.

πŸ’¬ Example Output

$ 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",
]

πŸ” Troubleshooting

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.

πŸ”— Related Docs


πŸ’™ "Infrastructure as Code should be standardized, consistent, and secure."