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.

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.

Azure, Azure Pipelines

IaC Terraform with Azure Pipelines

Finishing off this series of articles about IaC with Azure Pipelines, this article is going to follow on from IaC ARM Templates with Azure Pipelines and IaC Ansible with Azure Pipelines and focus on deploying the same simple infrastructure but this time using Terraform.

Azure Pipelines – Releases

For this pipeline I am going to change the Agent Specification to use Ubuntu.

As this is running on an hosted agent, the first thing to add is a task to install Terraform. Microsoft have a task for doing just that.

The Terraform task has a default version but it’s not the latest so I’ll update that.

Note: At the time of writing this the latest version was 0.12.26.

Next I need to add a Terraform task (another Microsoft addition), the task provides the basic Terraform commands, init, validate, plan, validate and apply and destroy.

The Terraform init task requires some configuration, the provider, as I am deploying to Azure this is ‘azurerm’, the configuration directory which is the azure/storage folder in the artifacts and contains the Terraform configuration file. For this example it is named storage.tf.

The task also requires the AzureRM backend configuration to be set.

I need to add the configuration folder as previously with the init command. Also I need to add command arguments to pass variables to my Terraform configuration. Variables are passed using multiple -var command line options.

-var="resource_group_name=$(storageGroup)" -var="resource_group_location=$(storageLocation)" -var="storage_name=$(storageName)"

Note: Terraform plan creates an execution plan to determine what changes are needed between the configuration and the actual state.

Now I need to apply the Terraform configuration and any changes detected by the plan step. As with the previous steps I need to add the configuration folder and add command arguments to pass variables to my Terraform configuration.

-var="resource_group_name=$(storageGroup)" -var="resource_group_location=$(storageLocation)" -var="storage_name=$(storageName)"

The configuration above uses a number of variables, so they need to be configured as well.

I initially thought that the AzureRM backend configuration might configure the resource group and storage account when it ran but sadly this is not the case and so further setup is required.

To setup the Azure storage for the AzureRM backend I will add an Azure CLI task.

And configure the Azure CLI to create the resource group, storage account and container that is required.

az group create --location $(storageLocation) --name $(terraformGroup)
az storage account create --name $(terraformStorageName) --resource-group $(terraformGroup) --location $(storageLocation) --sku $(terrformStorageSku)
az storage container create --name $(terraformContainerName) --account-name $(terraformStorageName)

Now it’s all configured let’s run the release and see if it succeeds

Hopefully everything created successfully in Azure, let’s have a look.

Looks like everything was created as expected including the state file in blob storage.


Azure Pipelines YAML

The previous articles cover creating the pipeline in the Azure DevOps portal and so here I will just include the YAML for the Terraform pipeline configuration. As with the other examples I will use a multi-stage, multi-job pipeline and the code is on GitHub.

The Terraform configuration storage.tf can also be found on GitHub.

# A pipeline with no CI trigger
trigger: none

# No PR triggers
pr: none

stages:
- stage: build_test
  displayName: Build and Test
  jobs:
  - job: build_test
    pool:
      vmImage: 'windows-latest' # Currently Windows 2019 and Visual Studio 2019
    steps:
    - script: echo Code Built and Tested!

- stage: release
  displayName: Release
  dependsOn: build_test
  jobs:
   - job: deploy_terraform
     pool:
       vmImage: 'ubuntu-latest' # Currently Ubuntu 18.04
     steps:
      - task: AzureCLI@2
        displayName: 'Azure CLI '
        inputs:
          azureSubscription: 'Twisters Portal'
          scriptType: bash
          scriptLocation: inlineScript
          inlineScript: |
           az group create --location $(storageLocation) --name $(terraformGroup)
           az storage account create --name $(terraformStorageName) --resource-group $(terraformGroup) --location $(storageLocation) --sku $(terrformStorageSku)
           az storage container create --name $(terraformContainerName) --account-name $(terraformStorageName)
          addSpnToEnvironment: false     

      - task: TerraformInstaller@0
        displayName: 'Install Terraform 0.12.26'
        inputs:
          terraformVersion: 0.12.26

      - task: TerraformTaskV1@0
        displayName: 'Terraform : Init Azure'
        inputs:
          workingDirectory: './azure/storage'
          backendServiceArm: 'Twisters Portal'
          backendAzureRmResourceGroupName: $(terraformGroup)
          backendAzureRmStorageAccountName: $(terraformStorageName)
          backendAzureRmContainerName: $(terraformContainerName)
          backendAzureRmKey: $(terraformStateFilename)     

      - task: TerraformTaskV1@0
        displayName: 'Terraform : Plan'
        inputs:
          command: plan
          workingDirectory: './azure/storage'
          commandOptions: '-var="resource_group_name=$(storageGroup)" -var="resource_group_location=$(storageLocation)" -var="storage_name=$(storageName)"'
          environmentServiceNameAzureRM: 'Twisters Portal'

      - task: TerraformTaskV1@0
        displayName: 'Terraform : Apply'
        inputs:
          command: apply
          workingDirectory: './azure/storage'
          commandOptions: '-var="resource_group_name=$(storageGroup)" -var="resource_group_location=$(storageLocation)" -var="storage_name=$(storageName)"'
          environmentServiceNameAzureRM: 'Twisters Portal'

   - job: deploy_build
     dependsOn: deploy_terraform
     pool:
       vmImage: 'windows-latest' # Currently Windows 2019 and Visual Studio 2019
     steps:
      - script: echo Deploying Code

Time to run the pipeline and see if it works.

All looks good, let’s see if everything is correct in Azure.

I hope that this article has been useful and is a good introduction to deploying Terrform configurations with Azure Pipelines.