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:
- Static Content Delivery: Azure CDN and Azure Storage Account work together to serve the static files (HTML, CSS, and JavaScript).
- 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.