Skip to main content

Deploying my blog with Hugo

·1207 words·6 mins

Hosting and Deployment
#

Following on from my previous post, we now have a basic layout and first piece of content prepared. This article will deal with the setup of the Azure tenant and the deployment of both the infrastructure and website.

Overview
#

For the deployment, I will start with an empty Azure tenant. This means GitHub will not have the rights to deploy the required infra (Resource Group & Static Website) so I will need to create the resources needed to permit this. Once in place, I can deploy the actual infra required by the blog via GitHub Actions pipeline from Terraform code, my preferred method and then use the GitHub actions runner to build and deploy the Hugo blog as follows:

thegoal
The markdown files to hugo azure static website pipeline

Please ignore the imperfect diagram, that was the best Gemini Pro 3.1 could do after 10 attempts and corrective prompts.

Initial GitHub –> Azure Connection
#

The initial “bootstrap phase” will be focused on configuring the RBAC (Role Based Access Control) to allow a GitHub Identity (an EntraID service principal) the rights to deploy infrastructure to our subscription. We will also configure a storage account in which our terraform will be able to store it’s statefile. This initial setup will be handled via PowerShell as that’s my preference but could be done by Azure CLI or button clicking if you prefer.

These are the commands I wil run :

# Connect to tenant
Connect-AzAccount -Tenant "xxxx-xxxx-xxxx-xxxx-xxxx"

# Set some variables
$location = "<deplomentregion>"
$rgName = "<rgname>"
$storageName = "<saname>" # Must be globally unique

# Create the resource Group
New-AzResourceGroup -Name $rgName -Location $location

# Create the storage account
$storage = New-AzStorageAccount -ResourceGroupName $rgName -Name $storageName -SkuName "Standard_LRS" -Location $location -AllowBlobPublicAccess $false -MinimumTlsVersion TLS1_2

# Create the blob storage container
New-AzStorageContainer -Name "tfstate-blog" -Context $storage.Context

# Create the service principal
$app = New-AzADApplication -DisplayName "sp-github-actions-mgmt"
Start-Sleep -Seconds 10
$sp = New-AzADServicePrincipal -ApplicationId $app.AppId

# Grant roles
$subId = (Get-AzContext).Subscription.Id
New-AzRoleAssignment -ApplicationId $app.AppId -RoleDefinitionName "Contributor" -Scope "/subscriptions/$subId"

# Create the OIDC Trust
$fedParams = @{
    Name = "github-actions-blog"
    Issuer = "https://token.actions.githubusercontent.com"
    Subject = "repo:<githubusername>/<githubreponame>:ref:refs/heads/main"
    Description = "OIDC trust for Blog deployment"
    Audience = @("api://AzureADTokenExchange")
}
New-AzADAppFederatedCredential -ApplicationObjectId $app.Id @fedParams

# Output the vars for GitHub secrets
Write-Host "AZURE_CLIENT_ID       : $($app.AppId)"
Write-Host "AZURE_TENANT_ID       : $((Get-AzContext).Tenant.Id)"
Write-Host "AZURE_SUBSCRIPTION_ID : $subId"

GitHub Secrets Setup
#

I will go to my GitHub Repository -> Settings -> Secrets and variables -> Actions. I will then add the following “Repository secrets”:

  • AZURE_CLIENT_ID: The $appId from Step 2.
  • AZURE_TENANT_ID: Your Azure Tenant ID (az account show --query tenantId -o tsv).
  • AZURE_SUBSCRIPTION_ID: Your Subscription ID ($subId).
Secrets
Completed GitHub secrets section

Terraform
#

Back in VSCode, I will create a new folder called terraform, create 2x files inside and populate them as below:

terraform/providers.tf Defines the Terraform “backend” which comprises of:

  • Which provider to use
  • Where to store the state file
  • How to connect
terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
  }
  backend "azurerm" {
    resource_group_name  = "<rgname>"
    storage_account_name = "<saname>" # Must match Phase 1
    container_name       = "tfstate-blog"
    key                  = "prod.terraform.tfstate"
    use_oidc             = true
  }
}

provider "azurerm" {
  features {}
  use_oidc = true
}

terraform/main.tf Defines the two resources that will be deployed, the resource group and the static webapp and an output the deployment token to feed back into GitHub to allow GitHub Actions workflow to push the Hugo site on update.

resource "azurerm_resource_group" "blog" {
  name     = "rg-securityblog-prod"
  location = "westeurope"
}

resource "azurerm_static_web_app" "blog_app" {
  name                = "swa-benstalker-tech"
  resource_group_name = azurerm_resource_group.blog.name
  location            = azurerm_resource_group.blog.location
  sku_tier            = "Free"
  sku_size            = "Free"
}

# We output the deployment token so GitHub Actions can use it to push the Hugo site
output "swa_api_key" {
  value     = azurerm_static_web_app.blog_app.api_key
  sensitive = true
}

At this stage, I will commit and merge with the GitHub again.

GitHub Actions Pipeline
#

Next I will define the GitHub actions workflow by creating a file at .github/workflows/deploy.yml. This workflow logs into Azure securely via OIDC, runs Terraform to build the infrastructure, extracts the SWA token dynamically, and deploys your Hugo site.

YAML

name: Deploy Infra and Blog

on:
  push:
    branches: ["main"]

# REQUIRED for OIDC Authentication
permissions:
  id-token: write
  contents: read

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Code
        uses: actions/checkout@v4
        with:
          submodules: true
          fetch-depth: 0

      - name: Azure Login via OIDC
        uses: azure/login@v2
        with:
          client-id: ${{ secrets.AZURE_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v3

      - name: Terraform Init & Apply
        id: tf
        working-directory: ./terraform
        run: |
          terraform init
          terraform apply -auto-approve
        env:
          ARM_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
          ARM_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
          ARM_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
          ARM_USE_OIDC: true

      - name: Get SWA Deployment Token
        id: get_token
        working-directory: ./terraform
        run: |
          SWA_TOKEN=$(terraform output -raw swa_api_key)
          echo "::add-mask::$SWA_TOKEN"
          echo "swa_token=$SWA_TOKEN" >> $GITHUB_OUTPUT

      - name: Build and Deploy Hugo to Azure Static Web Apps
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ steps.get_token.outputs.swa_token }}
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          action: "upload"
          app_location: "/" 
          output_location: "public"

I then committed and pushed these changes to GitHub. When the workflow was detected, it was immediately kicked off by GitHub:

ActionsRun
In Progress GitHub Actions Workflow

The workflow completed successfully and we have our very first version of this blog published.

FQDN & DNS
#

The last step is to buy the domain, configure name servers to point at Azure then setup the necesarry DNS records in Azure.  I have got into the habbit of using godaddy for all my domain names and this site will be no different.

GoDaddy
Domain Purchased

Now I’ve purchased, I will need to update the configuration for the Azure Static Website as well as in GoDaddy. First up, Godaddy:

GoDaddy Configuration
#

I have the default Azure hostname for this website: agreeable-grass-08e492903.1.azurestaticapps.net. In GoDaddy, I will edit the existing CNAME record for www pointing at the this URL.

Terraform
#

Next up is adding this custom domain name via Terraform to the static webapp. I will add this resource block to my main.tf file:

# Adding CNAME record to static web app
resource "azurerm_static_web_app_custom_domain" "www_domain" {
  static_web_app_id = azurerm_static_web_app.blog_app.id
  domain_name       = "www.benstalker.co.uk"
  validation_type   = "cname-delegation"
}

With this in place, I can commit and push the code changes to GitHub, triggering a fresh GitHub Actions run. When completed I see the following:

![[Pasted image 20260301213512.png]]

Now the root domain. Due to limitations, this must be done by button clicking in the portal. Not great, but it’s what we have when using Azure. I will button click on the following manor:

1. Generate the Token in Azure

  • Go to Azure Static Web App in the portal.
  • Click Custom domains -> + Add -> Custom domain on other DNS.
  • Enter benstalker.co.uk and click Next.
  • Azure will generate a TXT validation token.

2. Add the TXT Record to GoDaddy

  • Go to GoDaddy DNS management.
  • Add a new record.
  • Type: TXT
  • Name: @ (This represents the root domain)
  • Value: Paste the token from Azure.
  • Save it.

3. Validate and Route

  • Back in Azure, click Validate. (Wait a few minutes if it fails the first time).
  • Once it validates, Azure will give you an Alias/A-Record IP Address.
  • Go back to GoDaddy, add an A record with the name @ pointing to that IP addresses
Ben Stalker
Author
Ben Stalker
A passionate IT professional who loves all things coding and scripting