Julian Unruh
EN

Schlanke Website auf AWS: sicher, schnell und fast kostenlos

· 8 Min. Lesezeit #aws #opentofu #security #cloudfront

Ein Turm aus flachen Steinen auf einem Felsen am Meer, dahinter der Abendhimmel
Foto: Unsplash

Für eine eigene Website stehen meist zwei Wege im Raum. Ein Baukasten kostet jeden Monat 20+ Euro und erlaubt nur, was der Anbieter vorsieht. Ein eigener Server bietet alle Freiheiten, muss aber gepatcht, überwacht und abgesichert werden, auch wenn er die meiste Zeit nichts zu tun hat.

Es gibt einen dritten Weg, der für Portfolios, Firmenseiten, Blogs und Dokumentationen oft der beste ist: Die Seite wird statisch gebaut und von einem CDN ausgeliefert. Nur die wenigen Teile, die wirklich dynamisch sein müssen, laufen als einzelne Funktionen. Das Ergebnis ist schnell, bietet kaum Angriffsfläche und kostet fast nichts.

Wie das konkret aussieht, zeigt diese Seite. nelumbo.dev läuft genau so, und der AWS-Anteil der Rechnung liegt unter einem Dollar im Monat.

Das Prinzip: statisch ausliefern, nur das Nötige dynamisch

Ein Static Site Generator wie Astro, Hugo oder Eleventy macht aus Texten und Vorlagen fertige HTML-, CSS- und Bilddateien. Auf dem Server läuft danach kein Code mehr, es gibt keine Datenbank und kein Admin-Login. Was nicht läuft, muss nicht gepatcht werden, und es kostet nichts, solange niemand die Seite aufruft.

Alles Dynamische, etwa ein Kontaktformular, übernimmt eine Lambda-Funktion. Sie läuft nur bei einem Aufruf und wird auch nur pro Aufruf abgerechnet.

Der Aufbau besteht aus wenigen Bausteinen:

  • S3 speichert die gebauten Dateien.
  • CloudFront liefert sie aus, mit kostenlosem TLS-Zertifikat.
  • Eine CloudFront Function sorgt für saubere URLs.
  • Eine Lambda-Funktion mit SES verschickt das Kontaktformular.
  • Cloudflare übernimmt Domain, DNS und die Weiterleitung von Mails.

Ein privater Bucket als Ablage

Viele Anleitungen empfehlen, bei S3 das „Static Website Hosting“ einzuschalten und den Bucket öffentlich zu machen. Davon ist abzuraten. Mit eigener Domain gibt es so kein HTTPS, und die Dateien wären auch an CloudFront vorbei erreichbar.

Besser ist ein privater Bucket, bei dem alle Schalter von „Block Public Access“ aktiv sind. Lesen darf ihn nur die eigene CloudFront-Distribution. Das regelt Origin Access Control: CloudFront signiert jede Anfrage an S3, und die Bucket Policy erlaubt nur Anfragen, die von genau dieser Distribution kommen.

Zwei Ergänzungen lohnen sich:

  • CloudFront sollte neben s3:GetObject auch s3:ListBucket dürfen. Sonst antwortet S3 auf fehlende Seiten mit „403 Forbidden“ statt mit „404 Not Found“, und die eigene 404-Seite greift nicht.
  • Die Bucket Policy verbietet über die Bedingung aws:SecureTransport alle Zugriffe ohne TLS. Zusätzlich ist „Block Public Access“ für das gesamte AWS-Konto aktiviert. So kann auch später kein Bucket versehentlich öffentlich werden.

Eine typische Website ist ein paar hundert Kilobyte bis wenige Megabyte groß. Die Speicherkosten sind damit kaum messbar.

CloudFront als Auslieferung

CloudFront liefert die Seite von Standorten in der Nähe der Besucher aus. Aktiviert sind HTTP/2 und HTTP/3, IPv6 und die automatische Komprimierung mit Brotli oder gzip. Die günstigste Preisklasse nutzt nur Standorte in Europa, Nordamerika und Israel. Für die meisten Seiten reicht das völlig, Besucher aus anderen Regionen erhalten die Seite trotzdem.

Bei kleinen Seiten fallen für CloudFront in der Regel keine Kosten an. Das dauerhafte Free Tier umfasst 1 TB Datenübertragung und 10 Millionen Anfragen pro Monat.

Das Zertifikat stellt AWS Certificate Manager kostenlos aus. Für CloudFront muss es in der Region us-east-1 liegen. Es erneuert sich selbst und braucht danach keine Pflege mehr. Als Sicherheitsrichtlinie für Verbindungen ist TLSv1.2_2025 gesetzt. Damit sind nur noch TLS 1.2 mit starken Verschlüsselungsverfahren und TLS 1.3 erlaubt.

Saubere URLs mit einer CloudFront Function

Static Site Generators legen Seiten meist als Ordner an, also /blog/beispiel/index.html. S3 ergänzt bei privatem Zugriff aber kein index.html. Ohne Hilfe führt /blog/beispiel/ ins Leere.

Das löst eine kleine CloudFront Function. Sie läuft bei jeder Anfrage direkt am Edge, noch vor dem Blick in den Cache. Für nelumbo.dev sieht sie so aus:

function handler(event) {
  var request = event.request;
  var uri = request.uri;

  if (request.headers.host.value !== 'nelumbo.dev') {
    return redirect('https://nelumbo.dev' + uri);
  }
  if (uri.endsWith('/')) {
    request.uri += 'index.html';
  } else if (!uri.split('/').pop().includes('.')) {
    return redirect(uri + '/');
  }
  return request;
}

Die Funktion hängt index.html an und ergänzt fehlende Schrägstriche am Ende. Außerdem leitet sie www und die technische CloudFront-Adresse auf die eigentliche Domain um. So existiert jede Seite unter genau einer URL, was auch Suchmaschinen entgegenkommt. CloudFront Functions sind deutlich leichter als Lambda@Edge und laufen in unter einer Millisekunde. Zwei Millionen Aufrufe im Monat sind kostenlos.

Caching, das zum Deployment passt

Gute Generatoren versehen CSS-, JavaScript- und Schriftdateien mit einem Hash im Dateinamen. Ändert sich der Inhalt, ändert sich der Name. Diese Dateien darf der Browser deshalb ein Jahr lang behalten. HTML dagegen speichert nur CloudFront, und der Cache wird bei jedem Deployment geleert. Mit der AWS CLI sind das drei Befehle:

aws s3 sync dist/_astro s3://$BUCKET/_astro --delete \
  --cache-control "public, max-age=31536000, immutable"
aws s3 sync dist s3://$BUCKET --delete --exclude "_astro/*" \
  --cache-control "public, max-age=0, s-maxage=31536000"
aws cloudfront create-invalidation --distribution-id $DISTRIBUTION --paths "/*"

CloudFront beantwortet damit fast jede Anfrage direkt aus dem Cache, und neue Inhalte sind trotzdem sofort sichtbar. Die Invalidierung ist kostenlos: Die ersten 1.000 Pfade pro Monat sind frei, und /* zählt als ein einziger Pfad.

Sicherheits-Header

Über eine eigene Response Headers Policy hängt CloudFront an jede Antwort Header, die den Browser anweisen, die Seite strenger zu behandeln. Bei nelumbo.dev sind das:

  • Strict-Transport-Security mit zwei Jahren Laufzeit, für alle Subdomains und mit preload. Browser verbinden sich dann ausschließlich per HTTPS.
  • Content-Security-Policy: Sie legt fest, woher Skripte, Styles, Bilder und Schriften kommen dürfen. Astro erzeugt beim Build für jedes eingebettete Skript und jeden Style einen Hash und schreibt die passende Richtlinie direkt in die Seite. Was nicht dazu passt, führt der Browser nicht aus. Die Direktive frame-ancestors 'none' wirkt nur als Header und kommt deshalb von CloudFront.
  • X-Frame-Options und X-Content-Type-Options für ältere Browser.
  • Referrer-Policy, damit andere Seiten nur die Domain sehen, nicht die vollständige Adresse.
  • Permissions-Policy, die Kamera, Mikrofon, Standort und ähnliche Schnittstellen abschaltet.
  • Cross-Origin-Opener-Policy und Cross-Origin-Resource-Policy, die die Seite von fremden Fenstern und Seiten abschotten.

Header, die nur verraten, was hinter der Seite steckt, entfernt die Policy. Zusätzliche Kosten entstehen dadurch nicht.

Für eine schlanke CSP hilft es, Schriften und Skripte nicht von fremden Servern zu laden, sondern selbst auszuliefern. Das hält die Richtlinie einfach, spart Verbindungen und kommt dem Datenschutz zugute.

Ein Kontaktformular ohne Server

Ein Formular braucht keinen API Gateway. Lambda stellt mit der Function URL selbst eine öffentliche HTTPS-Adresse bereit, das spart eine ganze Komponente. Die ARM-Architektur ist bei Lambda günstiger, und 128 MB Speicher reichen für ein Formular völlig aus.

Die Funktion prüft alle Eingaben selbst: Pflichtfelder, Längen und das Format der E-Mail-Adresse. Die Prüfung im Browser allein genügt nie. CORS lässt nur Anfragen von der eigenen Domain zu. Die Nachricht geht über SES hinaus, mit der Adresse des Absenders als Antwortadresse. So lässt sich direkt antworten, ohne dass irgendwo eine Datenbank nötig ist.

Die Rolle der Funktion erhält nur die Rechte, die sie wirklich braucht: Mails über die eigene Domain versenden und ins eigene Log schreiben. Die Logs werden nach 30 Tagen gelöscht.

Die Kosten sind vernachlässigbar. Lambda hat ein dauerhaftes Free Tier mit einer Million Aufrufen im Monat, SES kostet zehn Cent pro tausend Mails.

DNS und Mail bei Cloudflare

Domain und DNS können problemlos bei Cloudflare bleiben. DNS ist dort kostenlos, eine Hosted Zone bei Route 53 kostet dagegen 50 Cent im Monat und damit mehr als der ganze Rest. Die Einträge für CloudFront stehen auf „DNS only“, ohne Cloudflare-Proxy. Zwei CDNs hintereinander bringen keinen Vorteil.

Auch im DNS lässt sich mit wenigen Einträgen viel absichern:

  • DNSSEC signiert die Einträge, sodass niemand Besucher mit gefälschten Antworten umleiten kann. Liegt die Domain bei Cloudflare, genügt ein Schalter.
  • CAA-Einträge legen fest, welche Zertifizierungsstellen überhaupt Zertifikate für die Domain ausstellen dürfen.
  • DKIM für SES und eine strenge DMARC-Richtlinie mit p=reject verhindern, dass jemand glaubwürdig Mails im Namen der Domain verschickt.

Cloudflare Email Routing leitet Adressen wie info@ kostenlos an ein bestehendes Postfach weiter. Ein eigener Mailserver ist nicht nötig.

Alles als Code

Die gesamte Infrastruktur ist als Code beschrieben, hier mit OpenTofu, genauso gut ginge Terraform. Bei nelumbo.dev sind das rund 30 Ressourcen. Jede Änderung ist vorab in tofu plan sichtbar, und die Seite lässt sich jederzeit neu aufbauen.

Der State liegt in einem versionierten S3-Bucket, der ebenfalls nur per TLS erreichbar ist. Seit OpenTofu 1.10 genügt für das Locking eine Lock-Datei im selben Bucket, eine DynamoDB-Tabelle ist nicht mehr nötig. Die AWS-Konto-ID ist fest im Provider hinterlegt. Ist versehentlich das falsche Profil aktiv, bricht OpenTofu ab, statt im falschen Konto Ressourcen anzulegen. Der Cloudflare-Provider legt in derselben Konfiguration alle DNS-Einträge an.

Was das kostet

Die monatliche Rechnung für nelumbo.dev:

Posten Kosten im Monat
S3 (Seite und State) Bruchteile eines Cents
CloudFront inklusive Function 0 USD im Free Tier
Zertifikat (ACM) 0 USD
Lambda 0 USD im Free Tier
SES 0,10 USD pro 1.000 Mails
CloudWatch Logs wenige Cent
DNS, DNSSEC, Mail-Weiterleitung 0 USD bei Cloudflare
Domain .dev rund 1 USD, umgelegt aus dem Jahrespreis

Der AWS-Teil bleibt deutlich unter einem Dollar. Nennenswert kostet nur die Domain.

Wann dieser Aufbau passt

Für Websites, deren Inhalte technisch versierte Leute selbst pflegen, ist dieser Aufbau kaum zu schlagen. Die Seite bleibt schnell, weil fast alles aus dem Cache kommt, und sicher, weil es kaum etwas anzugreifen gibt. Sie wächst ohne Umbau mit, ob für hundert oder für Millionen Besucher. Wird mehr Dynamik gebraucht, etwa eine Suche oder ein Newsletter, kommen weitere Funktionen hinzu, die ebenfalls nur pro Aufruf kosten.

Weniger geeignet ist er, wenn Redakteur:innen ohne technisches Wissen Inhalte im Browser bearbeiten sollen. Dann braucht es zusätzlich ein Headless CMS, das die Seite bei Änderungen neu baut.

Nach dem Einstieg bleibt kaum Pflegeaufwand: Es gibt nichts zu patchen, Zertifikate erneuern sich selbst, und ein Deployment ist ein einziger Befehl.


← Alle Artikel