Azure-arkitektur

4. Azure-arkitektur

Denne side er din mental-model af TesseraCMS's Azure-infrastruktur — hvad lever hvor, hvordan de hænger sammen, og hvad du skal vide for at drifte det.

Resource group

Alt lever i rg-palle-portfolio (legacy-navn fra dengang dette var Palle's portfolio; nu hoster det hele platformen). Resource group + subscription:

Subscription: 1df54212-9e59-4904-a0e5-0bcfe5529ba1
Resource group: rg-palle-portfolio
Region: northeurope

Når vi laver nye ressourcer, prefix med tesseracms-* så det er klart de tilhører platform-CMSet og ikke Palle specifikt.

Container App

tesseracms — Azure Container App der hoster Next.js SSR-appen.

  • Image: bygges fra Dockerfile i repo'et; pushet til Azure Container Registry (ACR) palleportfolioacr af GitHub Actions
  • Replicas: min 1, max 3 (auto-scaling baseret på HTTP-traffic)
  • CPU/memory: 0.5 CPU / 1 GB per replica
  • Ingress: external (offentligt tilgængelig), port 3000
  • Easy Auth: AAD provider configureret → app er hidden bag login (med public-page-undtagelser via middleware)
  • Custom domains: pallejacobsen.dk + nyborg-rideklub.dk + flere (per tenant). Hver med managed cert

Skalering

Default håndterer fint dagens trafik (~1000 requests/dag på tværs af alle tenants). Hvis vi vokser:

  • Bump max-replicas til 5-10
  • Overvej Workload profile til consumption+dedicated mix
  • For >10k req/dag overvej caching-layer foran (CDN eller Front Door)

Storage Account

palleportfoliostor — Azure Storage account med både tabeller og blobs.

Tables

  • Tenants — én row pr. tenant (slug, displayName, siteType, layoutPackage, status, customDomains)
  • TenantModules — én row pr. tenant + module (aktivering, konfig som JSON)
  • TenantRoles — én row pr. tenant + bruger (rolle, tildelingsdato)
  • RoleAuditLog — append-only log over rolle-ændringer
  • ContentItems — ALT user-genereret content (pages, heros, formularer, rollups, brands, snippets, ponies, news, …). Partition key = <slug>:<type>, row key = <id>
  • Counters — sequential ID-tællere pr. (tenant, type)
  • FormSubmissions — indkomne form-submissions (separat fra ContentItems for retention-politik)
  • MediaIndex — index over uploadede blobs med alt-text, dimensions, refs

Blobs

  • media-container — alle uploadede billeder + dokumenter
  • Path-struktur: <tenant-slug>/<type>/<id>/<filename> — fx palle_jacobsen/Hero/hero_main/cover.jpg
  • Public access: blob-level (offentligt læsbart), container-level lukket
  • CORS: konfigureret via scripts/configure-blob-cors.ts — kun vores domains må læse

Azure Communication Services (Email)

tesseracms-acs — ACS-ressource til transactional email (form-submission-notifikationer, invite-mails).

  • Connected email-domain: managed domæne (Azure-leveret) eller custom domain afhængigt af tenant
  • Sender adresse: fx DoNotReply@<some-hash>.azurecomm.net for managed, eller noreply@kunde.dk for custom
  • Templates: bor i koden som MJML, kompileres til HTML on-send
  • Brand inheritance: hver tenant kan have egen logo + farver via MailAsset-records (TASK-126-ish)

Cost-control: ACS koster pr. email + pr. data-volume. Vi sender ~10-50 mails/dag i øjeblikket — godt under nogen tærskel.

GitHub Actions deploy-pipeline

.github/workflows/deploy.yml håndterer CI/CD:

  1. Trigger: push til master
  2. Build job:
    • Checkout
    • Setup Node 22
    • npm ci
    • npx tsc --noEmit (typecheck)
    • npm run build (Next standalone-build)
    • Build Docker image fra Dockerfile
    • Push image til ACR
  3. Deploy job:
    • Azure login via OIDC (federated credential — INGEN secret)
    • az containerapp update med ny image-tag
    • Container App roller automatisk replicas til nye image

Typisk varighed: 3-4 min. Hvis det går længere, tjek Actions-fanen for stuck step.

Roll-back: GitHub → Actions → forrige successful run → Re-run all jobs → samme image-tag deploys igen.

Identity og auth

  • Azure AD tenant: deler vi med Anthropic (mit ejer-tenant — vi har et eget app-registration)
  • App registration: TesseraCMS-Auth — håndterer Easy Auth-flowet
  • Graph API client: TesseraCMS-Graph — bruges til user-search + B2B-invite. Credentials i Container App env-vars (GRAPH_CLIENT_ID, GRAPH_CLIENT_SECRET, GRAPH_TENANT_ID)
  • Federated credential: GitHub Actions bruger OIDC; ingen long-lived secret i repo

DNS & certs

Kunder ejer deres egne domains. For at koble:

  1. Kunden tilføjer CNAME-record: <kunde-domain><acaapp-default-domain>.azurecontainerapps.io
  2. Vi tilføjer custom domain i Azure (Verification + binding) — Let's Encrypt-cert udstedes automatisk
  3. Vi tilføjer domain i Tenants.customDomains
  4. Forventet propagation: 5 min - 24 timer (DNS-cache)

Cert auto-renewal: Azure håndterer det, men hvis CNAME går væk, fejler renewal. Tjek Custom domains-blade'n nu og da for warning-ikoner.

Cost-overblik (omtrentligt)

Månedlig drift:

  • Container App: ~50-150 kr (afhænger af scaling-aktivitet)
  • Storage Account: ~5-20 kr (lavt; vi har ikke meget data endnu)
  • ACS Email: ~5-30 kr (afhænger af volumen)
  • ACR: ~10 kr (basic tier)
  • DNS records (hvis vi hoster): ~0 (kunden ejer deres)

Total: ~70-200 kr/måned for hele platformen. Bumps når vi får flere tenants med mere trafik.