This post walks through integrating Azure Application Gateway v2 (with WAF) and Azure API Management to deliver a secure, scalable, and enterprise-grade API front door. We’ll cover reference architectures, networking, certificates, Azure API Management policies, zero-trust patterns with private endpoints, and end-to-end automation using Terraform and Azure DevOps.
Why Application Gateway v2 + Azure API Management?
- Layer-7 routing with path-based rules, host headers, URL rewrites, and WAF protection (OWASP Core Rule Set).
- Azure API Management provides API abstraction, versioning, throttling, caching, JWT validation, and per-API policies.
- Combined, App Gateway becomes the internet-facing secure entry point and Azure API Management the control plane for API governance.
Scenario:
Internet → App Gateway (WAF) → Azure API Management (External) → Backends
Best when Azure API Management needs to be publicly reachable but protected by WAF and central routing.
[Client] ─HTTPS──> [App Gateway v2 (WAF)] ─HTTPS──> [Azure API Management (External)] ─> [Private/On-prem/Azure Backends]
Pros: Simple, fast to implement, WAF in front, supports CDN/Front Door chaining.
Cons: Azure API Management is public; additional steps required for IP allow-lists and mTLS.
Scenario2:
Internet → App Gateway (WAF) → Azure API Management (Internal) via Private Endpoint
Azure API Management is internal; only App Gateway is public. Zero-trust friendly.
[Client] ─HTTPS──> [App Gateway v2 (WAF, Public)] ─HTTPS──> [Private Endpoint] ─> [Azure API Management (Internal)] ─> [Backends]
Pros: Azure API Management is not exposed to the internet; traffic flows through App Gateway + Private Link.
Cons: Requires vNet planning, DNS, and App Gateway-to-Private Link name resolution.
Scenario3:
Azure API Management (External) → App Gateway (Internal) → Private Backends
Azure API Management is the public front door; App Gateway does L7 routing to internal services.
[Client] ─HTTPS──> [Azure API Management (External)] ─HTTPS──> [App Gateway (Internal/WAF)] ─> [Backends]
Pros: Privileged Identity Management security & governance is your internet front door.
Cons: More Azure API Management policy work; App Gateway must be reachable from Azure API Management.
Network & DNS design checklist:
- Virtual networks & subnets:
- App Gateway-Subnet (required dedicated subnet)
- Azure API Management-Subnet (for internal tier)
- Shared-services-Subnet for Bastion/jumpbox/logging
- Private Link: Enable Azure API Management private endpoint in Azure API Management-Subnet.
- Private DNS Zones: privatelink.azure-api.net for Azure API Management, custom zones for backends.
- Name resolution: App Gateway must resolve Azure API Management private FQDN via vNet DNS or Azure Private DNS.
- Firewall & NSGs: Restrict inbound/outbound; allow only required ports to Azure API Management, Key Vault, Log Analytics.
- Hybrid Connectivity: Site-to-site VPN or ExpressRoute for on-prem backends; consider Azure Firewall or NVA.
Certificates & TLS
- Custom Domains:
- App Gateway: api.contoso.com
- Azure API Management: gateway.contoso.com (with custom hostname on Azure API Management)
- TLS Ports: HTTPS 443 end-to-end; disable TLS 1.0/1.1.
- Cert storage: Use Azure Key Vault for SSL certs; integrate App Gateway & Azure API Management with Key Vault (managed identity).
- mTLS (Client Certs): Enforce on Azure API Management with policies; optionally on App Gateway via mutual auth for selected listeners.
WAF (Web Application Firewall) on App Gateway v2
- Modes: Detection vs prevention (recommend prevention once tuned).
- CRS: Start with 3.2; baseline exclusions for APIs (JSON payloads).
- Managed rules: Enable bot protection, set anomaly scoring, create exclusions for headers like Authorization.
- Logging: Send WAF logs to Log Analytics; build alerts for blocked requests spikes.
Azure API Management Policies – Common patterns
- Inbound: `validate-jwt`, `check-header`, `rate-limit`, `ip-filter`, `set-backend-service`
- Backend: `retry`, `forward-request` with mTLS
- Outbound: `set-header`, `find-and-replace`, `cache`
- Global vs API vs Operation: Keep global minimal; override at API/operation for precision.
- Dev, Test, Prod: Parameterize via named values and Key Vault references.
Example – JWT validation and rate limit:
xml
<policies>
<inbound>
<base />
<validate-jwt header-name="Authorization" failed-validation-httpcode="401">
<openid-config url="https://login.microsoftonline.com/<tenant-id>/v2.0/.well-known/openid-configuration" />
<audiences>
<audience>api://contoso-app-id</audience>
</audiences>
<issuers>
<issuer>https://sts.windows.net/<tenant-id>/</issuer>
</issuers>
</validate-jwt>
<rate-limit calls="100" renewal-period="60" />
</inbound>
<backend>
<forward-request />
</backend>
<outbound>
<base />
</outbound>
</policies>
## Terraform – Core Resources (App Gateway v2 + Azure API Management)
Note: Simplified example; parameterize for prod, add Key Vault integrations, diagnostics, and role assignments.
# Variables (example)
variable "location" { default = "eastus" }
variable "rg_name" { default = "rg-appgw-apim" }
variable "vnet_name" { default = "vnet-core" }
variable "appgw_subnet" { default = "snet-appgw" }
variable "apim_subnet" { default = "snet-apim" }
provider "azurerm" { features {} }
resource "azurerm_resource_group" "rg" {
name = var.rg_name
location = var.location
}
resource "azurerm_virtual_network" "vnet" {
name = var.vnet_name
location = var.location
resource_group_name = azurerm_resource_group.rg.name
address_space = ["10.10.0.0/16"]
}
resource "azurerm_subnet" "snet_appgw" {
name = var.appgw_subnet
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["10.10.1.0/24"]
}
resource "azurerm_subnet" "snet_apim" {
name = var.apim_subnet
resource_group_name = azurerm_resource_group.rg.name
virtual_network_name = azurerm_virtual_network.vnet.name
address_prefixes = ["10.10.2.0/24"]
}
# Public IP for App Gateway
resource "azurerm_public_ip" "appgw_pip" {
name = "pip-appgw"
resource_group_name = azurerm_resource_group.rg.name
location = var.location
allocation_method = "Static"
sku = "Standard"
}
# Application Gateway v2 (WAF)
resource "azurerm_application_gateway" "appgw" {
name = "agw-v2-waf"
resource_group_name = azurerm_resource_group.rg.name
location = var.location
sku {
name = "WAF_v2"
tier = "WAF_v2"
capacity = 2
}
gateway_ip_configuration {
name = "appgw-ipcfg"
subnet_id = azurerm_subnet.snet_appgw.id
}
frontend_port {
name = "https-port"
port = 443
}
frontend_ip_configuration {
name = "appgw-feip"
public_ip_address_id = azurerm_public_ip.appgw_pip.id
}
ssl_certificate {
name = "ssl-agw"
data = filebase64("certs/agw.pfx")
password = var.agw_pfx_password
}
http_listener {
name = "listener-https"
frontend_ip_configuration_name = "appgw-feip"
frontend_port_name = "https-port"
protocol = "Https"
ssl_certificate_name = "ssl-agw"
host_name = "api.contoso.com"
}
backend_address_pool {
name = "apim-bepool"
# For Azure API Management private endpoint, use FQDN via custom probe, or IP when static
fqdns = ["gateway.contoso.internal"]
}
backend_http_settings {
name = "https-settings"
cookie_based_affinity = "Disabled"
port = 443
protocol = "Https"
pick_host_name_from_backend_address = true
request_timeout = 30
probe_name = "apim-probe"
}
probe {
name = "apim-probe"
protocol = "Https"
path = "/status-0123456789abcdef"
pick_host_name_from_backend_http_settings = true
interval = 30
timeout = 30
unhealthy_threshold = 3
}
request_routing_rule {
name = "route-to-apim"
rule_type = "Basic"
http_listener_name = "listener-https"
backend_address_pool_name = "apim-bepool"
backend_http_settings_name = "https-settings"
}
waf_configuration {
enabled = true
firewall_mode = "Prevention"
rule_set_type = "OWASP"
rule_set_version = "3.2"
}
}
# Azure API Management (Developer SKU for demo – use Premium for prod & VNET integration)
resource "azurerm_api_management" "apim" {
name = "apim-contoso"
location = var.location
resource_group_name = azurerm_resource_group.rg.name
publisher_name = "Contoso"
publisher_email = "admin@contoso.com"
sku_name = "Developer_1"
virtual_network_type = "None" # Use "Internal" for vNet, then add private endpoint
}
## Azure DevOps – CI/CD YAML (App Gateway + Azure API Management via Terraform)
`yaml
trigger:
branches:
include: [ main ]
pool:
vmImage: 'ubuntu-latest'
variables:
TF_VERSION: '1.8.5'
ARM_USE_MSI: true
stages:
- stage: Validate
jobs:
- job: tf_validate
steps:
- task: Bash@3
displayName: 'Install Terraform'
inputs:
targetType: 'inline'
script: |
sudo apt-get update && sudo apt-get install -y unzip
curl -L -o tf.zip https://releases.hashicorp.com/terraform/${TF_VERSION}/terraform_${TF_VERSION}_linux_amd64.zip
unzip tf.zip && sudo mv terraform /usr/local/bin/
terraform -version
- task: AzureCLI@2
displayName: 'Terraform init & validate'
inputs:
azureSubscription: '$(AZURE_SERVICE_CONNECTION)'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
terraform init -backend-config=backend.hcl
terraform fmt -check
terraform validate
- stage: Plan
jobs:
- job: tf_plan
steps:
- task: AzureCLI@2
inputs:
azureSubscription: '$(AZURE_SERVICE_CONNECTION)'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
terraform plan -out=tfplan
- publish: tfplan
artifact: tfplan
- stage: Apply
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- job: tf_apply
steps:
- download: current
artifact: tfplan
- task: AzureCLI@2
inputs:
azureSubscription: '$(AZURE_SERVICE_CONNECTION)'
scriptType: 'bash'
scriptLocation: 'inlineScript'
inlineScript: |
terraform apply -auto-approve tfplan
##Observability & diagnostics
- Access Logs: App Gateway & WAF logs to Log Analytics; query with KQL.
- Azure API Management Metrics: Requests, backend duration, cache hits; enable diagnostic settings to Log Analytics/Storage/Event Hub.
- End-to-end tracing: Correlate `x-correlation-id` across App Gateway, Azure API Management, and backend logs.
- Alerts: 4xx/5xx thresholds, WAF blocks spike, Azure API Management throttling events, TLS certificate expiry.
## Security hardening
- Enforce TLS 1.2+, disable weak ciphers.
- WAF exclusions tuned minimally; regular rule reviews.
- Azure API Management IP allow-lists for admin endpoints; use RBAC and separate admin vs gateway hostnames.
- Private Endpoints for Azure API Management & backends; deny public network access where possible.
- mTLS from App Gateway→Azure API Management or Client→Azure API Management when required (Key Vault for client certs).
- DDoS Protection on vNet with public exposure; consider Azure Front Door WAF for global edge.
## Cost & Performance
- Right-size App Gateway v2 capacity; enable autoscaling for variable traffic.
- Use Azure API Management Premium only if you need vNet, multi-region, or zone redundancy; otherwise consider Standard/Developer for non-prod.
- Caching policies in Azure API Management reduce backend load; use response compression.
- Health probes optimized for backend responsiveness (avoid tight intervals).
## Troubleshooting
- App Gateway 502/504: Check backend health, probe path, SNI/host header, TLS ciphers, DNS resolution to Azure API Management.
- Azure API Management 401/403: Validate JWT audience/issuer; clock skew; named values; policy order.
- Private Endpoint: DNS record in `privatelink.azure-api.net` exists; App Gateway subnet can resolve; NSG not blocking.
- Cert Issues: PFX password correct; full chain present; key usage supports server auth.
- Performance: Turn on App Gateway autoscaling; review Azure API Management throttling; check backend rate limits.
## Production checklist
- Custom domains & cert rotation via Key Vault
- WAF in Prevention with tuned exclusions
- Azure API Management policies for auth, rate limiting, cache, headers
- Private endpoints + DNS validated end-to-end
- Autoscaling & health probes tuned
- Diagnostics & alerts configured
- CI/CD gated approvals; Terraform state secured
- Runbooks for failover & certificate renewal