Ein Load Balancer auf AWS kostet mehr als alle meine Websites zusammen.
Dieser Blog, meine Seite bigmo.laufzeit.dev und die Hadith-Suche deendata.com laufen auf einem einzigen Container-Task in Frankfurt. Mit TLS, Login, zwei Datenbanken und Deploys ohne Ausfall. Das kostet mich rund 10 $ im Monat. Hier siehst du, wie das geht und wo der Haken ist.
So kommt dein Aufruf hier an
- Route 53 liefert für alle drei Domains die öffentliche IP des Tasks (TTL 60 s).
- Caddy im Task nimmt HTTPS an.
- Caddy reicht an Next.js auf
localhost:3000im selben Task weiter. - Next.js wählt die Site nach dem Hostnamen und fragt das passende Atlas-Projekt.
Warum kein Load Balancer?
Ein ALB kostet mit seinen IPv4-Adressen rund 25 $ im Monat. Ein NAT Gateway in Frankfurt rund 35 $. Ich brauche beides nicht.
Den Job des Load Balancers teilen sich hier zwei: Caddy holt die Zertifikate, die Lambda hält die Adresse aktuell. Und statt NAT steht der Task im öffentlichen Subnetz der Default-VPC. Er erreicht ECR, SSM und Atlas direkt. Herein kommt nur Port 80 und 443.
Ein Next.js-Prozess bedient alle drei Sites
Fargate Spot ist bis zu rund 70 % günstiger als On-Demand, ARM64 noch einmal rund 20 %. Mein Task hat die kleinste CPU, die Fargate hergibt: 0,25 vCPU. Dazu kommen 2 GB RAM für zwei Container.
Das reicht, weil nur ein Next.js-Prozess läuft. Jede Site ist ein Ordner unter src/app/sites/, proxy.ts routet nach dem Hostnamen. Eine neue Subdomain ist ein neuer Ordner, sonst nichts.
Den meisten Speicher frisst die deendata-Suche: Ihr Embedding-Modell läuft direkt im Node-Prozess, allein das sind rund 415 MiB.
Anfangs hatte der Task 1 GB. Das ging gut, bis alle drei Sites live waren und ein Crawl über 28.700 deendata-URLs lief. Dann beendete AWS den Container wegen Speichermangels, und rund 2,5 Minuten war alles weg. Ein Ersatz-Task startete von selbst, die Lambda zog das DNS nach. Seitdem hat der Task 2 GB, für rund 0,90 $ mehr im Monat.
Caddy besorgt die Zertifikate selbst
Caddy holt die Zertifikate bei Let's Encrypt per DNS-01 über Route 53. Nur so gibt es Wildcards. Gespeichert werden sie in S3, sonst würde jeder Spot-Neustart neue anfordern. Zugangsdaten braucht Caddy keine, es nutzt die Task-Rolle.
laufzeit.dev, *.laufzeit.dev {
tls {
dns route53 {
hosted_zone_id <zone-laufzeit>
wait_for_route53_sync true
}
}
reverse_proxy localhost:3000
}deendata.com bekommt einen zweiten, gleichen Block mit der eigenen Zone.
Die IP wechselt ständig, eine Lambda zieht nach
Fargate-Tasks bekommen keine feste IP. Jeder neue Task hat eine neue Adresse. ECS meldet jeden Wechsel an EventBridge, und meine Lambda reagiert:
- Task läuft: Sie gibt seine IP in beiden Atlas-Projekten frei, jedes mit eigenem API-Key. Dann wartet sie, bis der Task HEALTHY ist, und stellt das DNS in beiden Zonen um.
- Task gestoppt: Sie nimmt seine IP aus Atlas.
- Service auf 0: Nur dann löscht sie das DNS. Ein Eintrag auf eine IP, die AWS längst weitergegeben hat, lädt zum Subdomain-Takeover ein.
Scheitert ein Schritt, wiederholt EventBridge das Ereignis.
Deploys laufen ohne Ausfall
Anfangs hat ECS erst den alten Task gestoppt. Die Lambda löschte das DNS, und Resolver merkten sich „Domain existiert nicht“ bis zu 15 Minuten lang. Heute läuft es Blue/Green ohne Load Balancer:
- ECS startet den neuen Task neben dem alten.
- Ist er HEALTHY, zeigt das DNS auf ihn.
- Der alte Task bedient nach dem Stopp-Signal noch 60 Sekunden weiter, so lange wie die DNS-TTL.
Dazu habe ich das SOA-Minimum beider Zonen auf 60 gesetzt. Das ist die Zeit, die Resolver eine negative Antwort cachen. Beim ersten Deploy habe ich alle 2 Sekunden abgefragt: 286 Abrufe, 0 Fehler.
Zwei Datenbanken kosten nichts
- Zwei Atlas-M0-Projekte in Frankfurt, kostenlos und strikt getrennt:
laufzeitfür Artikel, Kommentare und Aufrufe,deendatafür die Hadithe samt Vektor- und Volltextsuche. - Login auf laufzeit.dev läuft über Firebase Auth, im kostenlosen Rahmen. Das Session-Cookie gilt nur für diesen Host.
- Secrets wandern von GitHub über den SSM Parameter Store in den Task. Der Secrets Manager würde 0,40 $ pro Secret und Monat kosten.
Wohin gehen die 10 $?
| Posten | $/Monat |
|---|---|
| Fargate Spot ARM (0,25 vCPU / 2 GB) | ~4–5 |
| Öffentliche IPv4-Adresse (1×) | 3,65 |
| Route 53 (2 Hosted Zones) | 1,00 |
| ECR (max. 5 Images pro Repo) | < 0,50 |
| CloudWatch Logs (3 Tage) | < 0,50 |
| Lambda, EventBridge, SSM, S3 | ~0 |
| MongoDB Atlas M0 (2×), Firebase Auth | 0 |
| Summe | ~10 |
Domains sind nicht eingerechnet. Die öffentliche IPv4 kostet fast so viel wie die Rechenleistung. Ein kostenloses AWS-Budget schickt mir bei 8, 10 und 15 $ eine Mail.
Vier Fallen, in die ich getappt bin
- Caddy mit certmagic-s3 braucht
emailim globalen Block. Sonst hält es den Ordnerusersfür die Konto-E-Mail. wait_for_route53_sync truemuss rein. Sonst findet Let's Encrypt den TXT-Eintrag nicht auf allen Nameservern.- Fargate überschreibt
HOSTNAME. SetzHOSTNAME=0.0.0.0in der Task Definition, sonst lauscht Next.js nicht auf127.0.0.1. - GitHub OIDC schickt den
sub-Claim teils mit IDs. Den echten Wert zeigt CloudTrail.
Was du dafür in Kauf nimmst
- Spot-Unterbrechung: Der Ersatz startet sofort, die Lücke dauert höchstens rund eine Minute.
- Keine Hochverfügbarkeit: ein Task, eine Availability Zone, kein automatischer Wechsel auf On-Demand. Läuft zehn Minuten kein Task, bekomme ich eine Mail.
- 0,25 vCPU: Nach einem Neustart braucht die erste deendata-Seite bis zu 20 Sekunden. Aus dem Cache kommt sie dann in 0,05–0,2 s.
Für alles mit SLA: zwei Tasks und ein Load Balancer.
Ohne Load Balancer und NAT zahlst du nur noch für den Task, eine IPv4-Adresse und ein paar Cent drumherum. Kopier dir den Kosten-Prompt oben und rechne dein eigenes Setup durch. Was kostet deins? Schreib es in die Kommentare.
