Azure Front Door Standard/Premium Tips and Tricks
I share my experience, lessons-learned, and tips and tricks for working with the new Azure Front Door Standard/Premium SKUs
Update
Since writing this article, Azure Front Door Standard/Premium has been released to GA (on March 29, 2022). You can read more about it here: Introducing the new Azure Front Door: Reimagined for modern apps and content.
Likewise, Azure Front Door Terraform resources have since become available to help manage:
- AzureRM v3.25.0 (Sept 29, 2022): Additions of azurerm_cdn_frontdoor_route, azurerm_cdn_frontdoor_custom_domain, andazurerm_cdn_frontdoor_route_disable_link_to_default_domain
- AzureRM v3.21.0 (Sept 2, 2022): Additions of azurerm_cdn_frontdoor_rule and azurerm_cdn_frontdoor_secret
- AzureRM v3.19.9 (Aug 18, 2022): Additions of azurerm_cdn_frontdoor_firewall_policy and azurerm_cdn_frontdoor_security_policy
- AzureRM v3.15.0 (July 21, 2022): Additions of azurerm_cdn_frontdoor_origin and azurerm_cdn_frontdoor_origin_group
- AzureRM v3.10.0 (June 10, 2022): Addition of azurerm_cdn_frontdoor_rule_set
- AzureRM v3.9.0 (June 2, 2022): Additions of cdn_frontdoor_profile (with options of
Standard_AzureFrontDoor
orPremium_AzureFrontDoor
) and azurerm_cdn_frontdoor_endpoint
The bulk of this article should still be relevant, but if anything is strikingly wrong or has changed, leave a comment to let me know!
Overview
I want to talk about Azure Front Door - not the old Azure Front Door - the new Azure Front Door, the new PREVIEW Front Door with the Standard/Premium SKUs. But Josh, wait, this is a DevOps blog, why are you talking about Azure Front Door? Well, I had the pleasure experience of working with Front Door (Preview) on my most recent project, and thought I would be doing the world a disservice by not sharing a little bit of the frustration knowledge I have gained while working with it. Whether no one else is using this service or no one is talking about it, we struggled to find many resources online for how to do certain things in the Front Door (Preview), so this is where this article comes into play.
I am not planning on writing an entire how-to article, this is just intended to serve as a resource that hopefully the SEO Gods can help make someone else’s life easier. If you are unfamiliar with Azure Front Door (Preview), or want some official background and guidance from Microsoft, see the What is Azure Front Door Standard/Premium (Preview)? page. Also, this is a great resource from Ken Muse on when you should use Azure Front Door vs. Application Gateway, Load Balancer, or Traffic Manager.
Note: For the purposes of this article, I am going to abbreviate Azure Front Door as AFD, and when I say AFD, I mean the new Preview Azure Front Door; I will not be referring to the classic / old Front door here.
(PS: Guinness Book of Records, see above for my submission on most times “Front Door” has been used in a single sentence)
Things That Work Well
- Private Endpoints
- Azure Front Door does a great job of routing to services such as App Services, Function Apps, Storage Accounts, and Private Link Services that are protected via Private Endpoints
- Since these are ‘magic’ aka managed Private Endpoints, the Private Endpoint doesn’t live in your subscription and you don’t have access to it. Therefore, there doesn’t seem to be a way to get the Origin Group / Origin deployment to automatically approve these, so you have to remember to go to the target resource and approve the Private Endpoint manually. This is similar to how Azure Data Factory’s Private Endpoints work
- Private Endpoints only work with the Premium SKU
- Certificates!
- Azure Front Door does a great job of automatically managing certificates - including expirations - the default setting is to let Azure Front Door handle all of this for you with 0 configuration
- You still have the option to bring your own certificate by creating an Azure Front Door Secret linked to a Certificate in an Azure Key Vault - Azure Front Door even shows expiration of that certificate on the Domain page
- If you are using your own certificate, it needs to be in PFX / PKCS 12 format (not PEM)
- Web Application Firewall (WAF)
- Anecdotally, I only have experience with the Premium SKU of the Azure Front Door Preview service, but creating a WAF tied to Microsoft-provided default rule set is relatively simple
- The Standard SKU does not let you use a WAF
- Letting teams share a single Front Door resource
- Teams can manage their own Endpoints in a single Azure Front Door resource without worry of mucking it up too much for other teams
- This splits the current $165/monthly cost for the Premium SKU
- Linking Origin to just about any hostname
- Works great for serving static websites hosted in an Azure Storage Account - we created a static website container
$web
and set our Origin toOrigin Type: Custom
andHost Name: mystaticsite.z21.web.core.windows.net
- We got the idea from article as a basis for this static website setup (okay this is referencing the Classic Azure Front Door, but the custom host name screenshot was what did the trick)
- If using Private Endpoints, you still have to have Azure Front Door create a private endpoint to the storage account. We set the ‘target sub resource’ (aka
groupId
in the ARM/API) toweb
. - We did something similar with our Kubernetes linkage, creating a Private Link Service bound to the AKS managed subnet and Internal Load Balancer of our nginx service. In this pattern, the
Origin Type: Custom
andHost Name
was set to the IP of the Internal Load Balancer and we usednull()
for ourOrigin Host Header
and let nginx do header routing based on the URL that is being sent from Front Door to the Cluster. We set the ‘target sub resource’ (akagroupId
in the ARM/API) tonull()
. See below example - Works as expected with App Services and Function Apps; just set
Origin Type: App Services
- Works great for serving static websites hosted in an Azure Storage Account - we created a static website container
Things That Don’t Work Well
- Private Endpoints
- For about 3-4 weeks in July/early August, Azure Front Door’s Private Endpoints just completely died and Microsoft support was super slow in getting any attention to this. I get that it’s a Preview feature and things happen, but the resolution time was a little disappointing. We haven’t had any issues since they “reverted” the change that broke this, though
- You have to manually approve the Private Endpoint on your target resource
- URL Rewriting
- We were trying to use a single Azure Front Door Endpoint to host all of our App Service APIs as Origins; sort of like a poor man’s APIM (around 18x cheaper if you’re not using all of the premium features of a Premium APIM)
- We wanted
myfd.z01.azurefd.net/foo
to redirect tofoo.azurewebsites.net
- Maybe we were just doing it wrong, but we struggled to do native URL Rewriting in Azure Front Door - it’s incredibly
possiblelikely that we are just misinterpreting how it’s supposed to work - See my Stack Overflow post of what we were trying to do, and someone’s suggestion on how to resolve (I have not had a chance to test yet)
- We instead did URL Rewriting by using middleware at the app level. It just requires a making a small modification in the startup.cs file (and if you’re serving a Swagger page, there too!). See: examples below
- For Function Apps, there is no way to rewrite the incoming URL. However, you can edit the host.json file and customize the base path by modifying the
routePrefix
property. See: example below
- Update Times
- Okay minor gripe, but it takes anywhere from 5-20 minutes for a change you make to Front Door to propagate down to you
- Sometimes loading in a different browser / using a proxy can help alleviate cache/dns issues
- No native Terraform Resource (yet)
- We are using the azurerm_template_deployment resource to deploy an ARM template within Terraform
- Editing via the UI
- You have to click the ‘Edit’ button on the Endpoint, then click into the Origin Group/Route to make changes - if you just click on the Origin Group/Route without clicking ‘Edit’ first, you will just be in a read only mode.
- WebSockets
- Azure Front Door does not support WebSockets - use Long Polling instead for SignalR use cases
Random notes
- HTTPS Redirect in .NET Web Apps
- It is considered best practice to redirect http to https. This can be done in the code by adding the following to the
startup.cs
file:app.UseHttpsRedirection();
- The problem with this method when using Azure Front Door with Private Endpoints is that this causes the app to redirect to the host (azurewebsites.net) instead of the incoming host URL (custom domain on Azure Front Door)
- To work around this, you should remove this line of code altogether from the application and let Front Door redirect traffic to HTTPS (a setting on the Route)
- If you’re working with an Angular app, make sure to remove any HTTPS redirect from the
web.config
- It is considered best practice to redirect http to https. This can be done in the code by adding the following to the
- Deleting Endpoints / Domains / Front Door
- If you want to delete a domain, you will need to clean up all of the associations (i.e.: the Route)
- You can delete the entire Endpoint the domain is associated to as well
- If there is/was a WAF associated to that endpoint, though, you need to manually go into the WAF resource and remove the association to the domain manually. This isn’t made clear by the UI error (
Failed to delete the custom domain(s)
) or the CLI error ((BadRequest) Property 'AfdDomainEntityKey.AfdDomainName' cannot be set to 'mysubdomain.mydomain.com'.
) - If you go to re-create the Endpoint with the same name, note that it will fail for the first time with a
Conflict: That resource name isn't available
error message!!! You simply have to attempt to create the endpoint another time and then it will go through properly. The error will look something like this:error": {\r\n "code": "Conflict",\r\n "message": "That resource name isn't available."
- If you delete your entire Front Door, you still need to delete the WAF manually, and you will still see a
conflict
error message the first time you try to re-create a deleted endpoint
Our services aren’t available right now
error- Make sure you have approved the Private Endpoint on the target resource
- Alternatively, go and edit the Origin Group > Origin, uncheck the Private Endpoint box, save the Origin, Save the Origin Group, wait 10-30 seconds for it to apply, edit the Origin Group > Origin, check the Private Endpoint box and select the right resource, save the Origin, save the Origin Group, and go and re-approve the Private Endpoint on the target resource
<h2>Our services aren't available right now</h2><p>We're working to restore all services as soon as possible.
error- Your endpoint is still provisioning
- Or, your Route is misconfigured
- The HTML will be not be rendered on the page for this error - if it does render it means Front Door is routing correctly it’s likely a problem with a private endpoint (see above)
Page not found
blue page error- Wait 5-20 minutes for the Endpoint to provision
- Sometimes CORS errors disguise themselves as a misconfigured Endpoint / Private Endpoint
- Be familiar with grabbing a Bearer token to interact directly with the Azure REST APIs
- As an example, you can plug that Bearer token into Postman and use this
GET
request to list the details about a Origin in an Origin Group:1
GET https://management.azure.com/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/my-afd-rg/providers/Microsoft.Cdn/profiles/my-afd/originGroups/myorigingroup/origins?api-version=2020-09-01&Full
- This can also be helpful when deciphering what changes / values you need to use in an ARM template
- As an example, you can plug that Bearer token into Postman and use this
Summary
Hopefully I haven’t scared you away; Azure Front Door (Preview) has a lot of great features and shows a lot promise! And when it works, it works real well. Half of the frustration that we had with this service was that no one else had written about it, so we were kind of making it up as we go along. We have a solid foundation now, and once there is a native Terraform module, making changes for us will be even easier.
Happy hacking!
See the Appendix below for miscellaneous logging, URL rewriting, AFD CLI, and ARM template examples
Appendix: Examples
Log Analytics / Diagnostics Query
This assumes you have a Diagnostics Settings created that captures FrontDoorAccessLog
logs and sends to a Log Analytics resource.
See the below for an example Log Analytics / Diagnostics query to find non-200 HTTP Status Codes
1
2
3
AzureDiagnostics
| where httpStatusCode_s != 200
and TimeGenerated > ago(500m)
URL Rewrite
See the below sections for examples on how to do URL Rewriting in .NET (.NET Core) App Services and Function Apps
.NET - startup.cs
1
2
3
4
5
6
7
8
9
10
11
using Microsoft.AspNetCore.Rewrite; // add this using
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
// url rewrite
var options = new RewriteOptions().AddRewrite(@"^myapi/(.*)", "$1",
skipRemainingRules: true);
app.UseRewriter(options);
... // remainder of code below
}
Note: This makes the URL for local development something like
http://localhost:5001/myapi/...
Note: If you need
/path
and/path/
to work, then the regex for the rewrite should be@"^myapi[/]?(.*)"
.NET - Swagger
If you are using Swagger for an API, you should also update the prefix for the Swagger endpoint:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
app.UseSwaggerUI(c =>
{
// Specify swagger JSON endpoint
var prefix = apiPath == "" ? "" : $"/{apiPath}";
c.SwaggerEndpoint($"{prefix}/swagger/{apiVersion}/swagger.json", apiDefinitionTitle);
c.DocExpansion(DocExpansion.None);
// specifying the Swagger-ui endpoint.
c.RoutePrefix = "swagger-ui";
c.DefaultModelsExpandDepth(-1);
});
Function Apps - host.json
host.json
:
1
2
3
4
5
6
7
{
"version": "2.0",
"extensions": {
"http": {
"routePrefix": "function1"
}
}
AFD CLI Commands
Delete Origin Group
1
az afd origin-group delete --profile-name my-afd --resource-group my-afd-rg --origin-group-name myorigingroup --yes
Delete Custom Domain
1
az afd custom-domain delete --profile-name my-afd --resource-group my-afd-rg --custom-domain-name mysubdomain.mydomain.net
Delete Route
1
az afd route delete --profile-name my-afd --resource-group my-afd-rg --endpoint-name my-endpoint --route default
Delete Endpoint
1
az afd endpoint delete --profile-name my-afd --resource-group my-afd-rg --endpoint-name my-endpoint
Show Endpoint
1
az afd endpoint show --profile-name my-afd --resource-group my-afd-rg --endpoint-name my-endpoint
Adding Custom Domain with Certificate
Note that when --custom-domain-name
asks for a name, it’s just the friendly name of the domain as it appears in AFD
1
az afd custom-domain create -my-afd-rg --custom-domain-name foobar --profile-my-afd --host-name '*.mysubdomain.mydomain.com' --minimum-tls-version TLS12 --certificate-type CustomerCertificate --secret my-wildcard-cert-pfx --debug
Purging Cache
1
az afd endpoint purge --ids "/subscriptions/xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx/resourceGroups/my-afd-rg/providers/Microsoft.Cdn/profiles/my-afd/afdendpoints/my-endpoint" --content-paths "/*"
ARM Null() property and Terraform
We couldn’t figure out a way to pass in null()
from Terraform to our ARM template using the azurerm_template_deployment resource, and passing in ""
failed in mysterious ways or ended up just hanging the deployment. Essentially, we wrote some logic to convert the ""
passed into the template parameter to null()
for us.
Note lines 20, 25, and 69 for the relevant logic:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"profileName": {
"type": "string"
},
"originGroupsName":{
"type": "string"
},
"hostName":{
"type": "string"
},
"hostHeader":{
"type": "string"
},
"privateLinkResourceId":{
"type": "string"
},
"privateLinkResourceType":{
"type": "string"
}
},
"variables": {
"is-private-link-service-type": "[equals('', parameters('privateLinkResourceType'))]",
"is-null-host-header": "[equals('', parameters('hostHeader'))]"
},
"resources": [
{
"type": "Microsoft.Cdn/profiles/originGroups",
"apiVersion": "2020-09-01",
"name": "[concat(parameters('profileName'), '/', parameters('originGroupsName'))]",
"properties": {
"loadBalancingSettings": {
"sampleSize": 4,
"successfulSamplesRequired": 3,
"additionalLatencyInMilliseconds": 50
},
"healthProbeSettings": {
"probePath": "/",
"probeRequestType": "HEAD",
"probeProtocol": "Http",
"probeIntervalInSeconds": 100
},
"sessionAffinityState": "Disabled"
}
},
{
"type": "Microsoft.Cdn/profiles/originGroups/origins",
"apiVersion": "2020-09-01",
"name": "[concat(parameters('profileName'), '/', parameters('originGroupsName'), '/default')]",
"dependsOn": [
"[resourceId('Microsoft.Cdn/profiles/originGroups', parameters('profileName'), parameters('originGroupsName'))]"
],
"properties": {
"hostName": "[parameters('hostName')]",
"originHostHeader": "[if(variables('is-null-host-header'), null(), parameters('hostHeader'))]",
"httpPort": 80,
"httpsPort": 443,
"priority": 1,
"weight": 1000,
"enabledState": "Enabled",
"sharedPrivateLinkResource": {
"privateLinkLocation": "[resourceGroup().location]",
"privateLink": {
"id": "[parameters('privateLinkResourceId')]"
},
"groupId": "[if(variables('is-private-link-service-type'), null(), parameters('privateLinkResourceType'))]",
"requestMessage": "Private link service from AFD"
}
}
}
]
}