Azure Front Door Terraform Deployment

 

Azure Front Door Terraform Deployment

This example deploys Azure Front Door (Standard or Premium) using the modern Azure Front Door Resource Model under the Azure CDN Provider Namespace in AzureRM (the resources named cdn_frontdoor_*).

1.  It includes:

  • Front Door Profile
  • Endpoint
  • Origin Group with Health Probe and Load Balancing Settings
  • One or More Origins
  • Route with HTTPS Enforcement
  • Optional Custom Domain with Azure-Managed Certificate
  • Optional WAF Policy and Association to the Custom Domain or Endpoint via Security Policy
  • Optional Diagnostic Settings to Log Analytics

2.      Folder Layout

  • main.tf
  • variables.tf
  • outputs.tf

3.      Main.tf

terraform {
  required_version = ">= 1.5.0"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">= 3.70.0"
    }
  }
}

provider "azurerm" {
  features {}
}

resource "azurerm_resource_group" "rg" {
  name     = var.resource_group_name
  location = var.resource_group_location
}

resource "azurerm_cdn_frontdoor_profile" "fd_profile" {
  name                = var.frontdoor_profile_name
  resource_group_name = azurerm_resource_group.rg.name
  sku_name            = var.frontdoor_sku
}

resource "azurerm_cdn_frontdoor_endpoint" "fd_endpoint" {
  name                     = var.frontdoor_endpoint_name
  cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.fd_profile.id
  enabled                  = true
}

resource "azurerm_cdn_frontdoor_origin_group" "fd_origin_group" {
  name                     = var.origin_group_name
  cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.fd_profile.id

  session_affinity_enabled = var.session_affinity_enabled

  load_balancing {
    additional_latency_in_milliseconds = var.lb_additional_latency_ms
    sample_size                        = var.lb_sample_size
    successful_samples_required         = var.lb_successful_samples_required
  }

  health_probe {
    interval_in_seconds = var.health_probe_interval_seconds
    path                = var.health_probe_path
    protocol            = var.health_probe_protocol
    request_type        = var.health_probe_request_type
  }
}

resource "azurerm_cdn_frontdoor_origin" "fd_origin" {
  for_each = { for o in var.origins : o.name => o }

  name                          = each.value.name
  cdn_frontdoor_origin_group_id = azurerm_cdn_frontdoor_origin_group.fd_origin_group.id

  enabled                        = true
  host_name                      = each.value.host_name
  http_port                      = each.value.http_port
  https_port                     = each.value.https_port
  origin_host_header             = each.value.origin_host_header
  priority                       = each.value.priority
  weight                         = each.value.weight
  certificate_name_check_enabled = each.value.certificate_name_check_enabled
}

resource "azurerm_cdn_frontdoor_route" "fd_route" {
  name                          = var.route_name
  cdn_frontdoor_endpoint_id     = azurerm_cdn_frontdoor_endpoint.fd_endpoint.id
  cdn_frontdoor_origin_group_id = azurerm_cdn_frontdoor_origin_group.fd_origin_group.id
  cdn_frontdoor_origin_ids      = [for o in azurerm_cdn_frontdoor_origin.fd_origin : o.id]

  enabled                = true
  forwarding_protocol    = var.route_forwarding_protocol
  https_redirect_enabled = var.route_https_redirect_enabled
  patterns_to_match      = var.route_patterns_to_match
  supported_protocols    = var.route_supported_protocols
  link_to_default_domain = var.route_link_to_default_domain

  cache {
    query_string_caching_behavior = var.cache_query_string_behavior
    compression_enabled           = var.cache_compression_enabled
    content_types_to_compress     = var.cache_content_types_to_compress
  }
}

resource "azurerm_cdn_frontdoor_custom_domain" "fd_custom_domain" {
  count                    = var.custom_domain_enabled ? 1 : 0
  name                     = var.custom_domain_resource_name
  cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.fd_profile.id
  host_name                = var.custom_domain_host_name

  tls {
    certificate_type    = "ManagedCertificate"
    minimum_tls_version = var.custom_domain_minimum_tls_version
  }
}

resource "azurerm_cdn_frontdoor_custom_domain_association" "fd_custom_domain_assoc" {
  count = var.custom_domain_enabled ? 1 : 0

  cdn_frontdoor_custom_domain_id = azurerm_cdn_frontdoor_custom_domain.fd_custom_domain[0].id
  cdn_frontdoor_route_ids        = [azurerm_cdn_frontdoor_route.fd_route.id]
}

resource "azurerm_cdn_frontdoor_firewall_policy" "fd_waf" {
  count               = var.waf_enabled ? 1 : 0
  name                = var.waf_policy_name
  resource_group_name = azurerm_resource_group.rg.name
  sku_name            = var.frontdoor_sku

  enabled                     = true
  mode                        = var.waf_mode
  redirect_url                = null
  custom_block_response_status_code = var.waf_custom_block_status_code
  custom_block_response_body        = var.waf_custom_block_body_base64

  managed_rule {
    type    = var.waf_managed_rule_type
    version = var.waf_managed_rule_version

    action = var.waf_managed_rule_action
  }

  dynamic "custom_rule" {
    for_each = var.waf_custom_rules
    content {
      name                           = custom_rule.value.name
      enabled                        = custom_rule.value.enabled
      priority                       = custom_rule.value.priority
      rate_limit_duration_in_minutes = custom_rule.value.rate_limit_duration_in_minutes
      rate_limit_threshold           = custom_rule.value.rate_limit_threshold
      type                           = custom_rule.value.type
      action                         = custom_rule.value.action

      match_condition {
        match_variable     = custom_rule.value.match_variable
        operator           = custom_rule.value.operator
        match_values       = custom_rule.value.match_values
        transforms         = custom_rule.value.transforms
        negation_condition = custom_rule.value.negation_condition
      }
    }
  }
}

resource "azurerm_cdn_frontdoor_security_policy" "fd_security_policy" {
  count                    = var.waf_enabled ? 1 : 0
  name                     = var.security_policy_name
  cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.fd_profile.id

  security_policies {
    firewall {
      cdn_frontdoor_firewall_policy_id = azurerm_cdn_frontdoor_firewall_policy.fd_waf[0].id

      association {
        patterns_to_match = var.waf_association_patterns_to_match

        dynamic "domain" {
          for_each = var.custom_domain_enabled ? [1] : []
          content {
            cdn_frontdoor_domain_id = azurerm_cdn_frontdoor_custom_domain.fd_custom_domain[0].id
          }
        }

        dynamic "domain" {
          for_each = var.custom_domain_enabled ? [] : [1]
          content {
            cdn_frontdoor_domain_id = azurerm_cdn_frontdoor_endpoint.fd_endpoint.id
          }
        }
      }
    }
  }
}

resource "azurerm_monitor_diagnostic_setting" "fd_diagnostics" {
  count                      = var.diagnostics_enabled ? 1 : 0
  name                       = var.diagnostics_name
  target_resource_id         = azurerm_cdn_frontdoor_profile.fd_profile.id
  log_analytics_workspace_id = var.log_analytics_workspace_id

  enabled_log {
    category = "FrontdoorAccessLog"
  }

  enabled_log {
    category = "FrontdoorWebApplicationFirewallLog"
  }

  metric {
    category = "AllMetrics"
    enabled  = true
  }
}

4.      Variables.tf

variable "resource_group_name" {
  type        = string
  description = "Resource group name for Front Door resources."
}

variable "resource_group_location" {
  type        = string
  description = "Azure region for the resource group. Front Door itself is global."
  default     = "eastus"
}

variable "frontdoor_profile_name" {
  type        = string
  description = "Front Door profile name."
}

variable "frontdoor_endpoint_name" {
  type        = string
  description = "Front Door endpoint name (becomes part of the default host)."
}

variable "frontdoor_sku" {
  type        = string
  description = "Front Door SKU. Use Standard_AzureFrontDoor or Premium_AzureFrontDoor."
  default     = "Standard_AzureFrontDoor"

  validation {
    condition     = contains(["Standard_AzureFrontDoor", "Premium_AzureFrontDoor"], var.frontdoor_sku)
    error_message = "frontdoor_sku must be Standard_AzureFrontDoor or Premium_AzureFrontDoor."
  }
}

variable "origin_group_name" {
  type        = string
  description = "Origin group name."
}

variable "session_affinity_enabled" {
  type        = bool
  description = "Enable session affinity on the origin group."
  default     = false
}

variable "lb_additional_latency_ms" {
  type        = number
  description = "Extra latency added for load balancing decisions."
  default     = 0
}

variable "lb_sample_size" {
  type        = number
  description = "Number of health probe samples considered."
  default     = 4
}

variable "lb_successful_samples_required" {
  type        = number
  description = "Minimum number of successful probe samples for a healthy origin."
  default     = 3
}

variable "health_probe_interval_seconds" {
  type        = number
  description = "Health probe interval in seconds."
  default     = 30
}

variable "health_probe_path" {
  type        = string
  description = "Health probe path."
  default     = "/health"
}

variable "health_probe_protocol" {
  type        = string
  description = "Health probe protocol. Http or Https."
  default     = "Https"

  validation {
    condition     = contains(["Http", "Https"], var.health_probe_protocol)
    error_message = "health_probe_protocol must be Http or Https."
  }
}

variable "health_probe_request_type" {
  type        = string
  description = "Health probe request type. GET or HEAD."
  default     = "GET"

  validation {
    condition     = contains(["GET", "HEAD"], var.health_probe_request_type)
    error_message = "health_probe_request_type must be GET or HEAD."
  }
}

variable "origins" {
  type = list(object({
    name                          = string
    host_name                      = string
    http_port                      = number
    https_port                     = number
    origin_host_header             = string
    priority                       = number
    weight                         = number
    certificate_name_check_enabled = bool
  }))
  description = "List of origins for the origin group."
}

variable "route_name" {
  type        = string
  description = "Route name."
}

variable "route_forwarding_protocol" {
  type        = string
  description = "Forwarding protocol. HttpOnly, HttpsOnly, MatchRequest."
  default     = "HttpsOnly"

  validation {
    condition     = contains(["HttpOnly", "HttpsOnly", "MatchRequest"], var.route_forwarding_protocol)
    error_message = "route_forwarding_protocol must be HttpOnly, HttpsOnly, or MatchRequest."
  }
}

variable "route_https_redirect_enabled" {
  type        = bool
  description = "Redirect HTTP to HTTPS at the edge."
  default     = true
}

variable "route_patterns_to_match" {
  type        = list(string)
  description = "Path patterns to match."
  default     = ["/*"]
}

variable "route_supported_protocols" {
  type        = list(string)
  description = "Supported protocols. Http and or Https."
  default     = ["Http", "Https"]
}

variable "route_link_to_default_domain" {
  type        = bool
  description = "Expose route on the default azurefd.net domain."
  default     = true
}

variable "cache_query_string_behavior" {
  type        = string
  description = "Query string caching behavior. IgnoreQueryString, UseQueryString, or NotSet."
  default     = "IgnoreQueryString"

  validation {
    condition     = contains(["IgnoreQueryString", "UseQueryString", "NotSet"], var.cache_query_string_behavior)
    error_message = "cache_query_string_behavior must be IgnoreQueryString, UseQueryString, or NotSet."
  }
}

variable "cache_compression_enabled" {
  type        = bool
  description = "Enable compression at the edge."
  default     = true
}

variable "cache_content_types_to_compress" {
  type        = list(string)
  description = "Content types to compress when compression is enabled."
  default     = ["text/plain", "text/html", "text/css", "application/javascript", "application/json", "text/xml"]
}

variable "custom_domain_enabled" {
  type        = bool
  description = "Enable custom domain and managed certificate."
  default     = false
}

variable "custom_domain_resource_name" {
  type        = string
  description = "Resource name for the custom domain object."
  default     = "fd-custom-domain"
}

variable "custom_domain_host_name" {
  type        = string
  description = "Custom domain hostname, for example www.contoso.com."
  default     = ""
}

variable "custom_domain_minimum_tls_version" {
  type        = string
  description = "Minimum TLS version for the custom domain."
  default     = "TLS1_2"
}

variable "waf_enabled" {
  type        = bool
  description = "Enable WAF policy and security policy association."
  default     = false
}

variable "waf_policy_name" {
  type        = string
  description = "WAF policy name."
  default     = "fd-waf-policy"
}

variable "security_policy_name" {
  type        = string
  description = "Front Door security policy name."
  default     = "fd-security-policy"
}

variable "waf_mode" {
  type        = string
  description = "WAF mode. Detection or Prevention."
  default     = "Prevention"

  validation {
    condition     = contains(["Detection", "Prevention"], var.waf_mode)
    error_message = "waf_mode must be Detection or Prevention."
  }
}

variable "waf_custom_block_status_code" {
  type        = number
  description = "Custom block response status code."
  default     = 403
}

variable "waf_custom_block_body_base64" {
  type        = string
  description = "Optional base64-encoded response body for blocked requests. Leave empty to use default."
  default     = ""
}

variable "waf_managed_rule_type" {
  type        = string
  description = "Managed rule set type."
  default     = "Microsoft_DefaultRuleSet"
}

variable "waf_managed_rule_version" {
  type        = string
  description = "Managed rule set version."
  default     = "2.1"
}

variable "waf_managed_rule_action" {
  type        = string
  description = "Action for managed rule set. Block, Log, or Redirect."
  default     = "Block"
}

variable "waf_custom_rules" {
  type = list(object({
    name                           = string
    enabled                        = bool
    priority                       = number
    rate_limit_duration_in_minutes = number
    rate_limit_threshold           = number
    type                           = string
    action                         = string

    match_variable     = string
    operator           = string
    match_values       = list(string)
    transforms         = list(string)
    negation_condition = bool
  }))
  description = "Optional list of custom WAF rules."
  default     = []
}

variable "waf_association_patterns_to_match" {
  type        = list(string)
  description = "Patterns to apply WAF to, typically /*."
  default     = ["/*"]
}

variable "diagnostics_enabled" {
  type        = bool
  description = "Enable diagnostic settings on the Front Door profile."
  default     = false
}

variable "diagnostics_name" {
  type        = string
  description = "Diagnostic setting name."
  default     = "fd-diag"
}

variable "log_analytics_workspace_id" {
  type        = string
  description = "Log Analytics workspace ID for diagnostics."
  default     = null
}

  1. Outputs.tf

output "frontdoor_profile_id" {
  value = azurerm_cdn_frontdoor_profile.fd_profile.id
}

output "frontdoor_endpoint_host_name" {
  value = azurerm_cdn_frontdoor_endpoint.fd_endpoint.host_name
}

output "frontdoor_endpoint_id" {
  value = azurerm_cdn_frontdoor_endpoint.fd_endpoint.id
}

output "frontdoor_route_id" {
  value = azurerm_cdn_frontdoor_route.fd_route.id
}

output "custom_domain_host_name" {
  value = var.custom_domain_enabled ? azurerm_cdn_frontdoor_custom_domain.fd_custom_domain[0].host_name : null
}

output "waf_policy_id" {
  value = var.waf_enabled ? azurerm_cdn_frontdoor_firewall_policy.fd_waf[0].id : null
}

6.      Example terraform.tfvars

resource_group_name     = "rg-frontdoor-prod"
resource_group_location = "eastus"

frontdoor_profile_name  = "fd-profile-prod"
frontdoor_endpoint_name = "fd-endpoint-prod"

frontdoor_sku      = "Premium_AzureFrontDoor"
origin_group_name  = "og-app-prod"
route_name         = "route-app-prod"

session_affinity_enabled         = false
health_probe_path                = "/health"
health_probe_protocol            = "Https"
health_probe_request_type        = "GET"
health_probe_interval_seconds    = 30
lb_additional_latency_ms         = 0
lb_sample_size                   = 4
lb_successful_samples_required   = 3

origins = [
  {
    name                          = "origin-app-1"
    host_name                      = "myapp.azurewebsites.net"
    http_port                      = 80
    https_port                     = 443
    origin_host_header             = "myapp.azurewebsites.net"
    priority                       = 1
    weight                         = 1000
    certificate_name_check_enabled = true
  }
]

route_forwarding_protocol      = "HttpsOnly"
route_https_redirect_enabled   = true
route_patterns_to_match        = ["/*"]
route_supported_protocols      = ["Http", "Https"]
route_link_to_default_domain   = true

custom_domain_enabled              = false
custom_domain_resource_name        = "fd-customdomain-prod"
custom_domain_host_name            = "www.contoso.com"
custom_domain_minimum_tls_version  = "TLS1_2"

waf_enabled                = false
waf_policy_name            = "fd-waf-prod"
security_policy_name       = "fd-sec-prod"
waf_mode                   = "Prevention"
waf_custom_block_status_code = 403
waf_custom_block_body_base64  = ""

waf_custom_rules = []

diagnostics_enabled          = false
log_analytics_workspace_id   = null

7.      Deployment Steps

8.      Initialize - terraform init

9.      Validate - terraform validate

10.   Plan - terraform plan -out tfplan

11.   Apply - terraform apply tfplan

12.   Important operational notes

13.   Custom domain validation

  • Azure Front Door custom domains require DNS validation (typically a CNAME to the endpoint host).
  • Terraform can create the Azure resource objects, but DNS records must exist before the managed certificate will successfully provision.
  • If you manage DNS in Azure DNS, I can add an Azure DNS zone and records into this same Terraform deployment.

Premium and Private Link

  • If you need origins reachable only via Private Link, that is typically implemented with Front Door Premium plus Private Link origin configuration.
  • If you tell me your origin type (App Service, Storage static website, AKS ingress, Application Gateway, internal load balancer), I can provide the Premium Private Link origin configuration pattern for that exact backend.

Lock down origin access

  • Best practice is to restrict direct access to origins so only Front Door can reach them.
  • The exact method depends on origin type (App Service access restrictions, NSGs, App Gateway, etc.).

 

If you would like to explore this topic in greater depth, see my books Azure Front Door Design Security Performance and Global Application Delivery and Mastering Terraform: A Comprehensive Guide to Infrastructure as Code, where the subject is covered in much greater detail. The guide expands on the concepts discussed in this article with deeper architectural explanations, service capabilities, and step-by-step implementation using Azure Portal, Azure CLI, Terraform, and Bicep. It also includes real-world deployment, configuration, and troubleshooting scenarios designed for IT professionals, administrators, and cloud architects. All of my books include detailed architectural diagrams and practical deployment examples using PowerShell, Azure CLI, Terraform, and Bicep.

0 comments

Leave a comment

Please note, comments need to be approved before they are published.