Zum Hauptinhalt springen
  1. Blog/

rdgy.at - Mit Hugo, Blowfish, OpenTofu, Azure und GitHub Actions in zwei Stunden live

Dominic Rudigier
Autor
Dominic Rudigier
BSc MSc Scrum Master
Ich helfe Teams dabei, unklare technische Probleme in funktionierende Systeme, verlässlichen Betrieb und pragmatische Ergebnisse zu übersetzen.

So habe ich rdgy.at aufgebaut – und so kannst du es nachmachen:

  • Hugo für die Website
  • Blowfish als Theme
  • Azure Static Web Apps für das Hosting
  • OpenTofu für die Infrastruktur
  • GitHub Actions für das automatische Deployment

Am Ende sieht die URL-Struktur so aus:

www.example.com/        -> Startseite
www.example.com/blog/   -> Blog
www.example.com/projects/
www.example.com/services/
example.com             -> leitet auf https://www.example.com weiter

Wenn du diesen Aufbau für dein Unternehmen brauchst – ob als einfacher Blog, Unternehmenswebsite oder als eigenständiges Produkt wie Belegmappe.at – melde dich unter office@rdgy.at. Wir besprechen, was du brauchst, und ich sage dir ehrlich, ob und wie ich helfen kann.


Phase 1 – Website lokal aufbauen
#

Deutsch ist die Standardsprache. Inhalte entstehen zuerst unter content/de, danach folgt die Übersetzung unter content/en.

1. Werkzeuge installieren
#

Du brauchst:

  • Hugo Extended
  • Git
  • OpenTofu
  • Azure CLI

Auf macOS reicht ein einziger Befehl:

brew install hugo opentofu azure-cli

Danach kurz prüfen, ob alles korrekt installiert ist:

hugo version
tofu version
az version

2. Website anlegen und Blowfish einbinden
#

hugo new site my-site
cd my-site
git init

git submodule add -b main \
  https://github.com/nunocoracao/blowfish.git \
  themes/blowfish

mkdir -p config/_default
cp themes/blowfish/config/_default/*.toml config/_default/

3. Hugo konfigurieren
#

Die zwei wichtigsten Konfigurationsdateien:

  • config/_default/hugo.toml – allgemeine Einstellungen, Sprachen, Navigation
  • config/_default/params.toml – Layout und Aussehen

config/_default/hugo.toml
#

theme = "blowfish"
baseURL = "https://www.example.com/"
defaultContentLanguage = "de"
defaultContentLanguageInSubdir = false

enableRobotsTXT = true
summaryLength = 0

[outputs]
  home = ["HTML", "RSS", "JSON"]

[taxonomies]
  tag = "tags"
  category = "categories"

[languages]
  [languages.en]
    weight = 2
    contentDir = "content/en"
  [languages.de]
    weight = 1
    contentDir = "content/de"

[languages.en.menu]
  [[languages.en.menu.main]]
    name = "Blog"
    pageRef = "blog"
    weight = 10
  [[languages.en.menu.main]]
    name = "Projects"
    pageRef = "projects"
    weight = 20
  [[languages.en.menu.main]]
    name = "Services"
    pageRef = "services"
    weight = 30

[languages.de.menu]
  [[languages.de.menu.main]]
    name = "Blog"
    pageRef = "blog"
    weight = 10
  [[languages.de.menu.main]]
    name = "Projekte"
    pageRef = "projects"
    weight = 20
  [[languages.de.menu.main]]
    name = "Leistungen"
    pageRef = "services"
    weight = 30

Ein paar wichtige Punkte:

  • baseURL zeigt immer auf www
  • Deutsch liegt direkt auf /, Englisch unter /en/
  • Die Navigation wird hier pro Sprache definiert

config/_default/params.toml
#

defaultBackgroundImage = "img/background.jpg"

[homepage]
  layout = "background"
  homepageImage = "img/background.jpg"
  showRecent = true
  showRecentItems = 3

[article]
  showDate = true
  showAuthor = true
  showReadingTime = true
  showTableOfContents = true
  showTaxonomies = true
  showPagination = true
  showHero = true
  heroStyle = "thumbAndBackground"

[list]
  showHero = true
  heroStyle = "background"
  showSummary = true
  showCards = true
  cardView = true

Das ergibt:

  • Startseite mit Hintergrundbild
  • Hero-Bereiche auf Blog- und Projektseiten
  • Die neuesten Beiträge direkt auf der Startseite
  • Kartenansicht für Blog und Projekte

4. Inhaltsstruktur anlegen
#

Abschnitte (Blog, Projekte) bekommen eine _index.md. Einzelne Seiten (Artikel, Projekte) sind normale .md-Dateien.

mkdir -p content/de/blog
mkdir -p content/de/projects
mkdir -p content/en/blog
mkdir -p content/en/projects
mkdir -p static/img

Die Struktur sieht dann so aus:

content/
├── de/
│   ├── _index.md
│   ├── blog/
│   │   ├── _index.md
│   │   └── first-post/index.md
│   ├── projects/
│   │   ├── _index.md
│   │   └── product-a.md
│   └── services.md
└── en/
    ├── _index.md
    ├── blog/
    ├── projects/
    └── services.md

Beispiel Startseite
#

content/de/_index.md

---
title: "Dominic"
description: "Cloud Engineer aus Innsbruck."
---

Ich baue Cloud-Infrastruktur für Teams, die Production-Deployments
lieber in erfahrene Hände legen.

Beispiel Blog-Index
#

content/de/blog/_index.md

---
title: "Blog"
description: "Notizen zu Cloud, Infrastructure as Code und Software Delivery."
---

Beispiel Projektseite
#

content/de/projects/product-a.md

---
title: "Produkt A"
description: "Kurze Produktbeschreibung."
summary: "Kurzer Text für die Projektkarte in der Übersicht."
showDate: false
showReadingTime: false
---

## Idee

Worum es im Produkt geht.

## Herausforderungen

- Herausforderung eins
- Herausforderung zwei

## Tech Stack

- Hugo
- PostgreSQL
- Azure

## Live-Projekt

[product-a.com](https://product-a.com)

Zuerst die deutschen Inhalte schreiben, dann die Übersetzung unter content/en/ anlegen.

5. Website lokal testen
#

hugo server

Dann im Browser aufrufen:

  • http://localhost:1313/ – deutsche Version
  • http://localhost:1313/en/ – englische Version

Erst wenn die Website lokal so aussieht wie gewünscht, geht es mit dem Deployment weiter.


Phase 2 – Infrastruktur mit OpenTofu
#

OpenTofu beschreibt die Azure-Infrastruktur als Code. Einmal anlegen, danach wiederverwendbar und versionierbar.

6. Azure mit OpenTofu provisionieren
#

Zuerst ein infra/-Verzeichnis im Projektordner anlegen und die vier Dateien darin erstellen. Danach die Zugangsdaten beschaffen und alles mit tofu apply ausrollen.

mkdir infra

infra/variables.tf
#

Hier werden alle Eingabewerte deklariert, die OpenTofu benötigt. Die Werte selbst stehen nicht hier – sie kommen später aus terraform.tfvars. So bleiben Struktur und Geheimnisse sauber getrennt.

sensitive = true sorgt dafür, dass OpenTofu diese Werte niemals im Terminal ausgibt – auch nicht beim apply.

location hat einen Standardwert (westeurope), der verwendet wird, wenn du keinen anderen angibst.

variable "subscription_id" {
  type      = string
  sensitive = true
}

variable "client_id" {
  type      = string
  sensitive = true
}

variable "client_secret" {
  type      = string
  sensitive = true
}

variable "tenant_id" {
  type      = string
  sensitive = true
}

variable "location" {
  type    = string
  default = "westeurope"
}

infra/main.tf
#

Das ist die eigentliche Infrastruktur. OpenTofu liest diese Datei und legt die beschriebenen Ressourcen in Azure an – oder aktualisiert sie, falls sie schon existieren.

terraform-Block – legt fest, welche Provider-Version verwendet wird. azurerm ist das offizielle Azure-Plugin. Die Version ~> 3.0 erlaubt alle Patch-Updates innerhalb von Version 3, aber keinen Major-Wechsel.

provider "azurerm" – verbindet OpenTofu mit deinem Azure-Account. Die Zugangsdaten kommen aus den Variablen oben.

azurerm_resource_group – eine Resource Group ist ein logischer Container in Azure. Alle zugehörigen Ressourcen landen darin. Einfach löschen, alles weg.

azurerm_static_web_app – das eigentliche Hosting. Azure Static Web Apps stellt die Website aus, verteilt sie global über ein CDN und kümmert sich um HTTPS automatisch. Das Free-Tier reicht für eine normale Website vollständig aus.

azurerm_static_web_app_custom_domain – verknüpft deine eigene Domain (www.example.com) mit der Static Web App. cname-delegation bedeutet: Azure prüft, ob der CNAME-Eintrag im DNS auf die Azure-Adresse zeigt, bevor die Domain aktiviert wird.

terraform {
  required_providers {
    azurerm = {
      source  = "hashicorp/azurerm"
      version = "~> 3.0"
    }
  }
  required_version = ">= 1.6.0"
}

provider "azurerm" {
  features {}
  subscription_id = var.subscription_id
  client_id       = var.client_id
  client_secret   = var.client_secret
  tenant_id       = var.tenant_id
}

resource "azurerm_resource_group" "site" {
  name     = "rg-my-site"
  location = var.location
}

resource "azurerm_static_web_app" "site" {
  name                = "stapp-my-site"
  resource_group_name = azurerm_resource_group.site.name
  location            = azurerm_resource_group.site.location
  sku_tier            = "Free"
  sku_size            = "Free"
}

resource "azurerm_static_web_app_custom_domain" "www" {
  static_web_app_id = azurerm_static_web_app.site.id
  domain_name       = "www.example.com"
  validation_type   = "cname-delegation"
}

infra/outputs.tf
#

Nach dem apply gibt OpenTofu diese Werte im Terminal aus. Du brauchst sie für die nächsten Schritte – Domain und GitHub-Secret.

  • site_url – die automatisch generierte Azure-URL deiner Website (z.B. https://proud-rock-abc123.azurestaticapps.net). Damit kannst du sofort testen, ob das Deployment funktioniert hat.
  • deployment_token – der geheime Schlüssel, den GitHub Actions braucht, um Dateien nach Azure hochzuladen. Kommt gleich als GitHub-Secret.
  • www_cname_target – der Hostname, auf den dein www-CNAME-Eintrag beim DNS-Anbieter zeigen muss.
output "site_url" {
  value = "https://${azurerm_static_web_app.site.default_host_name}"
}

output "deployment_token" {
  value     = azurerm_static_web_app.site.api_key
  sensitive = true
}

output "www_cname_target" {
  value = azurerm_static_web_app.site.default_host_name
}

Azure-Zugangsdaten beschaffen
#

Bevor du terraform.tfvars befüllst, brauchst du die vier Werte aus Azure. Einmalig einrichten:

1. In Azure einloggen

az login

Ein Browserfenster öffnet sich – dort mit dem Microsoft-Konto anmelden.

2. Subscription ID auslesen

az account show --query id -o tsv

Diesen Wert als subscription_id eintragen.

3. Service Principal erstellen

Ein Service Principal ist ein technischer Account, den OpenTofu zum Anlegen von Ressourcen in Azure verwendet – ähnlich wie ein API-Key, aber für Azure.

az ad sp create-for-rbac \
  --name "sp-my-site" \
  --role Contributor \
  --scopes /subscriptions/<DEINE_SUBSCRIPTION_ID>

Die Ausgabe sieht so aus:

{
  "appId":       "...",   <- das ist client_id
  "password":    "...",   <- das ist client_secret
  "tenant":      "..."    <- das ist tenant_id
}

Das Secret wird nur einmal angezeigt – sofort sichern.

infra/terraform.tfvars
#

Jetzt die vier Werte eintragen. Diese Datei enthält echte Zugangsdaten – sie darf nie ins Git-Repository. Am besten sofort in .gitignore eintragen:

echo "infra/terraform.tfvars" >> .gitignore
subscription_id = "..."
client_id       = "..."
client_secret   = "..."
tenant_id       = "..."

Infrastruktur anlegen
#

cd infra
tofu init
tofu apply
cd ..

tofu init lädt den Azure-Provider herunter. tofu apply zeigt dir zuerst eine Vorschau aller Ressourcen die angelegt werden – mit yes bestätigen.


Phase 3 – CI/CD mit GitHub Actions
#

Ab hier läuft jedes Deployment automatisch: Push auf main → Hugo baut die Website → Azure veröffentlicht sie.

7. GitHub-Repository einrichten
#

Falls noch nicht vorhanden, ein leeres Repository auf github.com anlegen. Dann lokal verbinden:

git remote add origin https://github.com/<dein-username>/my-site.git

8. GitHub Actions konfigurieren
#

.github/workflows/deploy.yml anlegen:

name: Deploy rdgy.at to Azure

on:
  push:
    branches: [main]

jobs:
  build_and_deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          submodules: true
          fetch-depth: 0

      - name: Setup Hugo
        uses: peaceiris/actions-hugo@v3
        with:
          hugo-version: '0.157.0'
          extended: true

      - name: Build
        run: hugo --minify

      - name: Deploy to Azure Static Web Apps
        uses: Azure/static-web-apps-deploy@v1
        with:
          azure_static_web_apps_api_token: ${{ secrets.AZURE_STATIC_WEB_APPS_API_TOKEN }}
          repo_token: ${{ secrets.GITHUB_TOKEN }}
          action: "upload"
          app_location: "public"
          output_location: ""
          skip_app_build: true

Hugo baut die Website nach public/ – der Deploy-Schritt lädt diesen Ordner direkt nach Azure hoch.

9. GitHub-Secret anlegen
#

Das Deployment-Token aus OpenTofu auslesen:

cd infra
tofu output -raw deployment_token
cd ..

Diesen Wert als Secret in GitHub hinterlegen:

  • Repository → Settings → Secrets and variables → Actions → New repository secret

Name:

AZURE_STATIC_WEB_APPS_API_TOKEN

Phase 4 – Domain & Live
#

10. Domain konfigurieren
#

Schritt 1: CNAME-Ziel auslesen

cd infra
tofu output www_cname_target
cd ..

Die Ausgabe sieht ungefähr so aus:

proud-rock-abc123.azurestaticapps.net

Schritt 2: DNS-Einträge setzen

Beim DNS-Anbieter (z.B. Cloudflare, namecheap, domains.google) folgende zwei Einträge anlegen:

TypNameWert
CNAMEwwwproud-rock-abc123.azurestaticapps.net
Redirect / ALIAS@ (Apex)https://www.example.com

Der www-Eintrag verbindet deine Domain direkt mit Azure. Den Apex-Eintrag (example.com ohne www) setzt du als HTTP-Weiterleitung – die meisten DNS-Anbieter nennen das „Redirect" oder „URL Forward".

Schritt 3: Domain-Validierung abwarten

Azure prüft automatisch, ob der CNAME-Eintrag korrekt gesetzt ist. Das kann einige Minuten bis zu einer Stunde dauern, je nach DNS-Anbieter. Danach ist HTTPS automatisch aktiv – kein Zertifikat manuell beantragen.

Status prüfen:

az staticwebapp hostname list --name stapp-my-site --resource-group rg-my-site

Sobald provisioningState auf Succeeded steht, ist die Domain aktiv.

11. Pushen und live gehen
#

git add .
git commit -m "feat: initial site"
git push origin main

GitHub Actions startet automatisch, baut die Website und deployed sie zu Azure Static Web Apps.


Kurzfassung
#

PhaseWas passiert
1 – LokalHugo + Blowfish aufsetzen, Inhalte schreiben, lokal testen
2 – IaCAzure-Infrastruktur mit OpenTofu anlegen
3 – CI/CDGitHub Actions für automatisches Deployment einrichten
4 – LiveDomain konfigurieren, ersten Push machen

Das Ergebnis ist eine schnelle, statische Website mit Blog, Projekten und zwei Sprachen – ohne eigenen Server.