
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
}
- 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