Host Static Website through Storage Account and Azure Front Door using Terraform.

Host Static Website through Storage Account and Azure Front Door using Terraform.

COPPER Nguyen

In this post, we will use Terraform to
1. Create a static website and hosted on Azure Storage Account
2. Config CDN  for this website using Azure Front Door.

Configure Terraform provider.

For provider authentication, we will use Service Principal with Client ID and Secret. To create an app registration, go to Microsoft Entra ID services, create an App Registration and grant Contributor role on the current azure subscription.

For provider use service principal authentication method, we wil enable storage_use_azuread in provider setting to disable Access Key authentication method on Storage Account (Will see later).

# Azure Provider source and version being used
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "3.84.0"
    }
  }
}

# Configure the Microsoft Azure Provider
provider "azurerm" {
  features {
  }
  storage_use_azuread = true
}
provider.tf

After creating App Registration credetial, use Azure CLI to login and set azure subscription id.

az login --service-principal -u $CLIENT_ID -p $CLIENT_PASSWORD --tenant $TENANT_ID
az account set --subscription $SUBSCRIPTION_ID

Configure Terraform variables.

We only create a Resource Group, a Storage Account and Front Door so these variables is enough.

variable "resource_group_name" {
  type = string
}

variable "fd_name" {
  type = string
}

variable "web_name" {
  type = string
}

variable "location" {
  type = string
}
variable "tags" {
  type = map(any)
}
variables.tf

I created ".auto.tfvars" to set value for those variables.

location            = "southeastasia"
resource_group_name = "acc1"
fd_name             = "acc1-fd"
web_name            = "acc1fdweb"
tags                = {}

Init Terraform Project

We will create main.tf this file include all resource to be created. Run tf init to initial project. The directory structure is like


Desktop/acc1/static-web  
❯ tree
.
├── main.tf
├── outputs.tf
├── provider.tf
├── .auto.tfvars
├── terraform.tfstate
└── variables.tf

Create Resource Group

resource "azurerm_resource_group" "this" {
  name     = var.resource_group_name
  location = var.location
  tags     = var.tags
}

Create Storage Account

We create a storage account, enable static website setting and also disable shared_access_key_enabled (Only allow use Azure AD to authenticate)

resource "azurerm_storage_account" "web" {
  name                            = var.web_name
  resource_group_name             = azurerm_resource_group.this.name
  location                        = azurerm_resource_group.this.location
  account_tier                    = "Standard"
  account_replication_type        = "GRS"
  access_tier                     = "Hot"
  shared_access_key_enabled       = false
  default_to_oauth_authentication = true
  static_website {
    index_document     = "index.html"
    error_404_document = "index.html"
  }
  infrastructure_encryption_enabled = true
  routing {
    publish_microsoft_endpoints = true
  }
  tags = var.tags
}

After running tf apply -auto-approve we will see this resource in Azure Portal.

Note:
To be able to view/upload blog data you must grant a data access role, such as Storage Blob Data Reader or Storage Blob Data Contributor.

The website setting.

The "$web" container, where we will upload static file.

Create Front Door

We will create  a Front Door Profile, an Endpojnt, configuration default route to forward request to Storage Website. For content caching, we only enable cache for specific file, css, js, image for example.

resource "azurerm_cdn_frontdoor_profile" "this" {
  name                     = var.fd_name
  resource_group_name      = azurerm_resource_group.this.name
  response_timeout_seconds = 16
  sku_name                 = "Standard_AzureFrontDoor"
  tags                     = var.tags
}

resource "azurerm_cdn_frontdoor_endpoint" "web" {
  name                     = "web"
  cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.this.id
  tags                     = var.tags
}

resource "azurerm_cdn_frontdoor_rule_set" "cache" {
  name                     = "caching"
  cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.this.id
}

resource "azurerm_cdn_frontdoor_rule" "cache" {
  depends_on = [
    azurerm_cdn_frontdoor_origin_group.web,
    azurerm_cdn_frontdoor_origin.web
  ]
  name                      = "static"
  cdn_frontdoor_rule_set_id = azurerm_cdn_frontdoor_rule_set.cache.id
  order                     = 1
  behavior_on_match         = "Stop"
  conditions {
    url_file_extension_condition {
      operator     = "Equal"
      match_values = ["css", "js", "ico", "png", "jpeg", "jpg", ".map"]
    }
  }
  actions {
    route_configuration_override_action {
      compression_enabled = true
      cache_behavior      = "HonorOrigin"
    }
  }
}

resource "azurerm_cdn_frontdoor_origin_group" "web" {
  name                     = "web"
  cdn_frontdoor_profile_id = azurerm_cdn_frontdoor_profile.this.id

  load_balancing {
    additional_latency_in_milliseconds = 0
    sample_size                        = 16
    successful_samples_required        = 3
  }
}

resource "azurerm_cdn_frontdoor_origin" "web" {
  depends_on                     = [azurerm_storage_account.web]
  name                           = "web"
  cdn_frontdoor_origin_group_id  = azurerm_cdn_frontdoor_origin_group.web.id
  enabled                        = true
  certificate_name_check_enabled = false
  host_name                      = azurerm_storage_account.web.primary_web_host
  http_port                      = 80
  https_port                     = 443
  origin_host_header             = azurerm_storage_account.web.primary_web_host
  priority                       = 1
  weight                         = 1
}


resource "azurerm_cdn_frontdoor_route" "default" {
  name                          = "default"
  cdn_frontdoor_endpoint_id     = azurerm_cdn_frontdoor_endpoint.web.id
  cdn_frontdoor_origin_group_id = azurerm_cdn_frontdoor_origin_group.web.id
  cdn_frontdoor_origin_ids      = [azurerm_cdn_frontdoor_origin.web.id]
  cdn_frontdoor_rule_set_ids    = [azurerm_cdn_frontdoor_rule_set.cache.id]
  enabled                       = true

  forwarding_protocol    = "MatchRequest"
  https_redirect_enabled = true
  patterns_to_match      = ["/*"]
  supported_protocols    = ["Http", "Https"]
  link_to_default_domain = true
}

After running terraform apply you will see the Front Door in Azure Portal

Uploading source code to Storage Account.

I will use https://create-react-app.dev/ to create a reactjs app, then build to static file. After building we have these files.

my-reactjs-app
❯ tree build
build
├── asset-manifest.json
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
├── robots.txt
└── static
    ├── css
    │   ├── main.f855e6bc.css
    │   └── main.f855e6bc.css.map
    ├── js
    │   ├── 787.c5090bc2.chunk.js
    │   ├── 787.c5090bc2.chunk.js.map
    │   ├── main.4e817f22.js
    │   ├── main.4e817f22.js.LICENSE.txt
    │   └── main.4e817f22.js.map
    └── media
        └── logo.6ce24c58023cc2f8fd88fe9d219db6c6.svg

To be able to sync the build file to Storage Account, i will use AZCopy. Download and installing this tool on your os.

Login with azcopy
Set the environment variable AZCOPY_SPA_CLIENT_SECRET to the client secret for secret based service principal auth.
azcopy login --service-principal --tenant <your tenant id> --application-id <your service principal's application ID>

Sync build file

Run this script to sync build file to $web container on Storage Account.

azcopy sync build 'https://acc1fdweb.blob.core.windows.net/$web' --recursive=true

Go to Portal to verify

Copy the Front Door endpoint, this is our website address.

Our website is showed.