My blog was originally hosted on Azure Blob Storage with static website hosting enabled. It was a clean and straightforward setup: Terraform provisioned a storage account, Jekyll built the static site, and a GitHub Actions pipeline published the site to blob storage. But there was one major issue — Azure Blob Storage doesn’t support automatic SSL certificates for custom domains.

That limitation was the driving force behind my migration to Azure Static Web Apps, which offers free SSL out of the box. This article documents how I performed that migration using a blue-green deployment strategy, while preserving my existing two-stage Terraform + Jekyll GitHub Actions pipeline.

1. Provision Green Environment

The original environment — my blue environment — consisted of a storage account deployed via Terraform:

resource "azurerm_resource_group" "main" {
  name     = "rg-${var.application_name}-${var.environment_name}"
  location = var.location
}


resource "random_string" "main" {
  length  = 8
  upper   = false
  special = false
}

resource "azurerm_storage_account" "main" {
  name                       = "st${random_string.main.result}"
  resource_group_name        = azurerm_resource_group.main.name
  location                   = azurerm_resource_group.main.location
  account_tier               = "Standard"
  account_replication_type   = "RAGZRS"
  https_traffic_only_enabled = false

  dynamic "custom_domain" {
    for_each = var.custom_domain != null ? [0] : []

    content {
      name          = var.custom_domain
      use_subdomain = false
    }
  }

  static_website {
    index_document     = "index.html"
    error_404_document = "404.html"
  }
}

My Azure Storage Account hosting my blog was provisioned in East US. However, Azure Static Websites aren’t supported in East US. So I need to try for a different region. I chose East US 2.

I didn’t even have the concept of a primary or a secondary region but Terraform doesn’t care if I change variable names. It only cares if the value passed into the resource changed. So the first thing I did was update the input variables to have the concept of a primary and secondary region.

variable "primary_location" {
  type = string
}
variable "secondary_location" {
  type = string
}

I only maintain one environment prod so I only have one *.tfvars file.

primary_location   = "eastus"
secondary_location = "eastus2"

Then I provisioned the new green environment — an Azure Static Web App in the secondary region:

resource "azurerm_resource_group" "secondary" {
  name     = "rg-${var.application_name}-${var.environment_name}-${var.secondary_location}"
  location = var.secondary_location
}

resource "azurerm_static_web_app" "main" {

  name                = "stapp${random_string.main.result}"
  resource_group_name = azurerm_resource_group.secondary.name
  location            = azurerm_resource_group.secondary.location

}

I also need to output the Static Web Site name so that it can be passed onto the second stage: publish.

output "static_website_name" {
  value = azurerm_static_web_app.main.name
}

In the GitHub Actions pipeline, I grabbed that output and passed it forward: echo “static_website_name=$(terraform output -raw static_website_name)” » “$GITHUB_OUTPUT” This value was declared as an output of the job called infra.

jobs:
  infra:
    runs-on: ubuntu-latest
    env:
      TERRAFORM_VERSION: "1.9.0"
    outputs:
      storage_account_name: $
      static_website_name: $

Notice, I am still leaving the storage_account_name . That’s because the blue environment is my storage account and the green environment is my Azure Static Web Site. I can’t delete the blue environment until I have fully transitioned to the green environment.

Alt

At this point, both blue (Blob Storage) and green (Static Web App) environments were provisioned and running in parallel. However, nothing is actually published to my green environment. Hence, I need to actually deploy to my green environment before I cutover.

2. Deploy to Green

The second stage of the pipeline builds the site using Jekyll and publishes it. Originally, this stage uploaded files directly to Azure Blob Storage. With the green environment now targeting Azure Static Web Apps, I needed to update the deployment mechanism to use the Static Web Apps CLI (swa) instead.

Since the jekyll-publish job already depends on the infra job, I could access the static_website_name output directly:

  jekyll-publish:
    runs-on: ubuntu-latest
    needs: infra

Finally, I add a GitHub Action step to publish to the Static Web Site. I need to use the swa or the Static Web Apps CLI.

      - name: Upload site to Azure Static Web Site
        working-directory: $
        env:
          ARM_SUBSCRIPTION_ID: $
          ARM_TENANT_ID: $
          ARM_CLIENT_ID: $
          ARM_CLIENT_SECRET: $
          STATIC_WEBSITE_NAME: $
        run: |

          ONE_HOUR_AGO=$(date -u -d '1 hour ago' +"%Y-%m-%dT%H:%M:%SZ")
          FIVE_MINUTES_AGO=$(date -u -d '5 minutes ago' +"%Y-%m-%dT%H:%M:%SZ")

          npm install -g @azure/static-web-apps-cli

          az login --service-principal -u $ARM_CLIENT_ID -p $ARM_CLIENT_SECRET --tenant $ARM_TENANT_ID
          swa login --tenant-id $ARM_TENANT_ID \
            --client-id $ARM_CLIENT_ID \
            --client-secret $ARM_CLIENT_SECRET \

          swa deploy ./_site \
            --app-name $STATIC_WEBSITE_NAME \
            --env prod

This change let me continue using Jekyll for site generation while publishing the output to the new green environment with full support for custom domains and SSL.

3. Cutover

Once the site was deployed to the new Azure Static Web App, I was ready to route traffic to it. First, I updated my DNS records to point my custom domain to the new Static Web App. Then I updated Terraform to bind the domain formally:

The old DNS was pointed at the Azure Storage Account’s Static Web Site.

Alt

I just updated it to point to the newly created domain name for the Azure Static Web Site.

Alt

Update the Terraform to add the custom domain

resource "azurerm_static_web_app_custom_domain" "main" {
  static_web_app_id = azurerm_static_web_app.main.id
  domain_name       = var.custom_domain
  validation_type   = "cname-delegation"
}

After running terraform apply, Azure handled the domain validation and provisioned an SSL certificate automatically.

The Static Web Site is looking good and showing the newly attached custom domain name.

Alt

I can also see both the auto-generated domain name and custom domain name are healthy under the “Custom domains” slice in the portal.

Alt

With that, the green environment was live — serving traffic over HTTPS using Azure’s free managed certificates — and the blue environment (Blob Storage) was no longer needed. I left it intact temporarily for rollback safety, but eventually it could be removed.

Conclusion

This migration replaced my Azure Blob Storage setup with Azure Static Web Apps to take advantage of built-in SSL support for custom domains — a capability that Blob Storage lacks. By preserving my two-stage Terraform and Jekyll pipeline and layering in blue-green deployment, I was able to transition environments safely and incrementally.

The end result is a more robust deployment process and a production site that now benefits from HTTPS by default, without additional configuration or cost.

That’s it!