Azure Application Gateway: Architecture, Routing, CORS, Certificates and Monitoring — A Deep Dive for Azure Architects

Azure Application Gateway architecture showing WAF, listeners, routing rules and backend pools

Reading time: 5 minutes, 8 seconds

Azure Application Gateway is more than just a load balancer — it’s a full Layer 7 application delivery controller with WAF, SSL offload, URL-based routing, multi-site hosting, and deep observability. This guide covers everything an Azure Architect or Engineer needs to know.


1. Architecture Overview

Application Gateway operates at Layer 7 (HTTP/HTTPS), handling traffic inspection, routing, and TLS termination before requests reach your application.

Azure Application Gateway architecture overview
Figure 1 — Application Gateway core architecture: WAF → Frontend IP → Listeners → Routing Rules → Backend Pools

Key Components

  • Frontend IP — Public and/or private IP. Standard v2 supports both simultaneously.
  • Listeners — Define protocol (HTTP/HTTPS), port, hostname. Basic or Multi-site.
  • WAF Policy — OWASP 3.2 ruleset, custom rules, bot protection, geo-filtering.
  • Routing Rules — Associate listener with backend pool + HTTP settings. Basic or Path-based.
  • Backend Pools — VMs, VMSS, App Services, AKS pods, on-premises IPs, FQDNs.
  • HTTP Settings — Port, protocol, timeouts, cookie affinity, connection draining.
  • Health Probes — Custom or default probes for backend health checks.

SKU Comparison

FeatureStandard v2WAF v2
Layer 7 LB✅✅
Autoscaling✅✅
Zone redundancy✅✅
WAF (OWASP)❌✅
Custom WAF rules❌✅
Bot protection❌✅

💡 Architect tip: Always deploy WAF v2 in production. Use Detection mode first to baseline traffic, then switch to Prevention mode.

resource appGateway 'Microsoft.Network/applicationGateways@2023-04-01' = {
name: 'agw-prod'
zones: ['1', '2', '3']
properties: {
sku: { name: 'WAF_v2', tier: 'WAF_v2' }
autoscaleConfiguration: { minCapacity: 2, maxCapacity: 10 }
}
}

2. Routing Options

2.1 Basic Routing

All traffic from a listener goes to a single backend pool. Use for single-application scenarios.

2.2 URL Path-Based Routing

Route to different backend pools based on URL path — the most common pattern for microservices.

URL path-based routing diagram
Figure 2 — URL path-based routing: different paths map to different backend pools with independent HTTP settings
urlPathMaps: [{
name: 'url-path-map'
properties: {
defaultBackendAddressPool: { id: webPool.id }
pathRules: [
{ name: 'api-rule', paths: ['/api/v1/*', '/api/v2/*'], backendPool: apiPool },
{ name: 'static-rule', paths: ['/images/*', '/static/*'], backendPool: cdnPool }
]
}
}]

⚠️ Gotcha: Path rules are evaluated in order — first match wins. Put specific paths before wildcards. The default rule catches everything unmatched.

2.3 Multi-Site (Hostname-Based) Routing

Host multiple applications on a single gateway with one public IP. Each listener binds to a specific hostname with its own TLS cert and backend pool.

Multi-site hosting diagram
Figure 3 — Multi-site hosting: one public IP, multiple hostnames, each with independent TLS cert and backend

2.4 Wildcard Listeners

Listeners support wildcard hostnames (e.g., *.contoso.com) to catch all subdomains with a single listener.

2.5 HTTP Header and URL Rewrite Rules

Modify request/response headers and URLs without touching backend code. Common uses: strip path prefixes, inject security headers, remove sensitive response headers like Server and X-Powered-By.

rewriteRules: [{
name: 'strip-api-prefix-and-add-security-headers'
ruleSequence: 100
conditions: [{ variable: 'var_uri_path', pattern: '/api/(.*)' }]
actionSet: {
urlConfiguration: { modifiedPath: '/{var_uri_path_1}' }
responseHeaderConfigurations: [
{ headerName: 'Server', headerValue: '' },
{ headerName: 'Strict-Transport-Security', headerValue: 'max-age=31536000; includeSubDomains' },
{ headerName: 'X-Content-Type-Options', headerValue: 'nosniff' },
{ headerName: 'X-Frame-Options', headerValue: 'DENY' }
]
}
}]

2.6 Redirects

Application Gateway handles HTTP→HTTPS redirects, path redirects, and external URL redirects natively — no backend logic needed.

💡 Trick: Enforce HTTPS at the gateway level to offload redirect logic from every backend service.

2.7 Connection Draining

Enable connection draining for graceful backend removal during rolling deployments. The gateway stops sending new requests to a deregistering backend while allowing existing connections to complete.

backendHttpSettings: {
connectionDraining: { enabled: true, drainTimeoutInSec: 60 }
}

3. CORS Policies — Scenarios and Examples

Application Gateway implements CORS via rewrite rules that inject the correct response headers centrally, rather than managing CORS on every backend.

Key CORS Headers

HeaderPurpose
Access-Control-Allow-OriginWhich origins are allowed
Access-Control-Allow-MethodsAllowed HTTP methods
Access-Control-Allow-HeadersAllowed request headers
Access-Control-Allow-CredentialsWhether cookies/auth are allowed
Access-Control-Max-AgeHow long preflight results are cached
Access-Control-Expose-HeadersHeaders exposed to JavaScript

Scenario 1: SPA Calling an API (Same Gateway)

React SPA at https://app.contoso.com calling REST API at https://api.contoso.com — both behind the same Application Gateway:

rewriteRules: [{
name: 'cors-spa-to-api'
conditions: [{ variable: 'http_req_Origin', pattern: 'https://app\.contoso\.com' }]
actionSet: {
responseHeaderConfigurations: [
{ headerName: 'Access-Control-Allow-Origin', headerValue: 'https://app.contoso.com' },
{ headerName: 'Access-Control-Allow-Methods', headerValue: 'GET, POST, PUT, DELETE, OPTIONS' },
{ headerName: 'Access-Control-Allow-Headers', headerValue: 'Authorization, Content-Type, X-Request-ID' },
{ headerName: 'Access-Control-Allow-Credentials', headerValue: 'true' },
{ headerName: 'Access-Control-Max-Age', headerValue: '3600' }
]
}
}]

Scenario 2: Handling OPTIONS Preflight

Route OPTIONS requests to a dedicated Azure Function returning 200 + CORS headers immediately, without hitting the main backend:

// Azure Function — OPTIONS preflight responder
module.exports = async function (context, req) {
context.res = {
status: 200,
headers: {
'Access-Control-Allow-Origin': req.headers['origin'] || '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Authorization, Content-Type',
'Access-Control-Max-Age': '86400'
},
body: ''
};
};

Scenario 3: Multi-Origin Allowlist (dev/staging/prod)

conditions: [{
variable: 'http_req_Origin',
pattern: 'https://(app|staging|dev)\.contoso\.com'
}]
actionSet: {
responseHeaderConfigurations: [{
headerName: 'Access-Control-Allow-Origin',
headerValue: '{http_req_Origin}' // Echo back the validated origin
}]
}

Scenario 4: Azure AD / OAuth2 (MSAL.js)

When using MSAL.js, ensure:

  • Access-Control-Allow-Credentials: true — required for session cookies
  • Access-Control-Expose-Headers: WWW-Authenticate — required for 401 challenge handling
  • Access-Control-Allow-Headers must include Authorization

⚠️ Security: Never combine Access-Control-Allow-Origin: * with Access-Control-Allow-Credentials: true — browsers will block it. Always specify the exact origin when credentials are involved.


4. Certificate Management

Certificate management flow
Figure 4 — Certificate lifecycle: Key Vault as source of truth with auto-rotation, frontend TLS termination, optional backend re-encryption

4.1 Frontend Certificates — Three Approaches

MethodProsCons
Key Vault (recommended)Auto-rotation, centralised, no PFX handlingRequires Managed Identity setup
PFX uploadSimple, no dependenciesManual rotation, cert stored in gateway config
Let’s Encrypt via AGICFree, automatedAKS only, requires cert-manager
// Step 1: Assign Key Vault Secrets User role to AGW Managed Identity
// Step 2: Reference cert WITHOUT version for auto-rotation
sslCertificates: [{
name: 'contoso-cert'
properties: {
// No version in the URI = picks up latest cert automatically (within 4h)
keyVaultSecretId: 'https://kv-prod.vault.azure.net/secrets/contoso-tls'
}
}]

💡 Trick: Omit the version from the Key Vault secret ID. When the cert is rotated in Key Vault, Application Gateway automatically picks up the new version within 4 hours — zero manual steps.

4.2 Backend Certificates (End-to-End TLS)

  • Public trusted CA: Enable “Use well-known CA certificate” in HTTP Settings — no upload needed.
  • Internal/self-signed CA: Upload the root certificate (DER/CER format) as a Trusted Root Certificate in HTTP Settings.

4.3 TLS Policy — Enforce Strong Ciphers

sslPolicy: {
policyType: 'Custom'
minProtocolVersion: 'TLSv1_2' // Never allow TLS 1.0 or 1.1
cipherSuites: [
'TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384',
'TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256'
]
}

4.4 Certificate Expiry Monitoring (KQL)

AzureDiagnostics
| where Category == "ApplicationGatewayAccessLog"
| where sslClientCertificateIssuer_s != ""
| summarize by sslClientCertificateIssuer_s, bin(TimeGenerated, 1d)

5. Monitoring, Debugging and Alerts

Monitoring and alerting architecture
Figure 5 — Full observability stack: App Gateway → Diagnostic Settings → Log Analytics → Alerts, Workbooks, Sentinel

5.1 Enable All Three Diagnostic Log Categories

  • ApplicationGatewayAccessLog — Every request: client IP, URI, response code, latency, backend pool, backend response time.
  • ApplicationGatewayPerformanceLog — Throughput, connection counts, healthy/unhealthy host counts per instance.
  • ApplicationGatewayFirewallLog — WAF rule matches, blocked requests, anomaly scores (WAF v2 only).

5.2 Key Metrics and Alert Thresholds

MetricAlert ThresholdWhat it means
UnhealthyHostCount> 0Any backend failing health probe
ResponseStatus (5xx)> 1% of requestsBackend errors
BackendLastByteResponseTime> your SLOBackend slowness
CapacityUnits> 75% of maxApproaching autoscale ceiling
WAFTotalRequests (Blocked)Sudden spikePotential attack in progress

5.3 Essential KQL Queries

Top backends by P95 response time:

AzureDiagnostics
| where Category == "ApplicationGatewayAccessLog" and TimeGenerated > ago(1h)
| extend backendLatency = todouble(backendResponseLatency_s)
| summarize avg(backendLatency), percentile(backendLatency, 95) by backendAddress_s
| order by percentile_backendLatency_95 desc

5xx errors by backend pool and URI:

AzureDiagnostics
| where Category == "ApplicationGatewayAccessLog"
| where httpStatus_d >= 500 and TimeGenerated > ago(1h)
| summarize count() by requestUri_s, backendPoolName_s, httpStatus_d
| order by count_ desc

WAF top blocked rules (last 24h):

AzureDiagnostics
| where Category == "ApplicationGatewayFirewallLog"
| where action_s == "Blocked" and TimeGenerated > ago(24h)
| summarize count() by ruleId_s, ruleGroup_s, requestUri_s
| order by count_ desc | take 20

P95 latency trend (renders as time chart):

AzureDiagnostics
| where Category == "ApplicationGatewayAccessLog" and TimeGenerated > ago(3h)
| summarize percentile(todouble(timeTaken_s), 95) by bin(TimeGenerated, 5m)
| render timechart

5.4 Debugging Tips

  • Backend health blade: Portal → Application Gateway → Backend Health. Real-time probe status per backend — fastest first stop for any routing issue.
  • X-Forwarded-For: Ensure backends log this header. Without it, client IPs appear as the gateway’s subnet IP.
  • 502 Bad Gateway: Almost always a health probe failure or TLS mismatch between gateway and backend. Check Backend Health first.
  • Probe port mismatch: Probing on port 443 but sending traffic to port 80 = backends appear healthy but traffic silently fails.
  • NSG mandatory rule: Inbound allow on ports 65200–65535 from GatewayManager service tag is required for App Gateway v2 management plane. Without it the gateway won’t deploy or function.

6. Architect and Engineer Tricks

Custom Health Probes — Go Beyond Root Path

probes: [{
name: 'api-health-probe'
properties: {
protocol: 'Https', path: '/health/live'
interval: 30, timeout: 10, unhealthyThreshold: 3
pickHostNameFromBackendHttpSettings: true
match: { statusCodes: ['200-399'] }
}
}]

Application Gateway + Azure Front Door

For global architectures: Front Door handles global load balancing, CDN, DDoS, and geo-routing. Application Gateway handles regional URL routing and backend pools. Lock AGW to only accept Front Door traffic via WAF custom rule:

customRules: [{
name: 'AllowFrontDoorOnly', priority: 1, action: 'Block'
matchConditions: [{
matchVariables: [{ variableName: 'RequestHeaders', selector: 'X-Azure-FDID' }]
operator: 'Equal', negationCondition: true
matchValues: ['your-front-door-resource-id']
}]
}]

Private Link Backend Integration

Route traffic to backends exposed via Azure Private Link — keeping all traffic on the Microsoft backbone. Supports PaaS services (Azure SQL, Storage) and third-party services.

Common Mistakes to Avoid

  • ❌ Single instance in production — Always set minCapacity: 2. One instance = potential downtime during Azure platform updates.
  • ❌ No availability zone pinning — Always specify zones: ['1','2','3'] to avoid all capacity landing in a single AZ.
  • ❌ WAF in Detection mode permanently — Set a 30-day calendar reminder. Detection logs but never blocks — it’s not protection.
  • ❌ Default 30s request timeout — Long-running APIs (reports, exports) will return 502. Tune timeout per backend pool HTTP settings.
  • ❌ Missing NSG management rule — Ports 65200–65535 from GatewayManager must be allowed inbound or the gateway won’t function.
  • ❌ Cookie affinity as a permanent crutch — Prefer stateless backends + Redis Cache. Affinity prevents balanced load distribution and complicates blue/green deployments.
  • ❌ Hardcoded cert versions in Key Vault references — Always omit the version suffix to get auto-rotation. A pinned version = manual cert rotation forever.

Summary

Azure Application Gateway is a foundational component in enterprise Azure architectures. When configured correctly, it provides a single secure ingress point with rich routing, WAF protection, automated TLS lifecycle, and deep observability.

  • ✅ Deploy WAF v2 with zone redundancy and minCapacity: 2
  • ✅ Use Key Vault for cert management with auto-rotation (no version in secret ID)
  • ✅ Use URL path routing to cleanly decompose monoliths and route microservices
  • ✅ Handle CORS centrally via rewrite rules — not scattered across backends
  • ✅ Enable all three diagnostic log categories and build KQL-based alerts
  • ✅ Inject security headers centrally via rewrite rules
  • ✅ Combine with Azure Front Door for global multi-region scenarios

Have questions or patterns to share? Drop them in the comments below.

Facebook
Twitter
LinkedIn
Email

Leave a Reply

Get new articles by email

Practical Cloud, DevOps and AI walkthroughs

We don’t spam! Read our privacy policy for more info.