Application Gateway (Add or Change a Workload)¶
Standard changes: Add workload to Application Gateway and Changes to workload configuration within Application Gateway.
Effect: the hub Application Gateway gets new or changed backend pools, HTTP settings, listeners, routing rules,
probes, redirects or certificates, so a workload is published to the internet behind the Web Application Firewall (WAF).
Only the connectivity layer changes. The gateway is updated in place.
The reviewer and operator runbooks for this change are Application Gateway change, Application Gateway deployment and, for WAF policy files, WAF policy review.
Request fields¶
| Request field | Where it goes in the gateway JSON | Notes |
|---|---|---|
| Workload Name | names of the pool, setting, listener, rule, probe | e.g. app1-pool, app1-https-settings, app1-https-listener, app1-rule |
| Rationale | pull request | |
| Environment | platform environment and region folder | |
| Backend Address (IP/FQDN) | backend_address_pools.<pool>.ip_addresses / .fqdns |
IPv4 only (config test). The backend must be reachable from the App Gateway subnet. |
| Listener URL | http_listeners.<listener>.host_name (or host_names) |
e.g. app1.example.com. Create the public DNS record yourself, unless you use the platform's public DNS zone. |
| Listener Port (HTTPS/HTTP) | frontend_ports.<port> + http_listeners.<listener>.protocol / .frontend_port_name |
|
| SSL Certificate | ssl_certificates.<cert>.key_vault_secret_id + http_listeners.<listener>.ssl_certificate_name |
Required for HTTPS listeners (config test). |
| Redirect (yes/no) | redirect_configurations.<redirect> + request_routing_rules.<rule>.redirect_configuration_name |
e.g. HTTP → HTTPS |
| Health Probe (custom or standard) | standard: backend_http_settings.<s>.probe_name = null. Custom: probes.<probe> + probe_name |
|
| Error page URLs (502, 403), optional | http_listeners.<listener>.custom_error_configurations |
Pages the gateway shows for a bad gateway or a forbidden response. Check the gateway schema for the structure. |
| Comments |
Before you start¶
- The gateway must be enabled for the region in
<env-folder>/<region>/connectivity.auto.tfvars:
use_web_application_firewall_policy_module = true
web_application_firewall_policy_json_file_relative_path = "<region>/web_application_firewall_policies/"
web_application_firewall_policy_json_file_pattern = "/**/web_application_firewall_policy_*.json"
use_new_application_firewall_policy_naming = true
use_application_gateway_module = true
application_gateway_json_file_relative_path = "<region>/application_gateways/"
application_gateway_json_file_pattern = "/**/application_gateway_*.json"
use_new_application_gateway_naming = true
use_application_gateway_module is the only switch that turns the gateway on. create_application_gateway is
passed, but the template overrides it with true (infra/templates/application_gateway/main.tf). See
Platform features to enable the gateway for the first time.
- Certificates. The platform creates a Key Vault for the gateway: kv<company>agw<env_short><region>001. The
gateway's user-assigned identity uai-<company>-agw-<env>-<region>-001 gets Key Vault Secrets User on it.
1. Import the PFX into that Key Vault as a certificate.
2. Use its secret URI (https://<kv>.vault.azure.net/secrets/<name> or …/<name>/<version>) as
key_vault_secret_id.
Let the requester import the PFX into the Key Vault. The PFX contains the private key and often a password, and this
way neither has to be sent to the platform team. Import it as a Certificate (not as a secret), in PFX format, before
you deploy the change. See Microsoft's tutorial
Import a certificate in Azure Key Vault.
If the PFX has a password, store the password as a secret in the same Key Vault and put its URI in
certificate_password_secret_id. Use null when there is no password.
The Key Vault denies public access except for allowed subnets. Import from an allowed network, or add your IP temporarily. To renew a certificate that is already in use, follow Replace a certificate.
First-time set-up¶
Only when the platform has no gateway yet:
- Switch the modules on in
<region>/connectivity.auto.tfvarsfor both environments. - Create the folders
application_gateways/andweb_application_firewall_policies/in the region folder. - Create
application_gateway_hub.jsonandweb_application_firewall_policy_hub.jsonwith exactly these names, because the config tests only checkapplication_gateway_hub.json. - Deploy a minimal gateway first: one frontend IP and port, a dummy backend pool, a dummy HTTP listener, one routing rule, the SKU and the WAF configuration. All top-level keys must be present (
nullwhere unused,{}forssl_certificates). Replace the dummy values after the first deployment. - Write
firewall_policy_idwithapplicationGatewayWebApplicationFirewallPoliciesstarting with a lower-casea(see the v4.12.x upgrade guide). - Fill the WAF policy file.
policy_settingsis only applied when the policy is created. - Run
huband check that onlyconnectivityshows the new gateway and policy.
How the JSON is organized¶
One JSON file describes one whole gateway: application_gateway_<name>.json in the region's
application_gateways/ folder. Adding a workload means adding entries to the maps of the existing file. Each map key is
the Azure name of that sub-resource.
Config tests only check a file named exactly
application_gateway_hub.json(src/Xebia.Mcp/Tests/Config/XebiaAppGateway.Mcp.Config.Tests.ps1). Use that name for the hub gateway, so your edits are validated before deployment.
All top-level keys must be present, but may be null, except ssl_certificates: use {} when there are none. The
module reads every key directly (infra/modules/application_gateway/main.tf).
Add a workload¶
Add these entries (example: HTTPS for app1.example.com → 10.20.4.10:443, with HTTP→HTTPS redirect and a custom probe):
"frontend_ports": {
"port-443": { "port": 443 },
"port-80": { "port": 80 }
},
"backend_address_pools": {
"app1-pool": { "fqdns": [], "ip_addresses": ["10.20.4.10"] }
},
"probes": {
"app1-probe": {
"host": "app1.example.com",
"interval": 30,
"minimum_servers": 0,
"path": "/health",
"pick_host_name_from_backend_http_settings": false,
"port": 443,
"protocol": "Https",
"timeout": 30,
"unhealthy_threshold": 3,
"matches": null
}
},
"backend_http_settings": {
"app1-https-settings": {
"affinity_cookie_name": null,
"authentication_certificates": null,
"connection_drainings": null,
"cookie_based_affinity": "Disabled",
"host_name": "app1.example.com",
"path": null,
"pick_host_name_from_backend_address": false,
"port": 443,
"probe_name": "app1-probe",
"protocol": "Https",
"request_timeout": 30,
"trusted_root_certificate_names": null
}
},
"ssl_certificates": {
"app1-cert": {
"key_vault_secret_id": "https://kv<company>agw<envshort><region>001.vault.azure.net/secrets/app1-example-com",
"certificate_password_secret_id": null
}
},
"http_listeners": {
"app1-https-listener": {
"custom_error_configurations": null,
"firewall_policy_id": null,
"frontend_ip_configuration_name": "appGatewayFrontendIP",
"frontend_port_name": "port-443",
"host_name": "app1.example.com",
"host_names": null,
"protocol": "Https",
"require_sni": true,
"ssl_certificate_name": "app1-cert",
"ssl_profile_name": null
},
"app1-http-listener": {
"custom_error_configurations": null,
"firewall_policy_id": null,
"frontend_ip_configuration_name": "appGatewayFrontendIP",
"frontend_port_name": "port-80",
"host_name": "app1.example.com",
"host_names": null,
"protocol": "Http",
"require_sni": null,
"ssl_certificate_name": null,
"ssl_profile_name": null
}
},
"redirect_configurations": {
"app1-http-to-https": {
"include_path": true,
"include_query_string": true,
"redirect_type": "Permanent",
"target_listener_name": "app1-https-listener",
"target_url": null
}
},
"request_routing_rules": {
"app1-rule": {
"backend_address_pool_name": "app1-pool",
"backend_http_settings_name": "app1-https-settings",
"http_listener_name": "app1-https-listener",
"priority": 110,
"redirect_configuration_name": null,
"rewrite_rule_set_name": null,
"rule_type": "Basic",
"url_path_map_name": null
},
"app1-redirect-rule": {
"backend_address_pool_name": null,
"backend_http_settings_name": null,
"http_listener_name": "app1-http-listener",
"priority": 111,
"redirect_configuration_name": "app1-http-to-https",
"rewrite_rule_set_name": null,
"rule_type": "Basic",
"url_path_map_name": null
}
}
Merge these into the existing maps; don't replace them. If the frontend ports already exist, reuse their names.
Make sure that the protocol of a listener and its frontend_port_name match: an Https listener uses the HTTPS port
(443 in the example), an Http listener the HTTP port (80). A custom probe needs protocol, host, path, interval,
timeout and unhealthy_threshold. As an alternative to a fixed host_name, an HTTP setting can take the host name from the
backend address: set pick_host_name_from_backend_address to true and host_name to null.
Rules checked by the config tests:
- routing-rule
prioritymust be 1–20000 and unique; - each listener may be used by only one routing rule;
- every reference (pool, setting, listener, port, certificate) must point to an existing key;
- HTTPS listeners must have
ssl_certificate_name; - backend IPs must be valid IPv4.
Standard vs custom health probe. With probe_name: null the gateway uses the Azure default probe for that HTTP
setting. To use a custom probe, define it under probes and reference it in probe_name.
WAF policy for the workload¶
- Gateway-wide WAF:
application_gateway.firewall_policy_id(full resource ID of the WAF policy). - Per-listener WAF: set
http_listeners.<listener>.firewall_policy_idto a different WAF policy's resource ID. - Per-path WAF:
url_path_maps.<map>.path_rules.<rule>.firewall_policy_id.
WAF policies are separate files: <region>/web_application_firewall_policies/web_application_firewall_policy_<name>.json.
They are created in resource group rg-<company>-agw-<env>-<region>-001, unless overwrite_rg_name_application_gateway
is set. Build the firewall_policy_id from that resource group name and the policy name.
policy_settingsis only applied when a WAF policy is created.infra/modules/web_application_firewall_policy/main.tfhaslifecycle { ignore_changes = [policy_settings] }, because Azure Policy keeps changing these settings. Later edits tomode(Detection/Prevention), body size limits and similar settings are not deployed. Change them in the portal and record the change, or create a new policy. Custom rules and managed rule overrides are updated.
WAF false positives and exclusions¶
A WAF in prevention mode blocks requests that match its managed rules. Sometimes a legitimate request is blocked, for example a request with many special characters in an argument. Handle this as an exception:
- The workload team gives the platform team the blocked request (URI, the name of the argument or header, the rule ID from the WAF log) and explains why it is a false positive.
- Security approves the exception.
- The platform team adds a narrow exclusion (for example for one argument and the rules that trigger on it) or disables a rule that Microsoft advises to disable for false positives, in the WAF policy file. Record the rationale and the example request in the change record.
Prefer an exclusion for the one argument over disabling a rule. Keep a list of the exclusions and disabled rules with
their rationale, so you can review them later. The shared services gateway module also supports exceptions that match
on a client IP address, a request header or a URI, with fixed limits and plan-time validation: see
WAF exceptions. Whether the hub module supports them is not documented. Check the WAF policy schema for the exact keys. Custom rules and managed
rule overrides are updated by hub, but see the note on policy_settings above.
Settings to decide for the WAF
One platform used: gateway SKU WAF v2 with autoscaling between 2 and 5 instances, HTTP/2 on, a TLS policy with a minimum of TLS 1.2, WAF mode Prevention, the OWASP 3.2 rule set, request body inspection on and log scrubbing on. Your platform may differ. Write down the values you choose.
Network path to the backend¶
The backend must accept traffic from the App Gateway subnet (AppGwSubnet in the hub virtual network). When that
traffic crosses the hub firewall, add a firewall rule from the App Gateway subnet to the backend
(Firewall rules). Check the spoke NSG as well.
Change an existing workload¶
Edit the workload's entries in place: backend address, host name, certificate, probe or redirect. Renaming a map key renames the sub-resource. Update all references to it in the same change.
Variant: gateway from the shared services code¶
The shared services code (Shared services: what is deployed and how)
contains a second Application Gateway implementation, the appgw addon. Use the table to see which implementation your
platform runs. The sections above describe the hub module.
| Topic | Hub module (this page) | Shared services appgw addon |
|---|---|---|
| Layer and place | connectivity layer, AppGwSubnet in the hub virtual network |
Own deployment in a spoke, AppGwSubnet in the spoke virtual network |
| Configuration | application_gateway_hub.json and web_application_firewall_policy_*.json in the region folder of the platform configuration |
JSON files in gateway-definitions/ and waf-definitions/ of the addon, with a variables file |
| Resource names | rg-<company>-agw-<env>-<region>-001, kv<company>agw<env_short><region>001 |
rg-agw-<environment>-<location_short>-001, agw-<environment>-<location_short>-001, kv-agw-<environment>-<location_short>-<random> |
| Certificate store | Key Vault created by the platform, Key Vault Secrets User for the gateway identity | Key Vault with private endpoint and RBAC, the gateway identity gets Key Vault Certificates Officer and Key Vault Secrets User |
| Gateway WAF policy | Set with firewall_policy_id in the gateway JSON |
From the file web_application_firewall_policy_hub.json |
policy_settings after creation |
Ignored (not deployed) | Deployed. Changing the policy name replaces the policy. |
| WAF exceptions | Not documented | Supported, with limits and checks (WAF exceptions) |
| Deployment | hub pipeline, connectivity layer |
Not part of the shared services pipeline in the sources. Ask how your platform deploys it. |
For the shared services addon the workload entries (pools, settings, listeners, rules, probes, certificates) use the same
sub-resource names and fields as in Add a workload. Check the wrapper object of the file against the code (the shared services files have a top-level application_gateway object). The certificate must be in the Key Vault of that gateway, and the
backend must be reachable from the gateway subnet in the spoke.
Deploy¶
- Validate locally (
pwsh ./dev/dev.ps1 -Action TestwithMCP_CONFIGpointing at yourmcp.json, see Run the tests locally) and open a pull request. - Run
hub. Only the connectivity layer should show~ update in-placeofazurerm_application_gateway.this[...], plus any WAF policy changes. - Apply. Application Gateway updates can take several minutes.
Logs¶
The gateway and the WAF can send their logs to a Log Analytics workspace through diagnostic settings. Depending on those
settings the entries are in the AzureDiagnostics table or in resource-specific tables. Give the people who troubleshoot
published apps read access to the logs of the gateway resource, and not to the whole workspace.
Verify¶
- Azure portal → Application Gateway → Backend health: the new pool is Healthy.
curl -v https://app1.example.com/from the internet returns the backend's response. Check that the certificate matches.- If a redirect is configured,
curl -I http://app1.example.com/returns301withLocation: https://….
Roll back¶
Revert the pull request and run hub.
Common problems¶
| Symptom | Cause |
|---|---|
| Backend health Unhealthy | NSG or firewall blocks the gateway; the probe path/host is wrong; or the backend certificate is not trusted (trusted_root_certificate_names). |
| Apply fails reading the certificate | The secret URI is wrong, the identity lacks Key Vault Secrets User, or Key Vault network rules block access. |
Plan error on ssl_certificates |
ssl_certificates is null. Use {}. |
| WAF mode change not applied | policy_settings is ignored after creation (see above). |
| Plan shows no gateway at all | use_application_gateway_module = false for that region. |