Skip to content
Introduction to Azure Serverless Web App Architecture
todd-bernson-leadership

Serverless architectures provide a powerful, scalable, and cost-effective approach for deploying web applications. This post walks you through the architecture of a serverless web app that leverages Azure services for both static and dynamic content, including Azure CDN, Storage Account, Function App, and a custom domain set up with Route 53.

The architecture for this Azure serverless web application is designed with simplicity and scalability in mind, powered by Terraform for efficient infrastructure provisioning. Let's dive into the components of this architecture and how they interact to serve both static and dynamic content to users.

Architecture Overview

Our Azure serverless web app is divided into two primary components:

  1. Static Content Delivery: Azure CDN and Azure Storage Account work together to serve the static files (HTML, CSS, and JavaScript).
  2. Dynamic Content Handling: An Azure Function App processes dynamic requests from the front end, enabling serverless computing capabilities.

Static Content with Azure CDN and Storage Account

The static assets are stored in an Azure Storage Account and are distributed via Azure CDN. The CDN reduces latency by caching content at strategic locations, providing faster load times for users around the globe. Using Terraform, we set up both the Storage Account and the CDN to make this process seamless.

Terraform Code Snippet - Storage Account and CDN

resource "azurerm_storage_account" "this" {
  name = replace(var.project, "_", "")

  resource_group_name = azurerm_resource_group.this.name
  location            = azurerm_resource_group.this.location

  account_tier             = "Standard"
  account_replication_type = "LRS"
}

resource "azurerm_storage_blob" "index_html" {
  name                   = "index.html"
  storage_account_name   = azurerm_storage_account.this.name
  storage_container_name = azurerm_storage_container.sitefiles.name
  type                   = "Block"
  content_type           = "text/html"

  source_content = templatefile("${local.site_files_index_template_dir}/index.html.tmpl", {
    function_app_url = azurerm_linux_function_app.this.default_hostname
    function_name    = azurerm_linux_function_app.this.name
  })
}

resource "azurerm_storage_blob" "site_files" {
  for_each = fileset(local.site_files_dir, "**/*")

  name = each.value

  content_type           = lookup(local.mime_types, split(".", each.value)[length(split(".", each.value)) - 1])
  source                 = "${local.site_files_dir}/${each.value}"
  storage_account_name   = azurerm_storage_account.this.name
  storage_container_name = azurerm_storage_container.sitefiles.name
  type                   = "Block"
}

resource "azurerm_storage_container" "function_code" {
  name                  = local.function_code_directory
  storage_account_name  = azurerm_storage_account.this.name
  container_access_type = "private"
}

resource "azurerm_storage_container" "sitefiles" {
  name                  = local.storage_sitefiles_path
  storage_account_name  = azurerm_storage_account.this.name
  container_access_type = "blob"
}

resource "azurerm_cdn_profile" "this" {
  name = local.project

  location            = azurerm_resource_group.this.location
  resource_group_name = azurerm_resource_group.this.name

  sku = var.cdn_profile_sku
}

resource "azurerm_cdn_endpoint" "this" {
  name = local.project

  location            = azurerm_resource_group.this.location
  resource_group_name = azurerm_resource_group.this.name

  optimization_type  = "GeneralWebDelivery"
  origin_host_header = azurerm_storage_account.this.primary_blob_host
  origin_path        = "/${local.storage_sitefiles_path}"
  profile_name       = azurerm_cdn_profile.this.name

  origin {
    name       = local.project
    host_name  = azurerm_storage_account.this.primary_blob_host
    http_port  = 80
    https_port = 443
  }

  is_compression_enabled    = true
  content_types_to_compress = ["text/html", "text/css", "application/javascript"]

  is_http_allowed  = false
  is_https_allowed = true
}

resource "azurerm_cdn_endpoint_custom_domain" "this" {
  name            = replace(local.site_domain, ".", "-")
  cdn_endpoint_id = azurerm_cdn_endpoint.this.id
  host_name       = local.site_domain

  cdn_managed_https {
    certificate_type = "Dedicated"
    protocol_type    = "ServerNameIndication"
    tls_version      = "TLS12"
  }
}

Dynamic Content with Azure Function App

For the backend, we use an Azure Function App to handle requests that require dynamic processing. This serverless function can be triggered by HTTP requests, enabling real-time interactions without the need for dedicated server resources.

Terraform Code Snippet - Function App

resource "azurerm_linux_function_app" "this" {
  name                = "${local.project}-functions"
  resource_group_name = azurerm_resource_group.this.name
  location            = azurerm_resource_group.this.location

  storage_account_name       = azurerm_storage_account.this.name
  storage_account_access_key = azurerm_storage_account.this.primary_access_key
  service_plan_id            = azurerm_service_plan.this.id

  identity {
    type = "SystemAssigned"
  }

  site_config {
    application_stack {
      node_version = "20"
    }

    cors {
      allowed_origins = [
        "https://${local.site_domain}"
      ]
    }
  }

  app_settings = {
    "WEBSITE_RUN_FROM_PACKAGE" = "${azurerm_storage_account.this.primary_blob_endpoint}${local.function_code_directory}/${local.function_zip_file}"
  }
}

resource "azurerm_role_assignment" "function_storage_access" {
  scope                = azurerm_storage_account.this.id
  role_definition_name = "Storage Blob Data Reader"
  principal_id         = azurerm_linux_function_app.this.identity[0].principal_id
}

resource "azurerm_service_plan" "this" {
  name = "${local.project}-service-plan"

  location            = azurerm_resource_group.this.location
  resource_group_name = azurerm_resource_group.this.name

  os_type  = local.app_function_os_type
  sku_name = "B1"
}

resource "azurerm_storage_blob" "function_code" {
  name = local.function_zip_file

  content_md5            = data.archive_file.function_code.output_md5
  source                 = data.archive_file.function_code.output_path
  storage_account_name   = azurerm_storage_account.this.name
  storage_container_name = azurerm_storage_container.function_code.name
  type                   = "Block"
}

Custom Domain Setup with Route 53

To make the web app accessible via a custom domain, we use AWS Route 53 to manage DNS and set up a CNAME record pointing to the Azure CDN endpoint. This setup allows for a professional, branded URL while leveraging Azure's infrastructure for hosting.

Terraform Code Snippet - Route 53 CNAME Record

resource "aws_route53_record" "site" {
  zone_id = data.aws_route53_zone.this.zone_id
  name    = local.site_domain
  type    = "CNAME"
  ttl     = 60

  set_identifier = local.project
  records        = [azurerm_cdn_endpoint.this.fqdn]

  weighted_routing_policy {
    weight = 100
  }
}

This Azure serverless web app architecture offers a cost-effective and scalable solution for serving both static and dynamic content. By leveraging Terraform for automation, you can easily deploy, manage, and scale this architecture with minimal effort. With Azure CDN for static files, an Azure Function App for dynamic content, and a custom domain managed via Route 53, this setup provides a powerful foundation for modern web applications.

Stay tuned for more posts in this series, where we'll dive deeper into the deployment process, best practices, and optimization strategies for Azure serverless architectures.

Related Articles

Inter-Region WireGuard VPN in AWS

Read more

Making PDFs Searchable Using AWS Textract and CloudSearch

Read more

Slack AI Bot with AWS Bedrock Part 2

Read more

Contact Us

Achieve a competitive advantage through BSC data analytics and cloud solutions.

Contact Us