Terraform Set: Azure Front Door
(Standard or Premium)
Fronting an AKS NGINX Ingress (Public)
This Deployment Creates:
- Resource Group and Networking (VNet, Subnet)
- AKS Cluster (System Node Pool)
- Static Public IP for NGINX Ingress LoadBalancer
- NGINX Ingress Controller via Helm, Bound to the Static Public IP
- A sample Kubernetes Ingress Rule (Optional) to Prove Routing
- Azure Front Door Profile, Endpoint, Origin Group, Origin (Pointing to the NGINX Public IP), and Route
- Optional WAF Policy and Association
- Optional Diagnostics to Log Analytics
Notes you should know up Front:
- This Pattern uses a public NGINX ingress IP as the Front Door origin. It is the most common and simplest AKS + Front Door design.
- For Production Hardening, you typically restrict Direct Access to the Ingress Public IP (at least by WAF Rules, or by IP allowlisting, or by moving to Premium + Private Link Patterns depending on requirements). I include practical Hardening Guidance at the End.
- This is a complete “Single Apply” set, but the NGINX Service may take a few minutes to allocate and Bind the Static IP after the Cluster is ready.
Folder Layout:
Versions.tf
Providers.tf
Variables.tf
Main.tf
Outputs.tf
Terraform.tfvars (Example)
Versions.tf
terraform {
required_version = ">= 1.5.0"
required_providers {
azurerm = {
source = "hashicorp/azurerm"
version = ">= 3.80.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = ">= 2.25.0"
}
helm = {
source = "hashicorp/helm"
version = ">= 2.12.0"
}
}
}
Providers.tf
provider "azurerm" {
features {}
}
data "azurerm_client_config" "current" {}
provider "kubernetes" {
host = azurerm_kubernetes_cluster.aks.kube_config[0].host
client_certificate = base64decode(azurerm_kubernetes_cluster.aks.kube_config[0].client_certificate)
client_key = base64decode(azurerm_kubernetes_cluster.aks.kube_config[0].client_key)
cluster_ca_certificate = base64decode(azurerm_kubernetes_cluster.aks.kube_config[0].cluster_ca_certificate)
}
provider "helm" {
kubernetes {
host = azurerm_kubernetes_cluster.aks.kube_config[0].host
client_certificate = base64decode(azurerm_kubernetes_cluster.aks.kube_config[0].client_certificate)
client_key = base64decode(azurerm_kubernetes_cluster.aks.kube_config[0].client_key)
cluster_ca_certificate = base64decode(azurerm_kubernetes_cluster.aks.kube_config[0].cluster_ca_certificate)
}
}
Variables.tf
variable "resource_group_name" {
type = string
description = "Resource group name."
}
variable "location" {
type = string
description = "Azure region for AKS and network."
default = "eastus"
}
variable "vnet_name" {
type = string
default = "vnet-aks"
}
variable "vnet_cidr" {
type = string
default = "10.10.0.0/16"
}
variable "aks_subnet_name" {
type = string
default = "snet-aks"
}
variable "aks_subnet_cidr" {
type = string
default = "10.10.1.0/24"
}
variable "aks_name" {
type = string
description = "AKS cluster name."
}
variable "dns_prefix" {
type = string
description = "AKS DNS prefix."
}
variable "kubernetes_version" {
type = string
description = "AKS Kubernetes version. Keep empty to let Azure choose default."
default = ""
}
variable "system_node_vm_size" {
type = string
default = "Standard_D4s_v5"
}
variable "system_node_count" {
type = number
default = 3
}
variable "aks_sku_tier" {
type = string
default = "Standard"
}
variable "ingress_namespace" {
type = string
default = "ingress-nginx"
}
variable "ingress_public_ip_name" {
type = string
default = "pip-aks-ingress"
}
variable "ingress_controller_release_name" {
type = string
default = "ingress-nginx"
}
variable "app_namespace" {
type = string
default = "demo"
}
variable "app_host_name" {
type = string
description = "Host header that Front Door will send to the ingress. Use a domain your ingress rules expect, such as app.contoso.com."
}
variable "frontdoor_profile_name" {
type = string
description = "Front Door profile name."
}
variable "frontdoor_endpoint_name" {
type = string
description = "Front Door endpoint name."
}
variable "frontdoor_sku" {
type = string
description = "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
default = "og-aks"
}
variable "route_name" {
type = string
default = "route-aks"
}
variable "health_probe_path" {
type = string
default = "/healthz"
}
variable "waf_enabled" {
type = bool
default = true
}
variable "waf_policy_name" {
type = string
default = "waf-frontdoor"
}
variable "security_policy_name" {
type = string
default = "fd-security-policy"
}
variable "waf_mode" {
type = string
default = "Prevention"
validation {
condition = contains(["Detection", "Prevention"], var.waf_mode)
error_message = "waf_mode must be Detection or Prevention."
}
}
variable "diagnostics_enabled" {
type = bool
default = false
}
variable "log_analytics_workspace_id" {
type = string
default = null
description = "Required if diagnostics_enabled is true."
}
Main.tf
resource "azurerm_resource_group" "rg" {
name = var.resource_group_name
location = var.location
}
resource "azurerm_virtual_network" "vnet" {
name = var.vnet_name
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
address_space = [var.vnet_cidr]
}
resource "azurerm_subnet" "aks" {
name = var.aks_subnet_name
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = [var.aks_subnet_cidr]
}
resource "azurerm_kubernetes_cluster" "aks" {
name = var.aks_name
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
dns_prefix = var.dns_prefix
sku_tier = var.aks_sku_tier
kubernetes_version = var.kubernetes_version != "" ? var.kubernetes_version : null
identity {
type = "SystemAssigned"
}
default_node_pool {
name = "sysnp"
node_count = var.system_node_count
vm_size = var.system_node_vm_size
vnet_subnet_id = azurerm_subnet.aks.id
orchestrator_version = var.kubernetes_version != "" ? var.kubernetes_version : null
type = "VirtualMachineScaleSets"
}
network_profile {
network_plugin = "azure"
network_policy = "azure"
outbound_type = "loadBalancer"
}
oidc_issuer_enabled = true
workload_identity_enabled = true
}
resource "azurerm_role_assignment" "aks_rg_network_contributor" {
scope = azurerm_resource_group.rg.id
role_definition_name = "Network Contributor"
principal_id = azurerm_kubernetes_cluster.aks.identity[0].principal_id
}
resource "azurerm_public_ip" "ingress" {
name = var.ingress_public_ip_name
location = azurerm_resource_group.rg.location
resource_group_name = azurerm_resource_group.rg.name
allocation_method = "Static"
sku = "Standard"
}
resource "kubernetes_namespace" "ingress" {
metadata {
name = var.ingress_namespace
}
depends_on = [azurerm_kubernetes_cluster.aks]
}
resource "helm_release" "ingress_nginx" {
name = var.ingress_controller_release_name
namespace = kubernetes_namespace.ingress.metadata[0].name
repository = "https://kubernetes.github.io/ingress-nginx"
chart = "ingress-nginx"
version = "4.11.0"
set {
name = "controller.service.type"
value = "LoadBalancer"
}
set {
name = "controller.service.loadBalancerIP"
value = azurerm_public_ip.ingress.ip_address
}
set {
name = "controller.service.annotations.service\\.beta\\.kubernetes\\.io/azure-load-balancer-resource-group"
value = azurerm_resource_group.rg.name
}
set {
name = "controller.config.use-forwarded-headers"
value = "true"
}
set {
name = "controller.config.compute-full-forwarded-for"
value = "true"
}
set {
name = "controller.config.enable-real-ip"
value = "true"
}
depends_on = [
azurerm_role_assignment.aks_rg_network_contributor
]
}
resource "kubernetes_namespace" "app" {
metadata {
name = var.app_namespace
}
depends_on = [azurerm_kubernetes_cluster.aks]
}
resource "kubernetes_deployment" "demo" {
metadata {
name = "demo-web"
namespace = kubernetes_namespace.app.metadata[0].name
labels = {
app = "demo-web"
}
}
spec {
replicas = 2
selector {
match_labels = {
app = "demo-web"
}
}
template {
metadata {
labels = {
app = "demo-web"
}
}
spec {
container {
name = "demo-web"
image = "nginxdemos/hello:plain-text"
port {
container_port = 80
}
readiness_probe {
http_get {
path = "/"
port = 80
}
initial_delay_seconds = 5
period_seconds = 10
}
liveness_probe {
http_get {
path = "/"
port = 80
}
initial_delay_seconds = 15
period_seconds = 20
}
}
}
}
}
depends_on = [helm_release.ingress_nginx]
}
resource "kubernetes_service" "demo" {
metadata {
name = "demo-web-svc"
namespace = kubernetes_namespace.app.metadata[0].name
labels = {
app = "demo-web"
}
}
spec {
selector = {
app = "demo-web"
}
port {
port = 80
target_port = 80
}
type = "ClusterIP"
}
depends_on = [kubernetes_deployment.demo]
}
resource "kubernetes_ingress_v1" "demo" {
metadata {
name = "demo-web-ing"
namespace = kubernetes_namespace.app.metadata[0].name
annotations = {
"kubernetes.io/ingress.class" = "nginx"
}
}
spec {
rule {
host = var.app_host_name
http {
path {
path = "/"
path_type = "Prefix"
backend {
service {
name = kubernetes_service.demo.metadata[0].name
port {
number = 80
}
}
}
}
}
}
}
depends_on = [helm_release.ingress_nginx]
}
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 = false
load_balancing {
additional_latency_in_milliseconds = 0
sample_size = 4
successful_samples_required = 3
}
health_probe {
interval_in_seconds = 30
path = var.health_probe_path
protocol = "Http"
request_type = "GET"
}
}
resource "azurerm_cdn_frontdoor_origin" "fd_origin" {
name = "aks-nginx-ingress"
cdn_frontdoor_origin_group_id = azurerm_cdn_frontdoor_origin_group.fd_origin_group.id
enabled = true
host_name = azurerm_public_ip.ingress.ip_address
origin_host_header = var.app_host_name
http_port = 80
https_port = 443
priority = 1
weight = 1000
certificate_name_check_enabled = false
depends_on = [helm_release.ingress_nginx]
}
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 = [azurerm_cdn_frontdoor_origin.fd_origin.id]
enabled = true
forwarding_protocol = "HttpOnly"
https_redirect_enabled = true
patterns_to_match = ["/*"]
supported_protocols = ["Http", "Https"]
link_to_default_domain = true
cache {
query_string_caching_behavior = "IgnoreQueryString"
compression_enabled = true
content_types_to_compress = ["text/plain", "text/html", "text/css", "application/javascript", "application/json", "text/xml"]
}
}
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
managed_rule {
type = "Microsoft_DefaultRuleSet"
version = "2.1"
action = "Block"
}
}
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 = ["/*"]
domain {
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 = "fd-diag"
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
}
}
Outputs.tf
output "aks_name" {
value = azurerm_kubernetes_cluster.aks.name
}
output "ingress_public_ip" {
value = azurerm_public_ip.ingress.ip_address
}
output "frontdoor_default_hostname" {
value = azurerm_cdn_frontdoor_endpoint.fd_endpoint.host_name
}
output "frontdoor_profile_id" {
value = azurerm_cdn_frontdoor_profile.fd_profile.id
}
Example Terraform.tfvars
resource_group_name = "rg-aks-fd-prod"
location = "eastus"
aks_name = "aks-prod-01"
dns_prefix = "aksprod01"
system_node_vm_size = "Standard_D4s_v5"
system_node_count = 3
aks_sku_tier = "Standard"
app_host_name = "app.contoso.com"
frontdoor_profile_name = "fd-profile-prod"
frontdoor_endpoint_name = "fd-endpoint-prod"
frontdoor_sku = "Standard_AzureFrontDoor"
origin_group_name = "og-aks-prod"
route_name = "route-aks-prod"
health_probe_path = "/healthz"
waf_enabled = true
waf_policy_name = "waf-fd-prod"
security_policy_name = "fd-sec-prod"
waf_mode = "Prevention"
diagnostics_enabled = false
log_analytics_workspace_id = null
How to run
Terraform init
Terraform Validate
Terraform Plan
Terraform Apply
How traffic flows (what you must configure to actually reach your app)
Front Door receives the client request on the Front Door default hostname.
Front Door forwards the request to the origin host (the NGINX ingress public IP on port 80 in this example).
Front Door sends the Host header you set in app_host_name (example: app.contoso.com).
NGINX matches the Kubernetes Ingress rule for that host and routes to the demo service.
If you do not plan to use a real DNS name yet, you can still test by setting app_host_name to a placeholder domain and sending Host headers in your test client, but production should use a real domain.
Production Hardening Changes you should apply (recommended)
Use HTTPS from Front Door to NGINX
Terminate TLS at NGINX with a certificate (for App_Host_Name)
Change Front Door Origin Forwarding to HttpsOnly
Set certificate_name_check_enabled to True when your Origin is a DNS Name (not an IP)
For strict TLS Validation, use a DNS Name on the Origin (for example, a DNS A Record to the Ingress IP) and set Origin_Host_Header to that DNS Name or the Application Host Name, Depending on your TLS and Ingress Configuration
Restrict Direct Access to the Ingress Public IP
If you must stay Public, use NGINX allowlist rules and WAF, and consider Azure Firewall or other Perimeter Controls
If you can move to Private Origins, use Front Door Premium Design Patterns (Private Link / Private Ingress Exposure) depending on your Architecture Constraints
- Health Probe Endpoint
1. Ensure /Healthz Responds quickly and Consistently
2. Prefer a dedicated Service Endpoint that does not depend on Slow Downstream Dependencies
- If you want, I can also provide the “Production” variant for AKS where
- NGINX Ingress is Internal Only
- Front Door Premium reaches the origin through a Private Connectivity Pattern
- End-to-end TLS is enforced with origin Certificate Validation
- Origin access is fully Locked Down so the Application is not reachable except through Front Door

If you would like to explore this topic in greater depth, see my book Azure Front Door Design Security Performance and Global Application Delivery, 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