Skip to main content
Jorge Bernhardt Jorge Bernhardt
  1. Posts/

Terraform - Deploying Static Websites Using Storage Accounts and Azure CDN

·1568 words·8 mins· 100 views · 5 likes ·
Terraform IaC Azure Storage Account Microsoft Azure

Static websites are gaining popularity due to their simplicity, security, and fast loading times. They are particularly useful for applications where the content is not updated frequently.

Azure Storage provides a convenient and cost-effective way to host such static websites, serving content directly from storage containers without the need for a web server. This method is not only efficient and cost-effective but also supports features like custom domains, SSL, and seamless integration with Azure’s global content delivery network (CDN) for improved performance.

Deploying a static website can be made effortless and streamlined with Terraform, an infrastructure as code tool that automates the deployment and management of cloud resources.

In this post, I will show you how to deploy a static website on Azure with ease. We will make use of Azure Storage Accounts for hosting and can also integrate it with Azure CDN to enhance its performance. By following this guide, you’ll gain the knowledge and skills to deploy your static website on Azure with Terraform in an easy way. Let’s start!

Prerequisites>

Prerequisites #

  • You need Terraform CLI on your local machine, if you’re new to using Terraform to deploy Microsoft Azure resources, then I recommend you check out this link.
  • A text editor or IDE of your choice (Visual Studio Code with Terraform extension is my recommendation)
Declare Azure Provider in Terraform>

Declare Azure Provider in Terraform #

The provider.tf file in Terraform is used to specify and configure the providers used in your Terraform configuration. A provider is a service or platform where the resources will be managed. This could be a cloud provider like Microsoft Azure, AWS, Google Cloud, etc.

This file is important because it tells Terraform which provider’s API to use when creating, updating, and deleting resources. Without it, Terraform wouldn’t know where to manage your resources.

provider "azurerm" {
  features {}
}
Deploy Azure Resources Using Terraform>

Deploy Azure Resources Using Terraform #

In this example, deploying a static website using Terraform on Azure, the main.tf file comprises these principal components:

  • locals: This block defines a mapping for the static websites and their configurations.
  • azurerm_resource_group: This block creates the resource groups where our static websites and their associated resources will be deployed.
  • azurerm_storage_account: Here, we’re setting up Azure Storage Accounts, which will host our static website’s files. Within this resource, we specify the static_website block to enable the static website feature on Azure Storage.
  • azurerm_storage_blob: This block manages the actual content of our static websites. It sets up the blobs (files) within the $web container of our storage accounts. We’ve separate blocks for the index page and the 404 error page.
  • azurerm_cdn_profile: If the website is set to use a CDN, this block creates the Azure CDN profile. CDNs improve the performance of our static website by caching content closer to end users.
  • azurerm_cdn_endpoint: This component is responsible for the actual CDN endpoint creation, linking the CDN profile to the storage account and defining the origin from which the CDN will pull the website content.
locals {
  sites = var.sites
}

# Resource Group for each site
resource "azurerm_resource_group" "rg" {
  for_each = local.sites

  name     = each.value.resource_group_name
  location = each.value.location
  tags     = var.tags
}

# Storage Account for each site to host static content
resource "azurerm_storage_account" "sa" {
  for_each = local.sites

  name                      = each.value.storage_account_name
  resource_group_name       = azurerm_resource_group.rg[each.key].name
  location                  = azurerm_resource_group.rg[each.key].location
  account_tier              = "Standard"
  account_replication_type  = "LRS"
  enable_https_traffic_only = true

  static_website {
    index_document     = each.value.index_document
    error_404_document = each.value.error_404_document
  }

  tags = var.tags
}

# Blob for the index page of each site
resource "azurerm_storage_blob" "index_blob" {
  for_each = local.sites

  name                   = each.value.index_document
  storage_account_name   = azurerm_storage_account.sa[each.key].name
  storage_container_name = "$web"
  type                   = "Block"
  source_content         = file(each.value.index_html_path)
  content_type           = "text/html"
}

# Blob for the error page of each site
resource "azurerm_storage_blob" "error_blob" {
  for_each = local.sites

  name                   = each.value.error_404_document
  storage_account_name   = azurerm_storage_account.sa[each.key].name
  storage_container_name = "$web"
  type                   = "Block"
  source_content         = file(each.value.error_404_html_path)
  content_type           = "text/html"
}

# CDN Profile for sites where CDN is requested
resource "azurerm_cdn_profile" "cdn_profile" {
  for_each = { for k, v in local.sites : k => v if v.deploy_cdn == true }

  name                = "${each.value.storage_account_name}-cdnprofile"
  resource_group_name = azurerm_resource_group.rg[each.key].name
  location            = azurerm_resource_group.rg[each.key].location
  sku                 = "Standard_Microsoft"
  tags                = var.tags
}

# CDN Endpoint for sites where CDN is requested
resource "azurerm_cdn_endpoint" "cdn_endpoint" {
  for_each = { for k, v in local.sites : k => v if v.deploy_cdn == true }

  name                = "${each.value.storage_account_name}-cdnendpoint"
  profile_name        = azurerm_cdn_profile.cdn_profile[each.key].name
  resource_group_name = azurerm_resource_group.rg[each.key].name
  location            = azurerm_resource_group.rg[each.key].location
  is_http_allowed     = false
  is_https_allowed    = true

  origin {
    name      = "contentOrigin"
    host_name = azurerm_storage_account.sa[each.key].primary_web_host
  }

  origin_host_header = azurerm_storage_account.sa[each.key].primary_web_host
  tags               = var.tags
}
Declaration of input variables>

Declaration of input variables #

In Terraform, the variables.tf file is fundamental for making our configurations more dynamic. It allows us to define crucial parameters to be used in our main.tf. By using variables, we can create adaptable configurations suitable for different use cases.

In this example, the variables defined in the variables.tf include:

  • sites: This complex variable is designed to capture a map of attributes essential for each static website’s deployment:
    • resource_group_name: The name of the Azure resource group where the storage account for the static website will be created.
    • storage_account_name: The unique name of the Azure storage account.
    • location: Specifies the Azure region where the resources will be deployed.
    • index_document: The name of the index document for the static website.
    • error_404_document: The name of the custom error document.
    • index_html_path: Path to the file that will be used as the index document for the static website.
    • error_404_html_path: Path to the file that will be used as the custom error document.
    • deploy_cdn: A boolean value indicating whether a CDN endpoint should be deployed for the static website.
  • tags: A map of metadata tags to assign to all the resources created.
variable "sites" {
  type = map(object({
    resource_group_name  = string
    storage_account_name = string
    location             = string
    index_document       = string
    error_404_document   = string
    index_html_path      = string
    error_404_html_path  = string
    deploy_cdn           = bool
  }))

  validation {
    # Validate that the storage account name is between 3 and 24 characters in length and only contains lower-case letters and numbers
    condition = alltrue([
      for k, v in var.sites :
      length(v.storage_account_name) >= 3 && length(v.storage_account_name) <= 24 && can(regex("^[a-z0-9]*$", v.storage_account_name))
    ])
    error_message = "The storage account name must be between 3 and 24 characters in length and use numbers and lower-case letters only."
  }
}


// Common tags for all Azure resources created
variable "tags" {
  description = "Common tags for all resources"

  # Define the type structure for the tags
  type = object({
    Environment = string
    Terraform   = string
  })

  # Default values for tags
  default = {
    Environment = "www.jorgebernhardt.com"
    Terraform   = "true"
  }
}

Validation Block: This is our safeguard. It checks the storage_account_name attribute to ensure that the name length is between 3 and 24 characters and contains only lowercase letters and numbers. Such validations catch potential misconfigurations early in the process, ensuring a smoother deployment with fewer errors.

Declaration of output values>

Declaration of output values #

The output.tf file in Terraform extracts and displays information about the resources created or managed by your Terraform configuration. These outputs are defined using the output keyword and can be used to return information that can be useful for the user, for other Terraform configurations, or for programmatically using the information in scripts or other tools.

In this example, the output.tf file returns the following information:

  • The names of the created storage accounts.
  • The URLs to access the static websites deployed on these storage accounts.
  • The optional CDN URLs if the CDN deployment was enabled for the static site.

After Terraform successfully applies your configuration, it will display your defined outputs. This provides instant access to key attributes of the resources you’ve set up.

output "site_info" {
  value = {
    for key, sa in azurerm_storage_account.sa :
    key => {
      storage_account_name = sa.name,
      static_website_url   = "https://${sa.primary_web_host}",
      cdn_url              = var.sites[key].deploy_cdn ? "https://${azurerm_cdn_endpoint.cdn_endpoint[key].name}.azureedge.net" : null,
    }
  }
  description = "Details of the static websites, including storage account names, URLs, and optional CDN URLs."
}
Executing the Terraform Deployment>

Executing the Terraform Deployment #

Now that you’ve declared the resources correctly, it’s time to take the following steps to deploy them in your Azure environment.

  • Initialization: To begin, execute the terraform init command. This will initialize your working directory that holds the .tf files and download the provider specified in the provider.tf file, and configure the Terraform backend. I suggest looking at this link if you’re curious about the process.

  • Planning: Next, execute the terraform plan. This command creates an execution plan and shows Terraform’s actions to achieve the desired state defined in your .tf files. This gives you a chance to review the changes before applying them.

  • Apply: When you’re satisfied with the plan, execute the terraform apply command. This will implement the required modifications to attain the intended infrastructure state. Before making any changes, you will be asked to confirm your decision.

  • Inspection: After applying the changes, you can use terraform show command to see the current state of your infrastructure.

  • Destroy (optional): when a project is no longer needed or when resources have become outdated. You can use the terraform destroy command. This will remove all the resources that Terraform has created.

References and useful links #

Thank you for taking the time to read my post. I sincerely hope that you find it helpful.