Skip to main content

Build and deploy Hugo website to Azure Static Website on Terraform provisioned infrastructure

Since a few weeks ago, this website is running on an Azure Static Web App instance instead of a self-hosted LAMP (Linux, Apache, MySQL, PHP) server. In this blog post I want to take you with me on the journey how to automatically build and deploy an Hugo website on Azure Static Website that is provisioned with Terraform using Azure DevOps CI/CD pipelines.

Hugo, a static website generator #

A few weeks ago, I made the decision to move from a content management system to a static website because of an article about static website generators I read. This technology was totally new to me and it caught my interest to try it out. There are several static website generators available, but I chose for Hugo for the following reasons:

  • It is build with Go which I’m more familiar with then Ruby (Jekyll)
  • It is really fast
  • Simple but powerfull
  • Built-in development server
  • It did not take long to get it up and running

Theme #

A website needs a face, but since I’m not really a front-end guy I went for an existing theme. The congo theme caught my eye because it was minimalistic and included some nice features that I found important like a search engine, pagination, table of contents, and taxonomies.

Azure Static Web App #

Azure Static Web App is a serverless Azure resource that automatically builds and deploys web apps to Azure from a code repository. It supports web hosting for static content, e.g. HTML, CSS, Javascript, and images. It also has seamless integration with Github and Azure Devops in a way that publishing a new version of a website is very easy and fast. Not less important is that Azure Static Web App is globally distributed which increases performance because the website is closer to the visitors of the website. Also Azure Static Web App has support for custom domains including SSL certificate that are automatically renewed and include the custom domain. The availability of multiple environments in a single Azure Static Web App is a huge advantage since this can be used for staging environments before you publish to the live website.

Azure DevOps #

Azure DevOps is a product of Microsoft that includes support for GIT repositories and CI/CD pipelines. The code for the website is under version control and stored into the GIT repository. This also includes the CD/CD pipeline definitions to automatically provision infrastructure and build and deploy the website.


Everything required to get the website up-and-running is configured into a single idempotent CI/CD pipeline. I use Terraform to deploy infrastructure to Azure and SWA CLI to build and deploy the website to the Azure Static Web App. The only thing I have to do manually is to setup the CNAME record because my DNS provider doesn’t provide an API for this.

My CI/CD pipeline consists of multiple stages:

  1. Pre-requirement stage that does the initialization of Terraform in Azure (i.e. creates a storage account) and validated the Terraform files.
  2. Plan configuration stage that calculates the changes. In a “Terraform Plan” tab I’m able to check what Terraform has planned for.
  3. Approval stage, which is either automatic or manual. In most circumstances an automatic approval is given, except for plans that contain destroy actions.
  4. Applying configuration stage that runs after the (automatic) approval when the Terraform plan includes changes.
  5. Deploy website stage that builds and deploys the website into an Azure Static Web App

To make life easier I use the Azure Pipeline Terraform Tasks plugin. This plugin includes two tasks. One to install the Terraform CLI binary and a second task which is basically a wrapper around Terraform CLI which also includes support to use an Azure RM service connection for authentication.

For build and deploying the website I use SWA CLI which is an all-in-one development tool for Azure Static Web Apps. It runs on NodeJS and can be installed using npm. I only use SWA CLI to build the website and deploy it to Azure Static Web App, but it also includes an emulator.

How to set this up #

Enough talking, let me show how you get your own Hugo static website running on Azure Static Web App.

Get a development environment #

To get an environment up-and-running you need an Azure subscription and Azure DevOps project. Get a free Azure account and free Azure DevOps organization if you don’t have one. Before you configure the Azure DevOps organization, you need a non-guest global administrator account. When you have a non-guest global administrator you sign in with that account to the Azure DevOps organization and connect to your Azure Active Directory. To install the Azure Pipeline Terraform Tasks plugin, go to the plugin page and click on the “Get it free” button.

When the Azure DevOps organization is setup, create a new Azure DevOps project and clone the GIT repository to your local machine.

Install Hugo #

I’m working on a Windows machine and to install Hugo I use chocolaty. Hugo is running on Go, so you also need Go installed. Follow the steps below to get Hugo up-and-running:

  1. Install chocolaty (
    Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString(''))
  2. Install hugo
    choco install hugo-extended
  3. Install Go (
  4. Install NodeJS ( (optional, only required when you want to run SWA CLI locally)

Install Terraform #

Terraform can be downloaded from the Terraform website. I don’t use it on my local machine because I want the changes to infrastructure and my website triggered from Azure DevOps pipelines.

Initiate a Hugo website #

You can best follow the instructions on the quick start page from the Hugo website to get you up-and-running. In my repository I’ve created a initiated my website using hugo new site website which creates a website directory.

Check if you website works by running a local development server using hugo server -D.

Terraform configuration #

I use the Terraform configuration below to configure:

  • The azurerm backend
  • Require the azurerm provider
  • Define variables to define the location of my resources (e.g. West Europe) and the custom domain name of my Azure Static Web App
  • Define three resources which are a resource group, azure static web app, and the custom domain name
  • Define an output variable to expose the API key of the Azure Static Web App
terraform {
    backend "azurerm" {}

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = ">=2.88.1"

# Configure the Microsoft Azure Provider
provider "azurerm" {
  features {
    resource_group {
      prevent_deletion_if_contains_resources = true

variable "location" {
  default = "West Europe"

variable "domainname" {
  default = ""

resource "azurerm_resource_group" "my_first_rg" {
  name     = "website-rg"
  location = var.location

resource "azurerm_static_site" "site" {
  name = "your-static-web-app-name"
  resource_group_name =
  location = var.location
  sku_tier = "Free" # Or 'Standard'

resource "azurerm_static_site_custom_domain" "example" {
  static_site_id  =
  domain_name     = var.domainname
  validation_type = "cname-delegation"

output "static_site_api_key" {
  value =
  sensitive = true

The Terraform configuration above is stored into a .tf file in the root of the repository.

SWA CLI configuration #

To be able to build and deploy the Hugo website to Azure Static Web App, I use SWA CLI because I couldn’t get it working using the built-in Azure Static Web App task. AWS CLI expects a swa-cli.config.json file in the root of the repository with the following content:

  "$schema": "",
  "configurations": {
    "terraform-projects": {
      "appLocation": "website",
      "outputLocation": "public",
      "appBuildCommand": "hugo --environment production -D",
      "run": "hugo server -D",
      "appDevserverUrl": "http://localhost:1313"

Change the appLocation if you don’t named your Hugo website “website”.

CI/CD pipeline definitions #

In the pipelines I use variables from variable groups and an Azure RM service connection. Set these up following the steps below:

  1. Create a new Variable Group “terraform” and “environment”
  2. Add variables below in “terraform” variable group:
    terraformVersion = latest
    terraformResourceGroupName = terraform
  3. Add variables below in “environment” variable group:
    subscriptionId = <subscription identifier of the subscription you want to provision infrastructure to>
    terraformStorageAccountName = <name of the storage account used by Terraform>
    azureRmServiceConnection = <name of the service connection that has the Owner role on the Subscription>
    approvalNotifyUsers = <one or more email addresses or a user group that is notified when approval is required>
  4. Create Service Connection to Azure RM either on Management Group level or Subscription level. Make sure that the Service Connection has enough permissions to create resources in the Subscription defined in the environment variable group.

The pipeline definitions I use are separated into multiple files for readability and re-usability.

Test it! #

Commit all changes and push it to Azure DevOps. Add your root pipeline definition to Azure DevOps and run it!

The first run will probably fail because of the CNAME delegation on the custom domain. It is best to complete this manually using the Azure Portal or you have to add extra logic to automate the creation of the CNAME for you domain name. The second time your CI/CD pipeline should look like the screenshot below.

Screen of a CI/CD pipeline in Azure DevOps

In Azure you will have two resource groups. The terraform resource group contains an Azure Storage Accounts that stores the Terraform state files created by the CI/CD pipeline. The website-rg resource group contains a single Azure Static Web App resource that runs your website.

That’s it! I hope this blog was helpfull. If you have any questions, leave a comment below!