Azure

Getting Started with Azure Front Door and Terraform

What is Azure Front Door?

Azure Front Door is basically a layer 7 global load balancer, global router with url based routing, WAF (Web Application Firewall) and web traffic manager all in one.

I recommend reading the Azure Front Door documentation for further details.

Create Azure Front Door

To create an Azure Front Door you can use the Azure Portal, there are a couple of examples you can follow to do that:

Creating Azure Front Door via the Azure Portal is a good start point to understand how it works, but for this example I am going to create IaC (Infrastructure as Code) to setup a basic Azure Front Door.

I have recently started using Terraform for building Azure resources and so I will use that here to create an Azure Front Door.

Requirements

  • Make sure I can build Terraform configurations (I am using a Docker container from my previous article – IaC with Containers)
  • Update Terraform to latest (at the time of writing it was 0.12.26)
  • Make sure the configuration is shareable
  • Support multiple configurations and rules

Right, I’ve got my container, updated Terraform and now need to look up sharing Terraform configurations.

Terraform uses modules for sharing configurations and the documentation is quite good. This seems a lot nicer than building linked ARM (Azure Resource Management) templates, as you can have shareable modules locally without having to use blob storage.

You can also take advantage of the Terraform Public Registry or sign up for Terraform Cloud which supports using a Private Registry.

UPDATE 04/02/2023 – I have been contacted a number of times about updating the module described in this post to support newer versions of terraform. In light of this I have created another version of the terraform module using Terraform 1.3.1 and azurerm provider 3.42.0 that can be found on my GitHub, this version can be updated independently of the one shown in this post. If you find any issues with this new template feel free to create a PR with the required changes or take a copy of it an update for your own projects. As Microsoft now consider this version of FrontDoor to be classic and Hashicorp will be deprecating it in v4.0 of the provider I will not be making any further changes to this module or keeping track of Terraform breaking changes.

Creation

So I need to create a folder for the module (I’ll name it frontdoor), a main.tf, variables.tf, outputs.tf and README.md.

Main.tf

Terraform includes azurerm_frontdoor resource in order to create an Azure Front Door.

Azure Front Door has a lot of settings and there are many parts, so let’s go through them a bit at a time.

Note: a lot of the sections allow a list of items (Load Balancing, Routing Rule, Backend Pool, Frontend Endpoint, etc.), this is to allow for multiple configurations and rules to be setup in one go.

Basic
  • Azure Front Door name
  • Resource Group name for Azure Front Door
  • Load balancer enabled
  • Backend pools
    • Certificate name check – enforce name check on HTTPS requests
    • Send/Receive Timeout – timeout forwarding the request to the backend
  • Tags – always good to tag your resources
# Create front door
resource "azurerm_frontdoor" "instance" {
  name                                         = var.frontdoor_name
  resource_group_name                          = var.frontdoor_resource_group_name
  enforce_backend_pools_certificate_name_check = var.enforce_backend_pools_certificate_name_check
  load_balancer_enabled                        = var.frontdoor_loadbalancer_enabled
  backend_pools_send_receive_timeout_seconds   = var.backend_pools_send_receive_timeout_seconds
  tags                                         = var.tags
}
Load Balancing
  • Name
  • Sample size – number of samples to use for load balancing decisions
  • Successful samples required – how many samples must succeed to be considered successful
  • Additional latency – how many milliseconds for probes to fall into the low latency bucket
  dynamic "backend_pool_load_balancing" {
    for_each = var.frontdoor_loadbalancer
    content {
      name                            = backend_pool_load_balancing.value.name
      sample_size                     = backend_pool_load_balancing.value.sample_size
      successful_samples_required     = backend_pool_load_balancing.value.successful_samples_required
      additional_latency_milliseconds = backend_pool_load_balancing.value.successful_samples_required
    }
  }
Routing Rule
  • Name
  • Accepted protocols – e.g. Http, Https
  • Patterns for route match – e.g. “/*”, “/mypath”, “/mypath/*”
  • Enabled
  • Forwarding or Redirect configuration
  dynamic "routing_rule" {
    for_each = var.frontdoor_routing_rule
    content {
        name               = routing_rule.value.name
        accepted_protocols = routing_rule.value.accepted_protocols
        patterns_to_match  = routing_rule.value.patterns_to_match        
        frontend_endpoints = values({for x, endpoint in var.frontend_endpoint : x => endpoint.name})
        dynamic "forwarding_configuration" {
          for_each = routing_rule.value.configuration == "Forwarding" ? routing_rule.value.forwarding_configuration : []
          content {
            backend_pool_name                     = forwarding_configuration.value.backend_pool_name
            cache_enabled                         = forwarding_configuration.value.cache_enabled                           
            cache_use_dynamic_compression         = forwarding_configuration.value.cache_use_dynamic_compression 
            cache_query_parameter_strip_directive = forwarding_configuration.value.cache_query_parameter_strip_directive
            custom_forwarding_path                = forwarding_configuration.value.custom_forwarding_path
            forwarding_protocol                   = forwarding_configuration.value.forwarding_protocol
          }
        }
        dynamic "redirect_configuration" {
          for_each = routing_rule.value.configuration == "Redirecting" ? routing_rule.value.redirect_configuration : []
          content {
            custom_host         = redirect_configuration.value.custom_host
            redirect_protocol   = redirect_configuration.value.redirect_protocol
            redirect_type       = redirect_configuration.value.redirect_type
            custom_fragment     = redirect_configuration.value.custom_fragment
            custom_path         = redirect_configuration.value.custom_path
            custom_query_string = redirect_configuration.value.custom_query_string
          }
        }
    }
  }

As the Frontend Endpoints are configured separately, being able to find a way to reuse the names to configure the frontend_endpoints for the routing was invaluable. The values function allows to read just the values from the given object field. The expression is very similar to C#, using a lambda (=>) to project just the name field to then get values from.

frontend_endpoints = values({for x, endpoint in var.frontend_endpoint : x => endpoint.name})
Health Probe
  • Name
  • Enabled
  • Path
  • Protocol – e.g. Http, Https
  • Probe method – e.g. HEAD, GET
  • Interval – interval between each health probe
 dynamic "backend_pool_health_probe" {
    for_each = var.frontdoor_health_probe
    content {
      name                = backend_pool_health_probe.value.name
      enabled             = backend_pool_health_probe.value.enabled
      path                = backend_pool_health_probe.value.path
      protocol            = backend_pool_health_probe.value.protocol
      probe_method        = backend_pool_health_probe.value.probe_method
      interval_in_seconds = backend_pool_health_probe.value.interval_in_seconds
    }  
  }
Backend Pool
  • Name
  • Load Balancer name
  • Health probe name
  • Backend
    • Enabled
    • Host Header
    • Address
    • HTTP port
    • HTTPS port
    • Priority
    • Weight
  dynamic "backend_pool" {
    for_each = var.frontdoor_backend
    content {
       name                = backend_pool.value.name      
       load_balancing_name = backend_pool.value.loadbalancing_name
       health_probe_name   = backend_pool.value.health_probe_name

       dynamic "backend" {
        for_each = backend_pool.value.backend
        content {
          enabled     = backend.value.enabled
          address     = backend.value.address
          host_header = backend.value.host_header
          http_port   = backend.value.http_port
          https_port  = backend.value.https_port
          priority    = backend.value.priority
          weight      = backend.value.weight
        }
      }
    }
  }

Frontend Endpoint
  • Name
  • Host Name
  • Custom Domain
  • Session Affinity
  • WAF Policy ID
  dynamic "frontend_endpoint" {
    for_each = var.frontend_endpoint
    content {
      name                                    = frontend_endpoint.value.name
      host_name                               = frontend_endpoint.value.host_name
      custom_https_provisioning_enabled       = frontend_endpoint.value.custom_https_provisioning_enabled    
      session_affinity_enabled                = frontend_endpoint.value.session_affinity_enabled
      session_affinity_ttl_seconds            = frontend_endpoint.value.session_affinity_ttl_seconds
      web_application_firewall_policy_link_id = frontend_endpoint.value.waf_policy_link_id
      dynamic "custom_https_configuration" {
        for_each = frontend_endpoint.value.custom_https_provisioning_enabled == false ? [] : list(frontend_endpoint.value.custom_https_configuration.certificate_source)
        content {
          certificate_source = custom_https_configuration.value.certificate_source
        }
      }
    }
  }

Variables.tf

All the variables that are defined for the Module.

variable "frontdoor_resource_group_name" {
  description = "(Required) Resource Group name"
  type = string
}

variable "frontdoor_name" {
  description = "(Required) Name of the Azure Front Door to create"
  type = string
}

variable "frontdoor_loadbalancer_enabled" {
  description = "(Required) Enable the load balancer for Azure Front Door"
  type = bool
}

variable "enforce_backend_pools_certificate_name_check" {
  description = "Enforce the certificate name check for Azure Front Door"
  type = bool
  default = false
}

variable "backend_pools_send_receive_timeout_seconds" {
  description = "Set the send/receive timeout for Azure Front Door"
  type = number
  default = 60
}

variable "tags" {
  description = "(Required) Tags for Azure Front Door"  
}

variable "frontend_endpoint" {
  description = "(Required) Frontend Endpoints for Azure Front Door"
}

variable "frontdoor_routing_rule" {
  description = "(Required) Routing rules for Azure Front Door"
}

variable "frontdoor_loadbalancer" {
  description = "(Required) Load Balancer settings for Azure Front Door"
}

variable "frontdoor_health_probe" {
  description = "(Required) Health Probe settings for Azure Front Door"
}

variable "frontdoor_backend" {
  description = "(Required) Backend settings for Azure Front Door"
}

Example of Use

Make sure that Terraform is not less than 0.12.x and that the provider (azurerm) is using the latest version (at the time of writing this was 2.14.0).

terraform {
  required_version = ">= 0.12"
}
# Configure the Azure Provider
provider "azurerm" {
  # whilst the `version` attribute is optional, we recommend pinning to a given version of the Provider
  version = "=2.14.0"
  features {}
}

# Create a resource group
resource "azurerm_resource_group" "instance" {
  name     = "my-frontdoor-rg"
  location = "westeurope"
}

# Create Front Door
module "front-door" {
  source                                            = "./modules/frontdoor"    
  tags                                              = { Department = "Ops"}
  frontdoor_resource_group_name                     = azurerm_resource_group.instance.name
  frontdoor_name                                    = "my-frontdoor"
  frontdoor_loadbalancer_enabled                    = true
  backend_pools_send_receive_timeout_seconds        = 240
    
  frontend_endpoint      = [{
      name                                    = "my-frontdoor-frontend-endpoint"
      host_name                               = "my-frontdoor.azurefd.net"
      custom_https_provisioning_enabled       = false
      custom_https_configuration              = { certificate_source = "FrontDoor"}
      session_affinity_enabled                = false
      session_affinity_ttl_seconds            = 0
      waf_policy_link_id                      = ""
  }]

  frontdoor_routing_rule = [{
      name               = "my-routing-rule"
      accepted_protocols = ["Http", "Https"] 
      patterns_to_match  = ["/*"]
      enabled            = true              
      configuration      = "Forwarding"
      forwarding_configuration = [{
        backend_pool_name                     = "backendBing"
        cache_enabled                         = false       
        cache_use_dynamic_compression         = false       
        cache_query_parameter_strip_directive = "StripNone" 
        custom_forwarding_path                = ""
        forwarding_protocol                   = "MatchRequest"   
      }]      
  }]

  frontdoor_loadbalancer =  [{      
      name                            = "loadbalancer"
      sample_size                     = 4
      successful_samples_required     = 2
      additional_latency_milliseconds = 0
  }]

  frontdoor_health_probe = [{      
      name                = "healthprobe"
      enabled             = true
      path                = "/"
      protocol            = "Http"
      probe_method        = "HEAD"
      interval_in_seconds = 60
  }]

  frontdoor_backend =  [{
      name               = "backendBing"
      loadbalancing_name = "loadbalancer"
      health_probe_name  = "healthprobe"
      backend = [{
        enabled     = true
        host_header = "www.bing.com"
        address     = "www.bing.com"
        http_port   = 80
        https_port  = 443
        priority    = 1
        weight      = 50
      }]
  }]
}

The code for this article and full module can be found in my GitHub repository.

I’ve ran the example with the newly created module, let’s take a look at the Azure Portal to see if an Azure Front Door was created.

It looks like everything was setup and working, selecting the link my-frontdoor.azurefd.net forwarded to Bing.com as the example was configured to do.

Note: Azure Front Door configuration can be viewed and updated via the Azure CLI.

Summary

I am sure there is more to learn about Terraform and Azure Front Door and this configuration may well get updated in the future as I learn more. I’ve not only gained a better understanding of what Terraform has to offer, but what Azure Front Door has to offer as well.

UX

UX: The Problems with Language Selection

Over the past few years, I have been working in back-ends and cloud infrastructure/architecture and have not worked much with front-ends at all, let alone the user experience. It has also been a long time since I worked on anything that needed any translation.

Recently the subject of translation had been mentioned for websites and I wondered if anything had changed since I last worked with multilingual websites with regard to the user experience.

It appears that designing multilingual websites is still a challenge even in 2020, there are still websites tying regions and languages together, flags to pick languages and language direction not always being applied.

We have to think about the whole user experience, what language is shown as the default? does the language require the layout to be right to left or left to right? If so what other controls need to be changed? what font size is required for each language so it displays correctly? Are images, icons, colours culturally appropriate? Will they offend?

There a lots of things to consider around user experience, but for this article I am just concentrating on language selection but the other things are equally important and need to be considered.

Language Selection

We live in a world where people live and work in multiple countries, some move to other countries and we go on holidays in other countries. This means we can no longer say everyone viewing my website in a given region speaks and reads the main language used.

Imagine a tourist website in England that only supported the English language, we would be expecting all the visitors coming for a holiday and finding places to go to also be fluent in the language. Now I think we can all agree that picking up a new language just to go away for a few weeks is a bit much, not that we wouldn’t try to speak some, but reading an entire website in a new language involves knowing a lot more.

So let’s looks at the issues for language selection:

Associating a Region and Language Together

There are many reasons why you might have your website in different regions, for example you might want or need to have different products or services available, or simply to make a region specific version of your website. For each region you add you may need to support additional languages.

Nike is a great example of a website that sells products depending on the region you are in. Not all items are available in all regions and shipping is only available for the selected region.

Scenario: You are working in France for 6 months, you’ve moved there temporarily for work and your grasp of French is not great and you need to buy some footwear. So you navigate to Nike’s shop and picked the region you are in.

Now the website is loaded, the language selected is French (Français), but there is no way to select another language which is not very helpful for this scenario.

Nike support a lot of languages and regions, separating the region and language selection would surely be a benefit and resolve this type of scenario. So for this scenario, if you were English, the language is supported in other regions just not the one you are in.

Where there are multiple official languages for a region, Switzerland for example have four languages, German (Deutsch) currently making up the largest share of around 63%, French (Français) around 23%, Italian (Italiano) around 8% and Romansh around 0.5%. Nike’s solution to this is separate entries for each supported variation, this would become difficult to navigate if more language choices were added per region in the same way.

Nike Region Selection Europe

Whilst I have used Nike as an example there are other companies doing similar.

Adobe Region Selection
Spotify Region Selection

I can understand why these region language combinations exist, its a one click selection, getting the user to a region with a language that they understand. But in providing a simple user experience it has missed other scenarios like the one above.

Associating a Flag with a Language

A countries flag is a representation of a nation, a symbol of recognition, however it is not a representation of the language spoken by the people. Using Switzerland again as an example, they currently have four official languages, so picking the Swiss flag would render which language? The current most popular? Would there be four Swiss flags one with each language next to it? It raises a lot of questions.

Then we are back to the scenario used above, you are in France and want to view the website in English, picking the English flag (for example) just feels odd when selecting the language.

There are known issues using flags and some of those have caused offence, from the wrong flag being shown, the wrong language being picked for the flag or the user is simply not from the region but speaks the language e.g. Using the flag of Spain for selection but the Spanish language is spoken in many different countries, Spain, Mexico, Columbia, Venezuela, etc.

Using flags for language selection is best avoided entirely.

Changing Language Direction

This article has been discussing language selection and why associating regions or flags with language have issues, but some languages require a change in direction reading left to right or right to left. It is important to understand that certain languages require this level of change and need to be thought about when supporting such languages and what happens when they are selected.

For example if a website was in the region of Saudi Arabia and it only supported the official languages English and Arabic. The website would need to render left to right for English and right to left for Arabic and not just change the text to the appropriate language.

Conclusion

So when we need to support multiple regions and languages decoupling the region and the language would improve the user experience. We should consider which languages are supported and which ones are the default for a given region e.g. picking the French region and defaulting to French (Français) but allow your users to select your other supported languages. We also need to make sure that we have the correct reading direction and consider any icons, colours, etc. that might cause offence in the region. There is plenty of information out there but I think this colour chart shows cultural colours really nicely.

The messages to take away are region does != language and flag does != language.

And remember when you are creating a language picker, display the language names as they would be in the requested language e.g.
English
Français
Deutsch
Italiano
Español
العربية
中文

I hope this was useful and helps build better user experiences for multilingual websites.