Static Web App mit Microsoft Azure
Azure Subscription
Die Reise beginnt in Azure. Man muss eine Subscription anlegen - dort werden die Zahlungsdaten hinterlegt. Wichtig ist, dass man ein Budget anlegt (Achtung: Es kann bis zu 24h dauern, bis man ein Budget bei einer neuer Subscription anlegen kann).
Repository
Beginnen tut die Reise in Azure DevOps: Falls noch nicht passiert, muss man eine Organisation anlegen.
Genauere Infos gibt es in der Dokumentation.
Als nächstes muss man Azure DevOps und Azure verbinden. Dazu geht man in die Organization Settings. Die TenantId bekommt man aus Microsoft Entra.
Danach einfach ein Project anlegen, welches ein git Repository hat.
Seite mit Hugo bauen
Als erstes muss man sich lokal hugo installieren. Die einfachste Möglichkeit ist natürlich einfach die Exekutable zu laden: https://github.com/gohugoio/hugo/releases.
Danach sind folgende Schritte zu machen:
hugo new site quickstart
cd quickstart
git init
git submodule add https://github.com/theNewDynamic/gohugo-theme-ananke.git themes/ananke
echo "theme = 'ananke'" >> hugo.toml
hugo server
Man kann die Seite dann lokal abrufen: http://localhost:1313/
Danach kann man die Seite ins Repository pushen.
Seite hosten
Und jetzt kommt der spannenste Teil: Wie kann man die Seite hosten. Das geht mit Static Web Apps. Es gibt ein gratis Angebot von 500MB Speicher mit 100GB Transfer - ideal für private Projekte. Bilder kann man ebenfalls hier speichern oder eben in einem Blob Storage. Biceps ist die state-of-the-art Methode für IaC auf Azure. Folgende Scripts sind nicht ideal (ohne Parameter) - genügen aber dem Anspruch eines privaten Mini-Projekts.
main.bicep
targetScope = 'subscription'
resource rgwebsiteprod 'Microsoft.Resources/resourceGroups@2023-07-01' = {
name: 'rg-website-prod-001'
location: 'germanywestcentral'
tags: {}
properties: {}
}
module storageAccount 'storage.bicep' = {
name: 'storageModule'
scope: rgwebsiteprod
params: {
storageName: '<storageaccountname>'
location: rgwebsiteprod.location
}
}
module staticSite 'webstatic.bicep' = {
name: 'staticWebsite'
scope: rgwebsiteprod
params: {
staticWebName: '<name>'
}
}
storage.bicep
@description('Azure region of the deployment')
param location string = resourceGroup().location
@description('Name of the storage account')
param storageName string
resource webstorage 'Microsoft.Storage/storageAccounts@2023-01-01' = {
sku: {
name: 'Standard_LRS'
}
kind: 'StorageV2'
name: storageName
location: location
tags: {}
properties: {
dnsEndpointType: 'Standard'
allowedCopyScope: 'AAD'
defaultToOAuthAuthentication: true
publicNetworkAccess: 'Enabled'
allowCrossTenantReplication: false
isSftpEnabled: false
minimumTlsVersion: 'TLS1_2'
allowBlobPublicAccess: true
allowSharedKeyAccess: false
isHnsEnabled: true
networkAcls: {
ipv6Rules: []
bypass: 'AzureServices'
virtualNetworkRules: []
ipRules: []
defaultAction: 'Allow'
}
supportsHttpsTrafficOnly: true
encryption: {
requireInfrastructureEncryption: false
services: {
file: {
keyType: 'Account'
enabled: true
}
blob: {
keyType: 'Account'
enabled: true
}
}
keySource: 'Microsoft.Storage'
}
accessTier: 'Hot'
}
}
resource webstorage_default 'Microsoft.Storage/storageAccounts/blobServices@2023-01-01' = {
parent: webstorage
name: 'default'
properties: {
containerDeleteRetentionPolicy: {
days: 7
enabled: true
}
cors: {
corsRules: []
}
deleteRetentionPolicy: {
allowPermanentDelete: false
days: 7
enabled: true
}
}
sku: {
name: 'Standard_LRS'
tier: 'Standard'
}
}
resource storageBlobDataContributorRoleDefinition 'Microsoft.Authorization/roleDefinitions@2018-01-01-preview' existing = {
scope: subscription()
name: 'ba92f5b4-2d11-453d-a403-e96b0029c9fe'
}
resource roleAssignment 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
scope: webstorage
name: guid(resourceGroup().id, '<tenant object id>', storageBlobDataContributorRoleDefinition.id)
properties: {
roleDefinitionId: storageBlobDataContributorRoleDefinition.id
principalId: '<tenant object id>'
principalType: 'User'
}
}
resource webstorage_files 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = {
parent: webstorage_default
name: 'files'
properties: {
defaultEncryptionScope: '$account-encryption-key'
denyEncryptionScopeOverride: false
publicAccess: 'Blob'
}
}
resource webstorageFilesImages 'Microsoft.Storage/storageAccounts/blobServices/containers@2023-01-01' = {
parent: webstorage_default
name: 'images'
properties: {
defaultEncryptionScope: '$account-encryption-key'
denyEncryptionScopeOverride: false
publicAccess: 'Blob'
}
}
webstatic.bicep
@description('The resource name')
param staticWebName string
resource staticSite 'Microsoft.Web/staticSites@2023-01-01' = {
location: 'West Europe'
name: staticWebName
properties: {
allowConfigFileUpdates: true
branch: 'master'
buildProperties: {
appLocation: 'site'
outputLocation: 'public'
}
enterpriseGradeCdnStatus: 'Disabled'
provider: 'Custom'
repositoryUrl: '<git url>'
stagingEnvironmentPolicy: 'Enabled'
}
sku: {
name: 'Free'
tier: 'Free'
}
}
Und: das war es auch schon. Static Web Apps fügt einen PAK zu dem Dev Ops Projekt hinzu und fügt auch eine Pipline ein, welche die Hugo App baut und published. Zwei Anmerkungen noch:
- Custom Domains gehen auch mit Biceps - blockieren aber das Deployment Script, bis die Domain verifziert ist. Das verschlingt natürlich Build-Server Minuten
- Wenn man die Webapp manuell anlegt, dann wird die Buildpipline automatisch konfiguriert - ich weiß nicht, warum er das mit Biceps nicht macht.
Daher die Schritte für die Buildpipline:
name: Azure Static Web Apps CI/CD
pr:
branches:
include:
- master
trigger:
branches:
include:
- master
jobs:
- job: build_and_deploy_job
displayName: Build and Deploy Job
condition: or(eq(variables['Build.Reason'], 'Manual'),or(eq(variables['Build.Reason'], 'PullRequest'),eq(variables['Build.Reason'], 'IndividualCI')))
pool:
vmImage: ubuntu-latest
variables:
- group: Azure-Static-Web-Apps-gray-island-xxxxxx-variable-group
steps:
- checkout: self
submodules: true
- task: AzureStaticWebApp@0
inputs:
azure_static_web_apps_api_token: $(AZURE_STATIC_WEB_APPS_API_TOKEN_GRAY_ISLAND_XXXXXX)
app_location: "/<folder>" # App source code path
api_location: ""
output_location: "public"
Hugo Deepdive
Anbei ein paar wichtige Hilfen
CDN für Bilder
Ich bin kein go-template-experte - aber es geht:
layouts\_default\_markup\render-image.html
- siehe auch https://gohugo.io/render-hooks/
{{- $url := urls.Parse .Destination -}}
<img class="content-image"
{{- if $url.IsAbs }} src="{{ .Destination | safeURL }}"{{ end -}}
{{- if (not $url.IsAbs) }} src="{{ print (.Page.Site.Params.cdnURL | safeURL) (.Destination | safeURL) }}"{{ end -}}
{{- with .Text }} alt="{{ . }}"{{ end -}}
{{- with .Title }} title="{{ . }}"{{ end -}}
>
{{- /* chomp trailing newline */ -}}
Bilder mit absoluter URL sollen nicht geändert werden. Selbiges für URLs:
{{- $url := urls.Parse .Destination -}}
<a
{{- if $url.IsAbs }} href="{{ .Destination | safeURL }}"{{ end -}}
{{- if (not $url.IsAbs) }} href="{{ print (.Page.Site.Params.cdnURL | safeURL) (.Destination | safeURL) }}"{{ end -}}
{{- with .Title }} title="{{ . }}"{{ end -}}
>
{{- with .Text | safeHTML }}{{ . }}{{ end -}}
</a>
{{- /* chomp trailing newline */ -}}
Flickr shortcode
Eine einfache Copy & Paste Aufgabe:
layouts\shortcodes\flickr.html
<a data-flickr-embed="true" data-header="true" data-footer="true" href="https://www.flickr.com/photos/72225550@N04" title=""><img src="https://live.staticflickr.com/65535/53372707376_9e8ef59edc_b.jpg" width="1024" height="768" alt=""/></a><script async src="//embedr.flickr.com/assets/client-code.js" charset="utf-8"></script>
Google Maps
layouts\shortcodes\gmap.html
<div style="position: relative; padding-bottom: 56.25%; height: 0; overflow: hidden;">
<iframe src={{.Get "src"}} style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; border:0;"></iframe>
</div>
Einbinden
{{< gmap src="https://www.google.com/maps/d/embed?mid=10RcasC0ZMZSQWHyDKJZ2TNfYnMO60UTE" >}}
Es gibt auch die Möglichkeit per <<%
was MarkDown rendern würde - aber das funktioniert mit HTML gemischt leider nicht.
Lokal testen
Da meine Seite mini ist, teste ich immer mit
hugo.exe server -s vodepat --disableFastRender --renderToMemory
Fazit
Azure Static Web Apps bieten eine super einfache Lösung - inkl. CI/CD. Für kleine Projekte gratis. Azure Blob Storage ist natürlich immer so ne Sache - man müsste Azure Front Door für CDN und DDoS Schutz dazukaufen - was den Preis allerdings gewaltig in die Höhe treibt.