Social Hub mit Azure Boardmitteln

Ich hab mir meinen eigenen Social Hub gebaut. Also eine einzige Seite, auf der alles zusammenläuft: LinkedIn, Instagram, der Blog, der Podcast, der Newsletter, meine Speaker-Termine. Ein Link, den ich weitergebe, und dahinter steckt der ganze Röstmeister-Kosmos statt drei einzelner URLs, von denen ich beim Tippen sowieso den vierten vergesse.

Falls dir der Begriff nichts sagt: Solche „eine Seite für alle Kanäle“ laufen oft unter Namen wie Linktree. Das sind fertige Bio-Link-Tools, SaaS, generisch, deine Daten liegen bei einem Dritten, und für alles jenseits der Basics zahlst du. Ich wollte das gleiche Ergebnis, aber selbst gebaut und in meiner Hand.

Also hab ich mir gedacht: Das kann ich mir selbst bauen. Auf Azure. Und weil ich es später erweitern will, ohne mich jedes Mal durchs Portal zu klicken, mit Infrastructure as Code. Ein Kollege, dem ich das Ergebnis gezeigt hab, meinte nur: schon ’ne coole Sache. Das war der Ansporn, es sauber zu Ende zu bringen und öffentlich zu machen.

Kleiner Reality-Check vorweg, wer mich kennt weiß es: Ich bin kein Entwickler. Ich bin Consultant. Ich hab bei Bicep und beim Express-Server ab und zu eine KI um Rat gefragt, so wie man einen Kollegen fragt, der die Ecke besser kennt. Aber genau das ist der Punkt. Man muss kein Entwickler sein, um mal einen Proof of Concept aufzusetzen und zu schauen, was draus wird. Rausgekommen ist eine Seite, die seit drei Monaten produktiv läuft. Kosten bisher: null Euro.

Den kompletten, lauffähigen Code habe ich als öffentliches Blueprint-Repo bereitgestellt, sauber anonymisiert, MIT-Lizenz: github.com/DerM365Roestmeister/m365-social-hub-blueprint. Fork it, trag deine Links ein, deploy. In diesem Artikel gehen wir von der Idee über die Architektur bis zum echten Bicep-Code und dem Deployment.

Der Röstmeister Social Hub: eine Seite mit allen Links zu Blog, Podcast, Newsletter, Social-Media-Kanälen und Speaker-Terminen
So sieht das Ergebnis aus: ein Link, dahinter alles. Läuft auf Azure App Service im F1-Tier, seit drei Monaten, für 0 €.

Die Idee: bewusst simpel

Meine Anforderungen waren überschaubar. Eine Seite. Alle Links, Publikationen und Events darauf. Schnell, kein Tracking, kein Drittanbieter dazwischen. Volle Kontrolle über Design und Daten.

Was ich ausdrücklich nicht wollte: eine Datenbank, ein CMS, einen Build-Prozess mit halbem Frontend-Framework. Für eine Seite mit ein paar Link-Karten ist das mit Kanonen auf Spatzen. Also: eine statische index.html, ausgeliefert von einem winzigen Node/Express-Server. Mehr nicht. Der Avatar ist reines CSS, Initialen im Kreis, damit ich nicht mal ein Bild irgendwo hosten muss.

Die Entscheidung gegen ein fertiges Bio-Link-Tool war schnell getroffen. Ich wollte kein fremdes Branding, meine Daten nicht bei einem Dritten und keine Paywall für zwei Extra-Buttons. Der Preis dafür ist etwas Eigenarbeit. Der Gewinn ist, dass mir das Ding komplett gehört.

Warum Azure App Service

Für M365-Admins ist Azure kein fremder Planet. Gleicher Tenant, gleiches Billing, gleiche Kachel im Portal. Wenn du ohnehin täglich in Entra ID und Conditional Access unterwegs bist, ist „mal eben eine kleine Web-App hosten“ kein Kontextwechsel, sondern der Nachbargang.

App Service ist für so eine kleine Node-App genau richtig. Du musst keinen Server patchen, HTTPS ist eingebaut, und falls es doch mal größer werden soll, skalierst du hoch statt neu zu bauen. Der eigentliche Aufhänger für alle, die „mal eben was Kleines“ hosten wollen, ist aber der Preis: Ich fahre die Seite auf dem F1-Tier, dem Free Tier. Drei Monate live, null Euro Rechnung.

ℹ️
Info

Azure Static Web Apps wäre für eine reine statische Seite auch ein Kandidat gewesen. Ich habe mich trotzdem für App Service mit einem kleinen Express-Server entschieden, weil ich zwei Dinge selbst in der Hand haben wollte: eigene Security-Header (Content-Security-Policy, HSTS und Kollegen) und einen /health-Endpoint für die Health-Checks. Das ist Geschmackssache, aber es war meine bewusste Entscheidung, kein Zufall.

Warum Bicep statt Klicken oder roher CLI

Angefangen hat das Projekt unspektakulär: mit imperativen az-Befehlen. Resource Group anlegen, Plan anlegen, Web App anlegen. Schritt für Schritt, brav der Reihe nach.

Das funktioniert. Beim ersten Mal. Das Problem kommt beim zweiten Mal. Willst du die Seite nochmal in einer anderen Region aufsetzen oder einem Kollegen zeigen, wie es geht, dann kopierst du Befehle aus einer alten Notiz zusammen und hoffst, dass du nichts vergessen hast. Keine Versionierung, keine Nachvollziehbarkeit, welche Ressource wann warum entstanden ist.

Bicep löst genau das. Du beschreibst den Zielzustand deklarativ, legst die Datei in Git, und ein einziger Befehl baut die komplette Infrastruktur. Für M365-Admins ist das gedanklich dasselbe Muster wie eine Configuration Baseline oder PowerShell DSC: Du sagst nicht „mach Schritt 1, dann 2, dann 3“, du sagst „so soll es am Ende aussehen“.

Und ja, Bicep kompiliert am Ende zu ARM-JSON. Aber du liest und schreibst eine saubere Konfigurationsdatei statt einer JSON-Wand mit spitzen Klammern. Kein ARM-Krieg. Das war der Moment, in dem ich als Nicht-Entwickler gemerkt habe: Das kann ich lesen. Das kann ich anpassen.

Die Architektur

Das Ganze ist absichtlich klein gehalten. Eine Resource Group, darin ein App Service Plan auf F1, darauf eine Web App mit Node 20 LTS, die per Express die statische index.html ausliefert. Keine Datenbank, kein CDN, keine Custom Domain im Standardfall.

Ebene 1
Resource Group
Wird vom Bicep-Template selbst angelegt, Standardname rg-<appName>.
Ebene 2
App Service Plan (F1, Linux)
Free Tier, ein Worker, kein Server-Management.
Ebene 3
Web App (Node 20 LTS)
HTTPS erzwungen, npm install läuft beim Deploy automatisch auf der Plattform.
Ebene 4
Express liefert die statische Seite
Anfrage rein, Security-Header setzen, index.html ausliefern, fertig.

Der komplette Code dafür liegt offen im Blueprint-Repo: github.com/DerM365Roestmeister/m365-social-hub-blueprint.

Der Bicep-Code im Detail

Jetzt zum Kern. Alle Snippets sind 1:1 aus dem öffentlichen Repo, nichts nachgestellt.

Alles in einem Deploy: Subscription-Scope

Der Trick, der mir das manuelle az group create erspart, steckt gleich in Zeile eins von infra/main.bicep. Das Template arbeitet auf Subscription-Ebene und legt die Resource Group selbst mit an:

targetScope = 'subscription'

@description('Name of the Web App. Must be globally unique across Azure.')
param appName string

@description('Azure region for all resources.')
param location string = 'westeurope'

@description('App Service Plan SKU. F1 is the free tier.')
param sku string = 'F1'

resource rg 'Microsoft.Resources/resourceGroups@2024-03-01' = {
  name: resourceGroupName
  location: location
}

module appService 'modules/appservice.bicep' = {
  name: 'appServiceDeployment'
  scope: rg
  params: {
    appName: appName
    location: location
    sku: sku
    nodeVersion: nodeVersion
    enableCustomDomain: enableCustomDomain
    customDomainName: customDomainName
  }
}

Die Pointe: Ein einziger az deployment sub create erstellt Resource Group, Plan und Web App in einem Rutsch. Kein Vorspiel, kein „erst die Gruppe, dann der Rest“.

Das App-Service-Modul

Die eigentlichen Ressourcen liegen in einem separaten Modul, infra/modules/appservice.bicep. Das ist das Modul-Pattern in Bicep: Das Subscription-Template kümmert sich um die Resource Group, das Modul läuft im Scope dieser Gruppe und baut Plan und App. Sauberer, als alles in eine Datei zu quetschen.

resource appServicePlan 'Microsoft.Web/serverfarms@2023-12-01' = {
  name: appServicePlanName
  location: location
  kind: 'linux'
  properties: {
    reserved: true
  }
  sku: {
    name: sku
  }
}

resource webApp 'Microsoft.Web/sites@2023-12-01' = {
  name: appName
  location: location
  properties: {
    serverFarmId: appServicePlan.id
    siteConfig: {
      linuxFxVersion: 'NODE|${nodeVersion}'
      appSettings: [
        {
          name: 'SCM_DO_BUILD_DURING_DEPLOYMENT'
          value: 'true'
        }
      ]
    }
    httpsOnly: true
  }
}

Zwei Dinge sind wichtig. linuxFxVersion sagt App Service, welche Runtime laufen soll. Und SCM_DO_BUILD_DURING_DEPLOYMENT auf true sorgt dafür, dass Azure beim Deploy selbst npm install fährt. Du lädst deinen Code hoch, den Rest macht die Plattform. httpsOnly erzwingt HTTPS, ohne dass du dich drum kümmern musst.

Parameter statt Hardcoding

Das hier ist für mich der eigentliche Aha-Moment gewesen, warum sich der ganze Blueprint überhaupt teilen lässt. In infra/main.bicepparam stehen keine echten Namen, nur Platzhalter:

using 'main.bicep'

param appName = 'change-me-app-name'
param location = 'westeurope'
param sku = 'F1'
param nodeVersion = '20-lts'
param enableCustomDomain = false
param customDomainName = ''

So macht man Infrastruktur-Code wiederverwendbar. Kein Tenant-Bezug im Template, nur Werte, die jede und jeder selbst ausfüllt. Genau deshalb kann ich das Repo öffentlich stellen und du kannst es 1:1 forken, ohne dass irgendwo meine Daten kleben.

Custom Domain, aber nur wenn du willst

Das Template wächst mit, ohne dass du bei F1 festhängst. Custom Domain und Managed Certificate werden nur angelegt, wenn du den Schalter umlegst. In Bicep sind das konditionale Ressourcen mit if:

resource hostNameBinding 'Microsoft.Web/sites/hostNameBindings@2023-12-01' = if (enableCustomDomain) {
  parent: webApp
  name: customDomainName
  properties: {
    siteName: appName
    hostNameType: 'Verified'
  }
}
⚠️
Achtung

Custom Domains gibt es auf dem F1 Free Tier nicht. Dafür brauchst du mindestens B1. Steht so auch als Kommentar im Code. Ein Parameter, ein Redeploy, fertig. Aber es kostet dann eben.

Deployment in der Praxis

So sieht der Ablauf aus, den ich tatsächlich benutze.

1
Trockenlauf mit What-If
Zeigt dir vorher, welche Ressourcen entstehen, geändert oder gelöscht würden, bevor irgendetwas real passiert.
2
Infrastruktur deployen
Ein Befehl, komplette Infrastruktur: Resource Group, Plan, Web App.
3
Anwendungscode separat ausliefern
Per ZIP-Deploy. Infrastruktur und Code sind zwei Paar Schuhe, und es ist gute Praxis, sie auch getrennt zu behandeln.
az deployment sub what-if \
  --location westeurope \
  --template-file infra/main.bicep \
  --parameters infra/main.bicepparam

Sieht das gut aus, kommt der echte Deploy:

az deployment sub create \
  --location westeurope \
  --template-file infra/main.bicep \
  --parameters infra/main.bicepparam

Und danach der Code:

zip -r deploy.zip . -x "node_modules/*" -x ".git/*" -x "*.zip"
az webapp deploy \
  --name <appName> \
  --resource-group rg-<appName> \
  --src-path deploy.zip \
  --type zip

Der klassische Stolperstein bei Node auf App Service ist übrigens das Startverhalten. Wenn die App nicht hochkommt, liegt es fast immer daran, dass der Startbefehl oder der PORT nicht stimmt. Deshalb liest der Express-Server den Port aus process.env.PORT, den App Service selbst setzt, und fällt lokal auf 3000 zurück. Genau die Art Detail, bei der ich mir eine zweite Meinung von der KI geholt habe, statt eine Stunde im Log zu suchen.

Die Kosten-Realität, ohne Schönfärberei

Nochmal klar: drei Monate live, F1-Tier, 0 € Rechnung. Das ist kein Marketing, das ist meine Abrechnung.

Tier Was du bekommst Für wen geeignet
F1 · Standard im Blueprint
0 € pro Monat
Kein Custom Domain, Tageskontingent von 60 CPU-Minuten (danach 403 bis zum Reset), kein „Always On“, also gelegentlich ein Cold Start Persönliche Bio-Seite mit überschaubarem Traffic. Für eine produktive Kunden-App der falsche Ort.
B1 · Upgrade-Pfad
rund 13 € pro Monat
Custom Domain möglich, dedizierte Compute-Ressourcen, kein Tages-Quota Wer eine eigene Domain will oder verlässliche Erreichbarkeit braucht.

Der Umstieg ist dank Bicep trivial: sku von F1 auf B1, enableCustomDomain auf true, redeploy. Preise ändern sich, also immer kurz auf der Azure-Preisseite gegenprüfen.

Vom eigenen Projekt zum öffentlichen Blueprint

Warum ein separates, bereinigtes Repo, statt einfach meinen Live-Code zu zeigen? Weil in meiner produktiven Version echte Links, echte Termine und mein Tenant-Bezug drinstecken. Das gehört nicht in die Öffentlichkeit. Gleichzeitig wollte ich keinen Pseudo-Code zeigen, der „so ähnlich“ aussieht, sondern echten, lauffähigen Code.

Die Lösung: eine fiktive Beispiel-Persona, Platzhalter-Links, MIT-Lizenz. Bewusst so gebaut, dass du es forkst, deine Inhalte einträgst und deployst, ohne irgendwo meinen Kram herauspulen zu müssen. Das ist der Unterschied zwischen einem Screenshot und einem Blueprint.

Zum Nachbauen

Repo hier: github.com/DerM365Roestmeister/m365-social-hub-blueprint · Fork das Repo, trag deine eigenen Links ein, az deployment sub create — und du hast in 10 Minuten deine eigene Version, für 0 €.

☕ Fazit · Ferdi-Style

Was bleibt

Ein persönlicher Social Hub muss weder teuer noch kompliziert sein. Mit App Service auf F1 kostet er nichts, mit Bicep ist die komplette Infrastruktur in Minuten reproduzierbar und versioniert.

Und man muss dafür kein Entwickler sein. Ein bisschen Neugier, ein bisschen Copy-Paste mit Verstand, ab und zu eine KI als Beifahrer. Der Rest ist Zielzustand beschreiben statt Schritte abarbeiten.

Espresso-Moment

Der beste Proof of Concept ist der, der drei Monate später immer noch läuft und keinen Cent gekostet hat.

Das nimmst du mit

  • App Service F1 hostet eine kleine Node-Seite kostenlos, mit den bekannten Grenzen: kein Custom Domain, 60 CPU-Minuten am Tag, Cold Start
  • Bicep beschreibt die Infrastruktur deklarativ und versionierbar — dasselbe Denkmuster wie eine Configuration Baseline
  • targetScope = 'subscription' legt die Resource Group gleich mit an, ein Deploy statt drei Klicks
  • Parameter statt Hardcoding macht dein Template teilbar, ganz ohne Tenant-Bezug im Code
  • az deployment sub what-if als Trockenlauf, bevor echte Ressourcen entstehen
  • Der Umstieg auf eine Custom Domain (B1, rund 13 € im Monat) ist ein Parameter plus Redeploy
  • Du musst kein Entwickler sein, um einen POC zu wagen — forken, deployen, schauen was draus wird

Du hast das Repo geforkt oder eine Idee, was noch rein müsste? Schreib mir auf LinkedIn.

#EspressoM365Fusion