IaC Azure

Terraform Module: Azure DDoS Protection Plan — one plan, many VNets, predictable spend

Quick take — Reusable hashicorp/azurerm module for an Azure DDoS Network Protection Plan: provision the plan once, share its ID across VNets, and avoid the per-VNet billing trap. Inputs, outputs, and production patterns. New here? Jump to the Quickstart below to deploy it in minutes; read on for how it works and when to reach for it.

Quickstart (copy-paste)

Minimal, runnable configuration — drop this in a .tf file and fill in the "..." placeholders (each required input is commented):

provider "azurerm" {
  features {}
}

module "ddos_protection" {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-azure-ddos-protection?ref=v1.0.0"

  name                = "..."  # Name of the DDoS Network Protection Plan (3-80 chars). …
  location            = "..."  # Azure region for the plan resource. Associated VNets ma…
  resource_group_name = "..."  # Existing resource group (typically connectivity/hub) th…
}

Then terraform init && terraform apply. Every other input has a sensible default — see Inputs below to override behaviour.

What this module is

An Azure DDoS Network Protection Plan is the billable container that turns on Azure’s always-on, adaptive Layer 3/4 DDoS mitigation for the public IPs attached to a virtual network. You create one plan, then associate virtual networks with it; every public IP inside an associated VNet inherits volumetric and protocol attack mitigation, adaptive tuning, attack telemetry, traffic-flow logs, and access to the DDoS Rapid Response team.

The single most important thing to understand about this resource is its billing model, and it is exactly why you want a module. A DDoS Network Protection Plan has a flat monthly charge (roughly $2,944/month) that covers protection for up to 100 public IP addresses, plus overage per protected IP beyond that. That charge is per plan, not per VNet. The catastrophic anti-pattern — which is trivially easy to hit if every team owns its own networking Terraform — is teams each declaring their own azurerm_network_ddos_protection_plan, turning one ~$3k/month line item into five.

Wrapping the plan in one tightly-scoped module gives you a single, governed point of truth: the plan is created exactly once (typically in a connectivity/hub subscription), it exports a stable id, and every spoke VNet — in any subscription — references that one ID. The module keeps the plan’s naming, tags, and lifecycle consistent, and makes “are we accidentally paying for this twice?” answerable with a single terraform state list.

When to use it

Use this module when:

Reach for the DDoS IP Protection SKU instead (configured directly on the azurerm_public_ip, not via this plan) when you have only a handful of public IPs and want pay-per-IP economics rather than a flat plan fee. Skip DDoS protection plans entirely for purely private workloads with no public IPs — there is nothing for the plan to protect.

Module structure

terraform-module-azure-ddos-protection/
├── versions.tf      # provider + Terraform version pins
├── main.tf          # the DDoS Network Protection Plan
├── variables.tf     # var-driven inputs with validation
└── outputs.tf       # id/name + key attributes for downstream wiring

versions.tf

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 4.0"
    }
  }
}

main.tf

# A DDoS Network Protection Plan is a flat-fee, regional-resource-group-scoped
# object. It is created ONCE and shared by associating VNets with its id.
# The association itself lives on the virtual network resource
# (azurerm_virtual_network.ddos_protection_plan), NOT here — this module
# intentionally owns only the plan so the plan's lifecycle is decoupled
# from any single network.
resource "azurerm_network_ddos_protection_plan" "this" {
  name                = var.name
  location            = var.location
  resource_group_name = var.resource_group_name

  tags = var.tags
}

variables.tf

variable "name" {
  description = "Name of the DDoS Network Protection Plan. A subscription is limited to one plan per region, so encode the region/scope in the name."
  type        = string

  validation {
    condition     = can(regex("^[a-zA-Z0-9][a-zA-Z0-9._-]{1,78}[a-zA-Z0-9_]$", var.name))
    error_message = "name must be 3-80 chars, start with a letter or number, and contain only letters, numbers, periods, underscores or hyphens."
  }
}

variable "location" {
  description = "Azure region for the plan (e.g. \"westeurope\"). Associated VNets do NOT have to be in this region, but the plan resource itself is regional."
  type        = string

  validation {
    condition     = length(trimspace(var.location)) > 0
    error_message = "location must be a non-empty Azure region name."
  }
}

variable "resource_group_name" {
  description = "Name of an existing resource group to hold the plan. Typically a connectivity/hub resource group."
  type        = string

  validation {
    condition     = length(var.resource_group_name) >= 1 && length(var.resource_group_name) <= 90
    error_message = "resource_group_name must be between 1 and 90 characters."
  }
}

variable "tags" {
  description = "Tags applied to the plan. Strongly recommend a cost-center / owner tag because this resource carries a flat ~$2,944/month charge."
  type        = map(string)
  default     = {}

  validation {
    condition     = alltrue([for k in keys(var.tags) : length(k) > 0])
    error_message = "Tag keys must be non-empty strings."
  }
}

outputs.tf

output "id" {
  description = "Resource ID of the DDoS Network Protection Plan. Set this as ddos_protection_plan.id on every spoke azurerm_virtual_network you want protected."
  value       = azurerm_network_ddos_protection_plan.this.id
}

output "name" {
  description = "Name of the DDoS Network Protection Plan."
  value       = azurerm_network_ddos_protection_plan.this.name
}

output "resource_group_name" {
  description = "Resource group that contains the plan."
  value       = azurerm_network_ddos_protection_plan.this.resource_group_name
}

output "location" {
  description = "Region of the plan resource."
  value       = azurerm_network_ddos_protection_plan.this.location
}

output "virtual_network_ids" {
  description = "IDs of virtual networks currently associated with this plan (computed by Azure as VNets opt in)."
  value       = azurerm_network_ddos_protection_plan.this.virtual_network_ids
}

How to use it

Create the plan once in your connectivity subscription, then have each spoke VNet point its ddos_protection_plan block at the module’s id output.

module "ddos_protection_plan" {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-azure-ddos-protection?ref=v1.0.0"

  name                = "ddos-plan-weu-connectivity"
  location            = "westeurope"
  resource_group_name = azurerm_resource_group.connectivity.name

  tags = {
    environment = "shared"
    cost_center = "platform-networking"
    owner       = "cloud-platform-team"
    managed_by  = "terraform"
  }
}

# Downstream: a spoke VNet opts in by referencing the plan's id output.
# The enable_protection flag must be true for the association to take effect.
resource "azurerm_virtual_network" "spoke_prod" {
  name                = "vnet-prod-weu-01"
  location            = "westeurope"
  resource_group_name = azurerm_resource_group.spoke_prod.name
  address_space       = ["10.20.0.0/16"]

  ddos_protection_plan {
    id     = module.ddos_protection_plan.id
    enable = true
  }
}

With Terragrunt

Terragrunt keeps this module DRY across environments — define the backend and provider once in a root config, then a thin terragrunt.hcl per environment supplies only the inputs that differ.

1. Root configlive/terragrunt.hcl (inherited by every module):

remote_state {
  backend = "azurerm"
  generate = { path = "backend.tf", if_exists = "overwrite" }
  config = {
    # ...azurerm state bucket/container + key per path...
  }
}

2. Module configlive/prod/ddos_protection/terragrunt.hcl:

include "root" {
  path = find_in_parent_folders()
}

terraform {
  source = "git::https://dev.azure.com/teknohut/kloudvin/_git/terraform-modules//terraform-module-azure-ddos-protection?ref=v1.0.0"
}

inputs = {
  name = "..."
  location = "..."
  resource_group_name = "..."
}

3. Deploy one environment, or roll out all modules together:

cd live/prod/ddos_protection && terragrunt apply        # this module
terragrunt run-all apply                      # every module under live/prod

Why Terragrunt here: the backend and provider live in one place instead of being copy-pasted into every module; inputs is overridden per environment (dev / stage / prod) without forking the module; and run-all orchestrates dependencies across modules. Reach for it once you have more than one environment or more than a handful of modules — for a single stack, the plain Quickstart above is enough.

Inputs

Name Type Default Required Description
name string n/a Yes Name of the DDoS Network Protection Plan (3-80 chars). A subscription allows one plan per region, so encode region/scope in the name.
location string n/a Yes Azure region for the plan resource. Associated VNets may live in other regions.
resource_group_name string n/a Yes Existing resource group (typically connectivity/hub) that holds the plan.
tags map(string) {} No Tags applied to the plan; include cost-center/owner because of the flat monthly charge.

Outputs

Name Description
id Resource ID of the plan; assign to ddos_protection_plan.id on each protected VNet.
name Name of the DDoS Network Protection Plan.
resource_group_name Resource group containing the plan.
location Region of the plan resource.
virtual_network_ids IDs of VNets currently associated with the plan (computed by Azure).

Enterprise scenario

A retail bank running an Azure Landing Zone has its connectivity subscription deploy this module once to create ddos-plan-weu-connectivity. The plan’s id output is published to a Terraform remote state (or a shared tfvars/parameter), and all 40+ spoke VNets across the corp, online-banking, and payments management groups reference that single ID in their ddos_protection_plan blocks. The result: every internet-facing Application Gateway and Azure Firewall public IP in the estate is protected under one ~$3k/month plan instead of dozens, FinOps sees exactly one DDoS line item, and the security team can engage DDoS Rapid Response with a single supported plan during an attack.

Best practices

TerraformAzureDDoS Protection PlanModuleIaC
Need this built for real?

Vinod is a Senior Cloud Architect (22+ yrs) — available for Azure / AWS / GCP architecture, landing zones, and migrations.

Work with me

Comments

Keep Reading