Using Azure, Terraform and GitHub Actions to host an (almost) free static site

22 June 2021

Let’s start this post by saying that everything below is unnecessary. The outcome from this is exactly what GitHub Pages gives you for (completely) free. Hosting in Azure comes with a very small cost (pricing explained below), but the point of this is to learn about Azure, Terraform and GitHub Actions in the process of hosting a small, static website whilst keeping the costs very low.


So what services will we be using, and how much will they cost us?

You will first need to sign up for accounts at Azure, GitHub and Terraform Cloud.


We will first need a new repository on GitHub in order to push our code to. Use the instructions provided on GitHub to create or link the repository to a local folder on your computer. Within the new repository, create a src folder and then an index.html file within the folder. We will put in some placeholder content for now:

        <h1>Hello, World</h1>

Commit and Push that to the remote repository.


Next, we will are going to create some Terraform files which will describe the resources we want to be created on Azure to host our site. Start by creating a new top level folder in your repository called .cloud.


Let’s create a file in that folder with the below contents:

terraform {
  backend "remote" {
    hostname     = ""
    organization = "MY-ORG"

    workspaces {
      name = "static-site"

  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">= 2.26"

  required_version = ">= 0.14.9"

provider "azurerm" {
  features {}
  subscription_id = var.azure_subscription_id
  client_id       = var.azure_client_id
  client_secret   = var.azure_client_secret
  tenant_id       = var.azure_tenant_id

resource "azurerm_resource_group" "static-site" {
  name     = "static-site"
  location = "uksouth"

Make sure you change the MY-ORG to match the organisation you entered when signing up to Terraform Cloud.

The above sets up some important information for us, including the Terraform backend and the connection to Azure (although the credentials for that will be dealt with later). It will also create our first resource in Azure: the Resource Group within which all our other resources (CDN, DNS, Storage) will be contained.


The next resource we will want to create is the Storage Account which will hold our static files to be read from the CDN. Create the file .cloud/ with the contents:

resource "azurerm_storage_account" "static-site" {
  name                      = "staticsitestorage"
  resource_group_name       =
  location                  = azurerm_resource_group.static-site.location
  account_tier              = "Standard"
  account_replication_type  = "LRS"
  enable_https_traffic_only = true
  static_website {
    index_document     = "index.html"
    error_404_document = "index.html"

  blob_properties {
    cors_rule {
      allowed_headers    = ["*"]
      allowed_methods    = ["GET", "HEAD"]
      allowed_origins    = ["*"]
      exposed_headers    = ["*"]
      max_age_in_seconds = 3600


Now lets add the resources to create a CDN which will be backed by the Storage Account above. This will be in a new file .cloud/

resource "azurerm_cdn_profile" "static-site" {
  name                = "static-site-cdn"
  resource_group_name =
  location            = "westeurope"
  sku                 = "Standard_Microsoft"

This adds the CDN “profile” into Azure which is just a container. We will now need to add the CDN “endpoint” which connects an external domain with an origin (our Storage Account):

resource "azurerm_cdn_endpoint" "static-site" {
  name                = "static-site-cdnep"
  profile_name        =
  resource_group_name =
  location            = "westeurope"

  origin_host_header = azurerm_storage_account.static-site.primary_web_host

  is_http_allowed        = true
  is_compression_enabled = true

  content_types_to_compress = [

  delivery_rule {
    name  = "httpRedirect"
    order = 1
    request_scheme_condition {
      operator     = "Equal"
      match_values = ["HTTP"]

    url_redirect_action {
      redirect_type = "PermanentRedirect"
      protocol      = "Https"

  delivery_rule {
    name  = "wwwRedirect"
    order = 2
    request_uri_condition {
      operator     = "BeginsWith"
      match_values = ["https://www."]
      transforms   = "Lowercase"

    url_redirect_action {
      redirect_type = "PermanentRedirect"
      protocol      = "Https"
      hostname      = ""

  origin {
    name      =
    host_name =

Some notes about the above block:


The last part of the Terraform is to set up the DNS in We will set up 2 CNAME records for the www subdomain and the “apex” or “root” domains to point towards our CDN Endpoint:

resource "azurerm_dns_zone" "jamescx" {
  name                = ""
  resource_group_name =

resource "azurerm_dns_cname_record" "www" {
  name                = "www"
  zone_name           =
  resource_group_name =
  ttl                 = 300
  target_resource_id  =

resource "azurerm_dns_a_record" "apex" {
  name                = "@"
  zone_name           =
  resource_group_name =
  ttl                 = 300
  target_resource_id  =

Creating the Resources (Terraform Cloud)

Now that we have our Terraform files, we need to get Terraform Cloud to create the resources into our Azure Account. The setup for this is best seen on the Terraform provider or the Microsoft Docs sites.

There is currently a manual step that will need to do with the CDN, which is to link up the domain and certificate. This is a missing feature (at time of writing) of the Terraform provider. Once the resources have been created in Azure, do these steps:

You will have to wait a while whilst the certificate is provisioned for you.

For the apex or root domain, there is an additional (and rather annoying) step. Azure will not (at time of writing) cerate you a free apex certificate so you will have to source one yourself. You can either do this for free (Let’s Encrypt) or purchase your own (I recommend Namecheap). The certificate can then be uploaded to a KeyVault within Azure and used from the CDN.

Github Flows

Lastly, we will get Github Flows to build and deploy the static site to the created Azure Resources.

Create the file .github/workflows/build-blog.yml with the blow contents:

name: Build & Release Blog
    - main
    runs-on: ubuntu-latest
     - name: CHECKOUT
       uses: actions/checkout@v2
     - name: AZURE LOGIN 
       uses: azure/login@v1
         creds: $
     - name: Upload to blob storage
       uses: azure/CLI@v1
         azcliversion: 2.0.72
         inlineScript: |
             az storage blob upload-batch --account-name staticsitestorage -d '$web' -s ./src
     - name: Purge CDN endpoint
       uses: azure/CLI@v1
         azcliversion: 2.0.72
         inlineScript: |
            az cdn endpoint purge --content-paths  "/*" --profile-name "static-site-cdn" --name "static-site-cdnep" --resource-group "static-site"

     - name: logout
       run: |
            az logout

The steps do this: login to Azure, upload the static files and then purge the CDN cache so that the new files are visible ASAP.

You will also need to setup the deployment credentials within Github. Details for this can be found on the Github Marketplace site.


And that should be it! You have now setup a static site and workflow such that when you push a change to your main branch, the files will appear on your domain.