{"id":29340,"date":"2026-09-13T14:03:55","date_gmt":"2026-09-13T12:03:55","guid":{"rendered":"https:\/\/blog.mi.hdm-stuttgart.de\/?p=29340"},"modified":"2026-09-13T17:47:26","modified_gmt":"2026-09-13T15:47:26","slug":"cloudsheets-copycat-von-googlesheets","status":"publish","type":"post","link":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/09\/13\/cloudsheets-copycat-von-googlesheets\/","title":{"rendered":"Cloudsheets &#8211; Implementierung einer cloud-basierten Echtzeit-Kollaborationsl\u00f6sung"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Eine Anwendung von Lenny Ziegler (lz045), Jonas Gehrung (jg175), Adrian Rossa(ar162)<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/grafik.png\"><img loading=\"lazy\" decoding=\"async\" width=\"940\" height=\"392\" data-attachment-id=\"29342\" data-permalink=\"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/09\/13\/cloudsheets-copycat-von-googlesheets\/grafik-36\/\" data-orig-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/grafik.png\" data-orig-size=\"940,392\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;alt&quot;:&quot;&quot;}\" data-image-title=\"grafik\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/grafik.png\" src=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/grafik.png\" alt=\"\" class=\"wp-image-29342\" srcset=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/grafik.png 940w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/grafik-300x125.png 300w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/grafik-768x320.png 768w\" sizes=\"auto, (max-width: 940px) 100vw, 940px\" \/><\/a><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Was ist Cloudsheets?<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">CloudSheets ist eine Cloud-basierte Web Applikation f\u00fcr die kollaborative Bearbeitung von Tabellen. Ziel des Projekts ist es, die Grundfunktionalit\u00e4t von Google Sheets in einer schlanken Anwendung nachzubauen. Gleichzeitig soll durch den gezielten Einsatz m\u00f6glichst vieler AWS-Services ein cloud-nativer Entwicklungsprozess und praktischer Umgang mit AWS gelernt werden.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Problem<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Die Echtzeitbearbeitung durch mehrere gleichzeitig aktive User und die daf\u00fcr notwendige Synchronisierung und Konfliktl\u00f6sung kann sehr komplex werden. Selbst wenn zwei User parallel dasselbe Feld der Tabelle bearbeiten, muss ein konsistenter Zustand erreicht werden. Zudem m\u00fcssen die Zugriffsrechte auf die erstellten Tabellen klar geregelt werden, um eine gemeinsame Nutzung zu erm\u00f6glichen. Erschwert wird dies dadurch, dass wir uns noch nie mit einer Plattform wie AWS besch\u00e4ftigt haben und die ben\u00f6tigten Services sowie deren Verkn\u00fcpfung erst kennenlernen mussten.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Dar\u00fcber hinaus mussten wir uns damit auseinandersetzen, wie man Updates und Tests durchf\u00fchrt, ohne den laufenden Betrieb der Applikation zu unterbrechen, und wie man effizient im Team arbeitet. Nicht zuletzt spielen die Kosten ebenfalls eine wichtige Rolle f\u00fcr unser Projekt, da wir von Anfang an mit dem 200$-Startguthaben auf AWS auskommen wollen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">L\u00f6sung<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Verwendung eines eigenen WebSocket-Servers im Backend (Hocuspocus) in Kombination mit CRDTs (Conflict-free Replicated Data Types)<\/li>\n\n\n\n<li>Unterteilung der Architektur in einen Produktionsstack (prod) und einen Entwicklungsstack (dev)<\/li>\n\n\n\n<li>Deployment mithilfe einer GitHub Actions Pipeline und Dokumentation mit MkDocs auf GitHub Pages<\/li>\n\n\n\n<li>Zerst\u00f6rung der Stacks nach dem Testlauf mittels <code class=\"\" data-line=\"\">cdk destroy -all <\/code>(keine laufenden Kosten) sowie der Einsatz kosteng\u00fcnstiger Services wie des Parameter Store statt des Secrets Managers<\/li>\n\n\n\n<li>Nutzung bestehender Libraries wie AG Grid im Frontend, um den Aufwand an Stellen zu minimieren, die nicht im Fokus des Projekts stehen<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Features<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Simples funktionales Frontend<\/li>\n\n\n\n<li>Account-Registrierung und Login mit Multi-Faktor-Authentifizierung<\/li>\n\n\n\n<li>Management von Sheets (Erstellen, L\u00f6schen, Teilen)<\/li>\n\n\n\n<li>Rollensystem mit Berechtigungsstufen (Owner, Viewer, Editor)<\/li>\n\n\n\n<li>Kollaboratives Bearbeiten von Sheets in Echtzeit<\/li>\n\n\n\n<li>Skalierung und Monitoring mithilfe von AWS<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Architektur und Anforderungen<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><a href=\"https:\/\/jobonabas.github.io\/CloudSheets\/architecture\/#diagram\">https:\/\/jobonabas.github.io\/CloudSheets\/architecture\/#diagram<\/a><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Wir haben uns f\u00fcr eine monolithische Schichtenarchitektur entschieden, da diese f\u00fcr unser Projekt den besten Kompromiss aus einfacher Wartbarkeit und geringem Overhead durch Einfachheit bietet. Der Nachteil des \u201cSingle Point of Failure\u201d wird von uns bewusst in Kauf genommen. Da wir uns konsequent f\u00fcr TypeScript als Programmiersprache entschieden haben, nutzen wir dies sowohl im Backend mit Fastify als auch im Frontend mit React\/Vite. Dies schafft eine einheitliche Codebasis, in der wir direkt von der Typisierung profitieren und Konflikte zwischen Front- und Backend minimieren.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00fcr die Datenhaltung kommt RDS (PostgreSQL) zum Einsatz. Da unser Kernfeature das Editieren von Tabellen ist, passt die relationale Struktur perfekt. Au\u00dferdem haben wir im Studium bereits mit relationalen Datenbanken gearbeitet.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Infrastruktur<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">AWS bietet drei Optionen f\u00fcr Backend-Logik: Lambda (FaaS), EC2 (Virtual Machines) und ECS (Container). Bei unserem Projekt entschieden wir uns f\u00fcr ECS\/ECR, also Container, da es den idealen \u201cMiddleground\u201d darstellt. Folgende Vor- und Nachteile bieten sich im Vergleich<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Im Vergleich mit Lambda: Lambda ist zustandslos, unsere Echtzeit-Editing-Funktion (WebSockets) w\u00e4re dort nur mit Aufwand umsetzbar.<\/li>\n\n\n\n<li>Im Vergleich zu ECS2 (VMs): Virtuelle Maschinen bieten mehr Kontrolle und Konfigurationsm\u00f6glichkeiten, welche f\u00fcr unser Projekt unn\u00f6tige Komplexit\u00e4t bedeuten w\u00fcrde.<\/li>\n\n\n\n<li>Container sind au\u00dferdem kosteng\u00fcnstig und eignen sich f\u00fcr unsere Websocket Anwendung.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Die gesamte Infrastruktur wird \u00fcber CDK in TypeScript definiert, um neben der Versionierbarkeit volle Kontrolle \u00fcber die Infrastruktur zu behalten.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Der Weg zum fertigen Deployment war jedoch von mehreren Entscheidungen und Kompromissen gepr\u00e4gt:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Frontend: Urspr\u00fcnglich wollten wir auf CloudFront verzichten, um Kosten zu sparen. Da S3 (als Bucket) kein HTTPS nativ unterst\u00fctzt, w\u00e4re die Domain jedoch nicht sicher gewesen. Da CloudFront jedoch schlussendlich doch der sauberste und damit kosteng\u00fcnstigste Weg ist, TLS\/HTTPS bereitzustellen, haben wir uns schlie\u00dflich dazu entschieden, es doch zu verwenden.<\/li>\n\n\n\n<li>Stack-Management: Um \u00c4nderungen in einer echten AWS Umgebung zu testen bevor sie in Betrieb gebracht werden unterteilen wir in einen Produktions (prod) und einen Dev-Stack (dev).<\/li>\n<\/ul>\n\n\n\n<h3 class=\"wp-block-heading\">Security<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Sicherheit stellte f\u00fcr uns einen zentralen Aspekt unserer Architekturplanung dar:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Authentifizierung: Da ein externer Auth-Provider zu einem Vendor-Lock-in f\u00fchren k\u00f6nnte und ein eigenes JWT-System zus\u00e4tzlichen Aufwand bedeuten w\u00fcrde, entschieden wir uns, AWS Cognito zu verwenden. Cognito integriert sich zudem nahtlos in das AWS-\u00d6kosystem und in unser CDK-Skript.<\/li>\n\n\n\n<li>Credentials: Initial war der Plan ausschlie\u00dflich, den Parameter Store aufgrund der niedrigen Kosten zu verwenden. Jedoch scheint der Secrets Manager AWS Best Practice zu sein und RDS generiert sein Passwort bereits automatisch dort. Deshalb nutzten wir schlussendlich beides.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Realtime-Collaboration<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Der Kern unserer Anwendung ist die Kollaboration in Echtzeit, realisiert \u00fcber WebSockets und CRDTs (Conflict-free Replicated Data Types).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Die Verwendung eines WebSockets erm\u00f6glicht dabei die sofortige \u00dcbertragung von \u00c4nderungen (CRDT-Deltas) an alle verbundenen Clients. Au\u00dferdem k\u00fcmmert sich der WebSocket um den aktiven Push von Nachrichten durch den Server, sobald eine \u00c4nderung stattfindet.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Zun\u00e4chst war der Plan, den offiziellen y-websocket-Server direkt zu nutzen, was jedoch an der mangelnden Kompatibilit\u00e4t mit unserem Fastify-Backend (Import-Probleme und fehlende Types) scheiterte. Schlussendlich entschieden wir uns f\u00fcr Hocuspocus, da es nahtlos mit Yjs kompatibel ist und flexible WebHooks bietet. Hocuspocus \u00fcbernimmt dabei nur die Logik f\u00fcr die CRDT-Daten, w\u00e4hrend das TCP-Connection-Upgrade und das Netzwerk von Fastify \u00fcbernommen werden. Das erm\u00f6glicht uns, alle API-Endpoints (REST und WebSocket) unter einem Dach zu vereinen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Tech-Stack<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Frontend<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table><tbody><tr><td><strong>Technologie<\/strong><\/td><td><strong>Zweck<\/strong><\/td><td><strong>Begr\u00fcndung<\/strong><\/td><\/tr><tr><td><strong>React + Vite<\/strong><\/td><td>Frontend Framework<\/td><td>Standard f\u00fcr performante UIs; schnelles HMR (Hot Module Replacement).<\/td><\/tr><tr><td><strong>AG Grid CE<\/strong><\/td><td>Spreadsheet-UI<\/td><td>Hohe Feature-Dichte (Editing, Filter) bei geringem Eigenaufwand. Premade (spart Arbeit)<\/td><\/tr><tr><td><strong>Yjs<\/strong><\/td><td>CRDT-Engine<\/td><td>Erm\u00f6glicht real time kollaboratives Editieren dank automatischer Synchronisierung des Dokuments\/Konfliktbehebung durch den CRDT-Datentyp<\/td><\/tr><tr><td><strong>hocuspocus<br>provider<\/strong><\/td><td>Provider<\/td><td>WebSocket-Verbindung f\u00fcr Yjs<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Backend<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Technologie<\/strong><\/td><td><strong>Zweck<\/strong><\/td><td><strong>Begr\u00fcndung<\/strong><\/td><\/tr><tr><td><strong>Node.js + Fastify<\/strong><\/td><td>API-Server<\/td><td>Hoher Durchsatz bei WebSockets, TypeScript-nativ, validierte Schemas.<\/td><\/tr><tr><td><strong>hocuspocus Server<\/strong><\/td><td>Sync-Instanz<\/td><td>Schlankes Backend-Modul f\u00fcr Yjs.<\/td><\/tr><tr><td><strong>PostgreSQL<\/strong><\/td><td>Persistenz<\/td><td>Speichert User-Daten und Yjs-Doc-Snapshots. Lokal in Docker oder in RDS.<\/td><\/tr><tr><td><strong>AWS Cognito<\/strong><\/td><td>Auth Service<\/td><td>Stellt die JWT Token zur Authentifizierung bei Nutzung der API Endpoints zur Verf\u00fcgung<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">Cloud(AWS)<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Service<\/strong><\/td><td><strong>Zweck<\/strong><\/td><td><strong>Begr\u00fcndung<\/strong><\/td><\/tr><tr><td><strong>ECS Express<\/strong><\/td><td>Container Hosting<\/td><td><strong>Kernanwendung, <\/strong>automatisiert SSL und Deployment direkt aus ECR.<\/td><\/tr><tr><td><strong>RDS PostgreSQL<\/strong><\/td><td>Managed DB<\/td><td>Datenhaltung; Free Tier (db.t3.micro).<\/td><\/tr><tr><td><strong>S3<\/strong><\/td><td>SPA Hosting<\/td><td>Hosting der statischen Assets (Frontend)<\/td><\/tr><tr><td><strong>SSM Parameter Store &amp; Secret Manager<\/strong><\/td><td>Config &amp; Secrets<\/td><td>Verwaltung von DB-Credentials und API-Keys<\/td><\/tr><tr><td><strong>ECR<\/strong><\/td><td>Image Registry<\/td><td>Notwendig f\u00fcr Container-Deployments auf ECS Express<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<h3 class=\"wp-block-heading\">DevOps\/IaC<\/h3>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Technologie<\/strong><\/td><td><strong>Zweck<\/strong><\/td><td><strong>Begr\u00fcndung<\/strong><\/td><\/tr><tr><td><strong>AWS CDK (TS)<\/strong><\/td><td>Infrastructure as Code<\/td><td>Gesamte Infrastruktur als Code, reproduzierbar\/versionierbar und typensicher (Typescript).<\/td><\/tr><tr><td><strong>GitHub Actions<\/strong><\/td><td>CI\/CD<\/td><td>Automatisiert den Build-Prozess und den Push zu ECR\/ECS.<\/td><\/tr><tr><td><strong>Docker<\/strong><\/td><td>Containerisierung<\/td><td>lokal Container testen (Backend, Datenbank etc)<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Funktionsprinzipien und Echtzeitbearbeitung<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Frontend<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Die kollaborative Bearbeitung zerf\u00e4llt in zwei Fragen, die fast nichts miteinander zu tun haben: Wie kommen Tabelle und Dokument zusammen? Und wie kommt das Dokument zu den anderen und in die Datenbank? Wir haben das absichtlich als zwei getrennte Issues aufgebaut, und die Trennlinie war von Anfang an klar: Ein Hook liefert das Dokument inklusive Verbindungsstatus, die Tabellenkomponente nimmt ein <code class=\"\" data-line=\"\">doc <\/code>entgegen und sonst nichts.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Der Effekt zeigte sich beim Umbau. Solange das Backend noch fehlte, kam das Dokument aus einem lokalen <code class=\"\" data-line=\"\">new Y.Doc()<\/code> im Browser. Danach kam es aus einem HocuspocusProvider. In der Ansicht war das ein getauschter Aufruf:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\">\/\/ vorher: rein lokal, ohne Backend\nconst { doc } = useLocalSheetDoc(id)\n\n\/\/ nachher: synchronisiert\nconst { doc, status, readOnly } = useSheetDoc(id, apiUrl)\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Der Commit, der die Synchronisation eingef\u00fchrt hat, ist nat\u00fcrlich gr\u00f6\u00dfer als diese Zeile: Die Verbindungslogik ist komplett neu dazugekommen. Entscheidend ist, was nicht dabei steht. <code class=\"\" data-line=\"\">sheetGrid.tsx<\/code> taucht im Diff \u00fcberhaupt nicht auf. Die Tabelle selbst musste kein einziges Mal angepasst werden. <code class=\"\" data-line=\"\">useLocalSheetDoc <\/code>ist au\u00dferdem bewusst stehen geblieben: Falls das Backend vor einer Vorf\u00fchrung ausf\u00e4llt, f\u00fchrt der Weg zur\u00fcck zu einer funktionsf\u00e4higen, aber nicht geteilten Tabelle genau \u00fcber denselben gewechselten Aufruf. Im Frontend verteilt sich das Ganze auf mehreren Dateien, die je eine Aufgabe haben: <code class=\"\" data-line=\"\">sheetDoc.ts<\/code> definiert das Datenmodell und die einzigen Funktionen, die es ver\u00e4ndern. <code class=\"\" data-line=\"\">sheetGrid.tsx<\/code> ist die Tabelle und wei\u00df nicht, ob sie lokal l\u00e4uft oder an einem Server h\u00e4ngt. <code class=\"\" data-line=\"\">sheetConnection.ts<\/code> k\u00fcmmert sich um die Verbindung und deren Lebenszyklus. Und <code class=\"\" data-line=\"\">sheetView.tsx <\/code>sorgt daf\u00fcr, dass die drei Teile zusammenarbeiten. Sp\u00e4ter kam <code class=\"\" data-line=\"\">presence.ts<\/code> dazu f\u00fcr Anwesenheit und Cursor.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Ein Schreibweg, ein Leseweg<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Das Grundprinzip: Die Tabelle h\u00e4lt keine eigenen Daten. Sie stellt dar, was im Yjs-Dokument steht, und schreibt jede \u00c4nderung dorthin zur\u00fcck.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Konkret hei\u00dft das: Wenn jemand einen Wert eintippt, meldet AG Grid ihn an den <code class=\"\" data-line=\"\">valueSetter<\/code> der Spalte. Der schreibt ihn nicht in das Zeilenobjekt, sondern ausschlie\u00dflich ins Dokument. Yjs schlie\u00dft die Transaktion und benachrichtigt seine Beobachter, noch im selben Aufruf. Der Beobachter liest daraufhin alle Zeilen neu aus dem Dokument aus und meldet React, dass es neu rendern soll. Erst dar\u00fcber landet der Wert wieder in der Anzeige.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\">\/\/ frontend\/src\/components\/sheetGrid.tsx\nvalueSetter: (params: ValueSetterParams&lt;SheetRow&gt;) =&gt; {\n  if (readOnly || !params.data) return false;\n  return setCell(doc, params.data.id, column.key, String(params.newValue ?? &#039;&#039;));\n},\n\n\/\/ frontend\/src\/lib\/sheetDoc.ts\nexport function setCell(doc: Y.Doc, rowId: string, column: string, value: string): boolean {\n  const rows = getRows(doc);\n  const index = findRowIndex(rows, rowId);\n  if (index &lt; 0) return false;\n  const row = rows.get(index);\n  if ((row.get(column) ?? &#039;&#039;) === value) return false;\n  \/\/ \u2026\n  if (value === &#039;&#039;) row.delete(column);\n  else row.set(column, value);\n  return true;\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Der Umweg wirkt zun\u00e4chst \u00fcberfl\u00fcssig, ist aber der Kern der Sache: Eine eigene Eingabe nimmt damit denselben Weg wie eine fremde. Es gibt genau einen Schreibweg (<code class=\"\" data-line=\"\">valueSetter \u2192 setCell<\/code>) und genau einen Leseweg (Dokument \u2192 Beobachter \u2192 Anzeige). Zwei Wege in die Anzeige k\u00f6nnten auseinanderlaufen, einer nicht.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Das Datenmodell<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Der Hocuspocus-Server k\u00fcmmert sich nicht um die Dokumentstruktur: Beim Laden schiebt er ein Update durch und beim Speichern holt er sich eines heraus. Was drinsteht, entscheidet letztlich das Frontend. Unsere Wahl lautet:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\">&#091;\n  Y.Map { id: &quot;a1b2\u2026&quot;, c0: &quot;Umsatz&quot;, c2: &quot;1200&quot; },\n  Y.Map { id: &quot;c3d4\u2026&quot;, c0: &quot;Kosten&quot;, c2: &quot;800&quot;  },\n]\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Die <code class=\"\" data-line=\"\">Y.Array<\/code> tr\u00e4gt die Reihenfolge der Zeilen, jede Zeile ist eine <code class=\"\" data-line=\"\">Y.Map<\/code> von Spaltenschl\u00fcssel auf Zellinhalt. Zehn feste Spalten, ein neues Sheet startet standardm\u00e4\u00dfig mit 25 Zeilen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Der Vorteil liegt im Zusammenf\u00fchren. Weil jede Zelle ein eigener Eintrag in der Map ist, ber\u00fchren sich gleichzeitige \u00c4nderungen an verschiedenen Zellen gar nicht. Was Yjs dabei garantiert, von uns mit zwei Tabs gegengepr\u00fcft:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><tbody><tr><td><strong>Gleichzeitig passiert<\/strong><\/td><td><strong>Ergebnis nach dem Abgleich<\/strong><\/td><\/tr><tr><td>A schreibt c0, B schreibt c1 derselben Zeile<\/td><td>Beide Werte stehen da<\/td><\/tr><tr><td>A und B schreiben dieselbe Zelle<\/td><td>Einer gewinnt, aber beide Seiten sehen denselben<\/td><\/tr><tr><td>A h\u00e4ngt eine Zeile an, B l\u00f6scht eine andere<\/td><td>Beides gilt, Reihenfolge stimmt \u00fcberein<\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><a><\/a>Nur bei derselben Zelle muss \u00fcberhaupt entschieden werden, und dort gilt der letzte Schreiber. F\u00fcr eine Tabelle ist das die richtige Regel: Eine Zelle wird als Ganzes best\u00e4tigt, nicht Zeichen f\u00fcr Zeichen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Zwei Details in diesem Modell haben uns \u00c4rger erspart. Erstens arbeiten die Zellinhalte hier als Strings, nicht als <code class=\"\" data-line=\"\">Y.Text<\/code>. Mit <code class=\"\" data-line=\"\">Y.Text<\/code> k\u00f6nnte man innerhalb einer Zelle Zeichen f\u00fcr Zeichen zusammenf\u00fchren, das braucht aber einen Editor pro Zelle. Diesen gibt es hier nicht, also bleibt der String die zuverl\u00e4ssige Wahl.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Zweitens hat jede Zeile eine eigene <code class=\"\" data-line=\"\">id<\/code>. AG Grid kennt Zeilen \u00fcber <code class=\"\" data-line=\"\">getRowId<\/code> und aktualisiert bei \u00c4nderungen nur die betroffene Zeile, statt die ganze Tabelle neu zu zeichnen. Ohne diese Optimierung w\u00fcrden fremde Eingaben die gerade ge\u00f6ffnete Zelle schlie\u00dfen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Beim Testen kam au\u00dferdem noch ein dritter Punkt dazu: Die Zeilennummer steckt ebenfalls in den Zeilendaten und wird nicht aus dem Grid-Index abgeleitet. Das passt zur gleichen Optimierung wie oben. AG Grid aktualisiert eine Zeile nur, wenn sich deren Daten \u00e4ndern. Nach dem L\u00f6schen rutschen die Zeilen darunter nach oben, ihre Daten bleiben aber gleich, und eine aus dem Index abgeleitete Nummer bliebe sichtbar stehen.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Yjs trifft React<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Yjs spricht kein React. \u00c4nderungen melden sich \u00fcber R\u00fcckruffunktionen. React braucht dagegen einen Wert, der sich beim Rendern nicht ver\u00e4ndert. Die Br\u00fccke dazu hei\u00dft <code class=\"\" data-line=\"\">useSyncExternalStore<\/code>.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\">\/\/ frontend\/src\/lib\/sheetDoc.ts\nreturn {\n  subscribe: (onStoreChange) =&gt; {\n    if (listeners.size === 0) {\n      rows.observeDeep(handleChange);\n      \/\/ \u2026\n      snapshot = readRows(rows);\n    }\n    listeners.add(onStoreChange);\n    \/\/ \u2026\n  },\n  getSnapshot: () =&gt; snapshot,\n};\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Beim ersten Rendern legt sich ein kleiner Speicher an, der den Dokumentinhalt als Array einfacher Objekte h\u00e4lt. \u00c4ndert sich das Dokument, liest der Beobachter alle Zeilen neu aus und meldet React, dass es neu rendern soll. Zwischen zwei \u00c4nderungen gibt der Speicher immer dasselbe Array zur\u00fcck, was React genau so verlangt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Der naheliegendere Weg w\u00e4re gewesen, im Effekt zu abonnieren und den Anfangszustand per <code class=\"\" data-line=\"\">setState<\/code> nachzuziehen. Genau das verbietet bei uns die Regel <code class=\"\" data-line=\"\">react-hooks\/set-state-in-effect<\/code> aus <code class=\"\" data-line=\"\">eslint-plugin-react-hooks<\/code>: synchrones <code class=\"\" data-line=\"\">setState<\/code> in einem Effekt. Der Speicher umgeht das Problem, statt die Regel zu unterdr\u00fccken. Eine kleine Feinheit steckt im Abonnement: Zwischen dem Anlegen des Speichers beim Rendern und dem Abonnieren im Effekt kann schon eine \u00c4nderung dazwischenkommen, oft der erste Serverzustand. Deshalb liest der Speicher beim Abonnieren nochmals frisch. Ohne das bliebe die Tabelle leer, bis zuf\u00e4llig die n\u00e4chste \u00c4nderung kommt.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">So funktioniert gemeinsames Bearbeiten<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Die Tabelle enth\u00e4lt keine eigenen Daten. Sie stellt dar, was im Yjs-Dokument steht, und schreibt jede \u00c4nderung dorthin zur\u00fcck. Ein Yjs-Dokument ist ein CRDT: eine Datenstruktur, die an mehreren Stellen gleichzeitig ver\u00e4ndert werden kann und am Ende \u00fcberall denselben Zustand ergibt, ohne Sperren und ohne Server, der Konflikte aufl\u00f6st.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Daraus ergibt sich im Frontend genau ein Schreibweg (Zelleingabe \u2192 Dokument) und genau ein Leseweg (Dokument \u2192 AG Grid). Auch die eigene Eingabe geht \u00fcber das Dokument und nicht direkt in die Anzeige, so bleiben lokale und fremde \u00c4nderungen synchron und laufen nicht auseinander.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Die Verbindung k\u00fcmmert sich dann um den Rest: Ein Provider verschickt jede \u00c4nderung als Bin\u00e4rpaket \u00fcber WebSocket zum Hocuspocus-Server, der sie an alle anderen Clients verteilt. Das Dokument schreibt er zeitversetzt in eine Postgres-Spalte (<code class=\"\" data-line=\"\">bytea<\/code>). Authentifiziert wird mit einem Cognito-Token, das im Hocuspocus-Protokoll mitl\u00e4uft, weil ein Browser bei WebSockets keine eigenen Header senden kann. Mit einem einfachen Skript l\u00e4sst sich messen, dass eine \u00c4nderung schon nach etwa 1,8 Millisekunden im anderen Tab ankommt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Bei gleichzeitigen \u00c4nderungen an derselben Zelle gewinnt deterministisch dieselbe Version auf beiden Seiten, entschieden wird das anhand der Client-ID. \u00c4nderungen an unterschiedlichen Zellen beeinflussen sich nicht gegenseitig, weil jede Zelle ihr eigenes Dokumentelement ist. F\u00fcr eine Tabelle bedeutet das: Eine Zelle wird als Ganzes best\u00e4tigt, nicht Zeichen f\u00fcr Zeichen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00e4llt die Verbindung aus, zeigt sich \u201eGetrennt\u201c, ein Banner listet die noch nicht \u00fcbertragenen \u00c4nderungen auf, und die Bearbeitung l\u00e4uft lokal weiter. Die Wiederverbindung erfolgt dann automatisch. Die Provider-Einstellungen haben wir auch angepasst: Standardm\u00e4\u00dfig verdoppelt sich die Wartezeit auf bis zu 30 Sekunden, was im Worst Case eine halbe Minute Wartezeit vor einer scheinbar toten Tabelle bedeutet. Bei uns passt sich der Abstand um den Faktor 1,5 an und liegt maximal bei zehn Sekunden. Der vorhandene Jitter des Providers hilft zus\u00e4tzlich, dass nach einem Backend-Neustart nicht alle Clients gleichzeitig anfragen.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\">\/\/ frontend\/src\/lib\/sheetConnection.ts\nconst RECONNECT = {\n  delay: 1000,\n  factor: 1.5,\n  maxDelay: 10000,\n  jitter: true,\n  minDelay: 500,\n  \/\/ \u2026\n  messageReconnectTimeout: 20000,\n} as const;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Die Verbindung geh\u00f6rt dabei nicht zur Ansicht, sondern wird pro Sheet geteilt und bleibt nach dem Verlassen noch f\u00fcnf Sekunden offen. Ohne diese Schonfrist w\u00e4re sie im React StrictMode kaum tragbar. Im Entwicklungsmodus h\u00e4ngt React jede Komponente einmal zus\u00e4tzlich an und wieder ab, der frisch ge\u00f6ffnete Socket w\u00fcrde sofort wieder geschlossen. Praktischer Nebeneffekt: Der Weg \u00dcbersicht \u2192 Sheet \u2192 \u00dcbersicht wird nicht jedes Mal neu aufgerufen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Haben beide Seiten offline verschiedene Zellen bearbeitet, tauchen am Ende beide \u00c4nderungen auf, darum k\u00fcmmert sich das CRDT und nicht der Server. Cursor und Anwesenheit der anderen laufen absichtlich nicht durchs Dokument, sondern \u00fcber Yjs\u2019 Awareness-Protokoll: Cursor sind fl\u00fcchtig und landen nicht in der Datenbank. Nach einem Absturz w\u00e4re die Markierung sonst permanent da. Warum Yjs statt Operational Transformation? OT br\u00e4uchte einen zentralen Transformationspunkt. Ein CRDT macht den Server bewusst simpel, dieser verteilt und speichert nur und die Offline-F\u00e4higkeit f\u00e4llt als Nebeneffekt auch einfach weg.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Challenges<\/h2>\n\n\n\n<h3 class=\"wp-block-heading\">Das Datenbankpasswort stand im Klartext<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Unsere RDS-Instanz generiert das Passwort selbst und speichert es im AWS Secrets Manager. Um es in den Container zu bekommen, haben wir daraus eine Verbindungs-URL gebaut. Der Aufruf-Name war im Nachhinein eine Warnung: <code class=\"\" data-line=\"\">unsafeUnwrap()<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Im synthetisierten CloudFormation-Template wirkte das harmlos. Dort stand lediglich eine Referenz wie <code class=\"\" data-line=\"\">{{resolve:secretsmanager:\u2026}}<\/code>, kein Passwort. Beim Deployment wird diese Referenz aber aufgel\u00f6st und das Ergebnis landet als normale Umgebungsvariable in der ECS-Service-Konfiguration. Jeder mit der Berechtigung <code class=\"\" data-line=\"\">ecs:DescribeServices<\/code> h\u00e4tte sie dort lesen k\u00f6nnen. Die Pipeline hat das Problem bis dahin dadurch gel\u00f6st, dass das Verzeichnis <code class=\"\" data-line=\"\">cdk.out<\/code> gar nicht erst als Artefakt hochgeladen wurde. Das war eher eine Symptombehandlung als eine echte L\u00f6sung.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Die L\u00f6sung: Benutzername und Passwort liegen jetzt im SSM Parameter Store, das Passwort als <code class=\"\" data-line=\"\">SecureString<\/code>. Der ECS-Agent holt beides beim Start des Tasks ab und leitet es ausschlie\u00dflich an den Container weiter. In der Service-Konfiguration sieht man danach nur noch den Parameter-ARN:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\">\/\/ infra\/hello-cdk\/lib\/ecs-express-stack.ts\nconst dbUser = config.database.secret?.secretValueFromJson(&#039;username&#039;).unsafeUnwrap() ?? &#039;cloudsheet&#039;;\nconst dbPass = config.database.secret?.secretValueFromJson(&#039;password&#039;).unsafeUnwrap() ?? &#039;&#039;;\nconst databaseUrl = `postgresql:\/\/${dbUser}:${dbPass}@${dbHost}:${dbPort}\/${dbName}`;\n\/\/ \u2026\nenvironment: &#091;\n  \/\/ \u2026\n  { name: &#039;DATABASE_URL&#039;, value: databaseUrl },\n\n\n\n\n\/\/ infra\/hello-cdk\/lib\/ecs-express-stack.ts\nsecrets: &#091;\n  {\n    name: &#039;DB_USERNAME&#039;,\n    valueFrom: dbParameterArn(this, config.dbCredentials.usernameParameterName),\n  },\n  \/\/ \u2026 DB_PASSWORD identisch, \u00fcber passwordParameterName\n],\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Dabei gibt es eine Einschr\u00e4nkung, die AWS zwar dokumentiert, aber nicht durchsetzt: CloudFormation kann keine SecureString-Parameter anlegen. <code class=\"\" data-line=\"\">AWS::SSM::Parameter<\/code> unterst\u00fctzt nur String und StringList. Im CDK wird der Typ zwar angeboten, das Template entsteht sauber, doch der Deploy scheitert erst zur Laufzeit. Es wurde durch eine kleine Custom Resource gel\u00f6st: eine Lambda, die nur den ARN des Secrets erh\u00e4lt, den Wert dann ausliest und per <em><code class=\"\" data-line=\"\">ssm:PutParameter<\/code><\/em> speichert. So landet zu keinem Zeitpunkt ein Klartextwert im CloudFormation-Template.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\">\/\/ infra\/hello-cdk\/lib\/db-credentials-parameter.ts\nnew CustomResource(this, &#039;Resource&#039;, {\n  serviceToken: provider.serviceToken,\n  resourceType: &#039;Custom::DbCredentialsToSsm&#039;,\n  properties: {\n    secretArn: props.secret.secretArn,\n    \/\/ \u2026\n  },\n});\n\/\/ \u2026\nactions: &#091;&#039;ssm:GetParameters&#039;, &#039;ssm:GetParameter&#039;],\nresources: this.parameterArns,\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Auf dem Backend wird die Verbindung aus den einzelnen <code class=\"\" data-line=\"\">DB_*<\/code>-Variablen zusammengesetzt, das ist bewusst kein Umweg \u00fcber eine URL. RDS erzeugt Passw\u00f6rter mit Zeichen wie <code class=\"\" data-line=\"\">@<\/code>, <code class=\"\" data-line=\"\">:<\/code> oder <code class=\"\" data-line=\"\">\/<\/code>, und genau das zerlegt einen <code class=\"\" data-line=\"\">postgresql:\/\/<\/code>-String an einer ung\u00fcnstigen Stelle. Fehlt eine Variable, gibt die Fehlermeldung sie beim Namen an und das Passwort bleibt unver\u00f6ffentlicht.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Zwei Punkte bleiben absichtlich offen: Secrets Manager bleibt f\u00fcr RDS die zentrale Quelle der Wahrheit. Der Parameter Store dient als Auslieferungsweg, nicht als einziger Speicher. Zugangsdaten existieren also an zwei Orten und eine Passwort-Rotation au\u00dferhalb des Stacks w\u00fcrde die Kopie veralten lassen. Der spannendste Nebeneffekt kam beim ersten echten Durchlauf: Vier Fehler, die seit Wochen im Repo schlummerten, wurden sichtbar. Die knex-CLI konnte keine TypeScript-Migrationen laden, dadurch w\u00e4re der Container nie gestartet. Der RDS-Instanz fehlte der Parameter <code class=\"\" data-line=\"\">databaseName<\/code>, die Datenbank h\u00e4tte deswegen schlicht nicht existiert. Dazu zwei Testprobleme, die die CI beim ersten Lauf gegen eine frische Datenbank rot gemacht h\u00e4tten. Solche Fehler lie\u00dfen sich nicht durch reines Lesen entdecken, sichtbar wurden sie erst, als die komplette CI-Sequenz lokal gegen eine leere Datenbank durchgelaufen ist.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Drei Variablen, die auseinanderlaufen konnten<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Unser Deployment-Workflow hatte drei Einstellm\u00f6glichkeiten: Zielumgebung, Name des ECR-Repositorys und Name des Stacks, der dieses Repository erstellt. Dar\u00fcber stand ein Kommentar, der die folgende Person bat, alle drei gemeinsam zu \u00e4ndern.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ein Kommentar ist keine Absicherung. Wenn eine der drei Zeilen angepasst wird und die anderen vergessen, landet man bei Dev-Infrastruktur und das Image wandert gleichzeitig in die Prod-Registry und in der Pipeline bemerkt es keiner.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Darum haben wir die Zuordnung in eine JSON-Datei verschoben, die von beiden Seiten gelesen wird: von der CDK-App und vom Workflow. Wir haben bewusst JSON statt TypeScript gew\u00e4hlt, sonst brauchte die Pipeline eine eigene Kopie, und genau diese Dopplung hat das Problem erst verursacht.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\">infra\/hello-cdk\/lib\/environments.json \n\n{\n  &quot;dev&quot;: {\n    &quot;ecrStack&quot;: &quot;EcrDevStack&quot;,\n    &quot;ecrRepository&quot;: &quot;cloudsheets-backend-dev&quot;,\n    &quot;stackSuffix&quot;: &quot;&quot;\n  },\n  &quot;prod&quot;: {\n    &quot;ecrStack&quot;: &quot;EcrStack&quot;,\n    &quot;ecrRepository&quot;: &quot;cloudsheets-backend&quot;,\n    &quot;stackSuffix&quot;: &quot;-prod&quot;\n  }\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Eine Kleinigkeit darin ist Absicht und kein Versehen: dev bleibt ohne Suffix, prod erh\u00e4lt -prod. Symmetrisch umbenennen klingt zwar h\u00fcbsch, funktioniert aber nicht, weil CloudFormation keine Ressourcen beim Umbenennen mitnimmt. Es g\u00e4be sonst zwei S\u00e4tze an Stacks, und alte RDS-, Bucket- und Distribution-Dienste w\u00fcrden zur\u00fcckbleiben, dazu k\u00e4men weitere Kosten.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Beim Umbau tauchten zwei weitere Fehler derselben Familie auf. Erstens waren immer beide ECR-Stacks Teil der App: Ein <code class=\"\" data-line=\"\">cdk deploy --all<\/code> f\u00fcr Dev legte nebenbei auch die Prod-Registry an. Zweitens schlich sich bei der App ein Tippfehler im Umgebungsnamen ein. <code class=\"\" data-line=\"\">appConfig[&#039;prd&#039;]<\/code> ist undefined und ab diesem Punkt f\u00fchrte jede Pr\u00fcfung der Form <code class=\"\" data-line=\"\">=== &#039;dev&#039;<\/code> automatisch zum Prod-Zweig. Ein vermeintlicher Dev-Stack h\u00e4tte \u00fcberall RETAIN-Richtlinien bekommen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Absicherung l\u00e4uft jetzt vierfach und in der Reihenfolge, in der die Pr\u00fcfungen greifen: Ein Ableitungsskript lehnt unbekannte Umgebungen ab und schl\u00e4gt Alarm, sobald alte Variablen wieder eingetragen werden. Jest-Tests pr\u00fcfen die Zuordnung, der ungew\u00f6hnlichste davon \u00f6ffnet die Workflow-Datei selbst und stellt sicher, dass dort nie wieder ein fest verdrahteter Name auftaucht. Ein YAML-Leser-Test wirkte eigenartig, doch genau dort steckte der Fehler. Ein CI-Job erzeugt bei jedem Pull Request beide Umgebungen und vergleicht die entstehenden Templates, kein Stack, kein Repository und kein Bucket der jeweils anderen Umgebung darf darin vorkommen. Und die CDK-App wirft bei unbekanntem Umgebungsnamen eine Exception, statt einfach weiterzulaufen. Seitdem gilt au\u00dferdem: Deployment ist eine Entscheidung, kein Nebeneffekt. Anfangs landeten Pushes direkt nach AWS. Jetzt h\u00e4ngt der Deploy-Job an einem GitHub-Environment mit manueller Freigabe, dadurch bleibt der Lauf stehen, bis jemand ihn gezielt freigibt. Das kostet einen Klick und schafft die Gelegenheit, vor dem Deploy die IAM-Berechtigungen in Ruhe zu pr\u00fcfen.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Ein gr\u00fcner Deploy, der nichts ausliefert<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Der erste vollst\u00e4ndige Deploy lief durch. Vier Stacks, 69 Ressourcen, alle im Status CREATE_COMPLETE. Dann lief der Smoke-Test der Pipeline, der nach dem Deploy den Endpunkt <code class=\"\" data-line=\"\">\/health<\/code> abfragt und zwanzig Mal schlug er mit Timeout fehl.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Der erste Impuls war, dass es am Container liegt. Die ECS-Konsole sagte etwas anderes: ein laufender Task im Status ACTIVE, \u201ehas reached a steady state\u201c, ein registriertes Target in der Target Group. Ein registriertes Target bedeutet, dass der Load Balancer den Health Check des Containers erfolgreich durchf\u00fchrt. Der Container beantwortete also genau die Anfrage, die von au\u00dfen im Timeout endete.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Wichtiger als die Fehlerursache war die Art des Problems. Ein defekter Container liefert \u00fcbliche HTTP-Fehlercodes wie 502 oder 503. Ein Timeout bedeutet dagegen: Es kam nicht einmal eine TCP-Verbindung zustande. Das Problem lag also im Netzwerk, vor dem Container. Und eine einzige DNS-Abfrage gen\u00fcgte, um es zu kl\u00e4ren.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\">$ nslookup cl-300fb2d34ac94b11a6e5221442d05494.ecs.eu-central-1.on.aws Addresses: \t10.0.245.77 \n10.0.172.123 \n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Private Adressen. Der Load Balancer blieb also intern, vom Internet aus war er dadurch grunds\u00e4tzlich nicht erreichbar.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Warum das passiert ist: Der ECS Express Modus nimmt einem viel Arbeit ab. Man \u00fcbergibt Subnetze und ein Container-Image, den Load Balancer samt Target Group und Security Group baut AWS selbst. Was der Ressourcentyp <code class=\"\" data-line=\"\">AWS::ECS::ExpressGatewayService<\/code> aber nicht anbietet, ist eine Eigenschaft f\u00fcr \u201e\u00f6ffentlich erreichbar\u201c. Diese Entscheidung h\u00e4ngt allein von den \u00fcbergebenen Subnetzen ab. Wir hatten private Subnetze gew\u00e4hlt, aus dem nachvollziehbaren Reflex, dass private Subnetze die sicherere Wahl sind. F\u00fcr die Tasks trifft das auch zu. Nur wird in denselben Subnetzen eben auch die Eingangst\u00fcr gebaut, und die stand damit in einem Bereich, den von au\u00dfen niemand erreicht.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Die Korrektur war eine Zeile: <code class=\"\" data-line=\"\">privateSubnets \u2192 publicSubnets.<\/code> Wichtiger war der Regressionstest, den wir dazugeschrieben haben. Er pr\u00fcft, dass das Gateway in den \u00f6ffentlichen Subnetzen liegt, und begr\u00fcndet im Kommentar, warum: Beim n\u00e4chsten Lesen sieht \u201eprivat&#8221; garantiert wieder wie die sichere Wahl aus.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\">\/\/ infra\/hello-cdk\/test\/ecs-express.test.ts\ntest(&#039;places the gateway in public subnets so it is reachable from outside the VPC&#039;, () =&gt; {\n  \/\/ \u2026\n  const subnets: Array&lt;{ &#039;Fn::ImportValue&#039;: string }&gt; =\n    service.Properties.NetworkConfiguration.Subnets;\n  expect(subnets).toHaveLength(2);\n  for (const subnet of subnets) {\n    expect(subnet&#091;&#039;Fn::ImportValue&#039;]).toContain(&#039;PublicSubnet&#039;);\n  }\n});\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\">Eine Verbindung, die React StrictMode \u00fcberlebt<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Die Verbindung geh\u00f6rt nicht zur Ansicht, sondern zu einem Verzeichnis, das anhand der Verbindungs-URL unterschieden wird. Die Ansicht meldet beim Einh\u00e4ngen lediglich, dass die Verbindung genutzt wird, und beim Aush\u00e4ngen wieder ab. F\u00e4llt der Z\u00e4hler auf null, gibt es eine f\u00fcnfsek\u00fcndige Schonfrist, bevor der Socket geschlossen wird.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\">\/\/ frontend\/src\/lib\/sheetConnection.ts\nfunction armRelease(connection: Connection, url: string): void {\n  connection.releaseTimer = window.setTimeout(() =&gt; {\n    connection.releaseTimer = undefined;\n    if (connection.refs &gt; 0) return;\n    connection.dispose();\n    connections.delete(url);\n  }, RELEASE_DELAY_MS);\n}\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Diese Schonfrist ist nicht optional. Im Entwicklungsmodus von React h\u00e4ngt StrictMode jede Komponente absichtlich einmal zus\u00e4tzlich aus und wieder ein. Ohne sie w\u00fcrde der Socket bei jedem Seitenaufruf sofort wieder geschlossen. Genau dort w\u00e4re die Anwendung kaputt, wo man gerade entwickelt und demonstriert. Als Nebeneffekt verbindet der kurze Weg \u00dcbersicht \u2192 Sheet \u2192 \u00dcbersicht nicht jedes Mal neu.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Wenn die Verbindung wegbricht<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Der Provider verbindet von sich aus neu, nur zu langsam: Voreingestellt verdoppelt sich die Wartezeit bis auf 30 Sekunden. So lange starrt man schlimmstenfalls auf eine tote Tabelle. Bei uns w\u00e4chst der Abstand deshalb um den Faktor 1,5 und ist bei zehn Sekunden gedeckelt.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\">\/\/ frontend\/src\/lib\/sheetConnection.ts\nconst RECONNECT = {\n  delay: 1000,\n  factor: 1.5,\n  maxDelay: 10000,\n  jitter: true,\n  minDelay: 500,\n  messageReconnectTimeout: 20000,\n} as const;\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Der Jitter, den der Provider ohnehin mitbringt, sorgt daf\u00fcr, dass nach einem Backend-Neustart nicht alle Clients gleichzeitig anklopfen. Solange die Verbindung weg ist, sieht man neben dem Titel \u201eGetrennt\u201c auch die Anzahl der noch nicht \u00fcbertragenen \u00c4nderungen, ohne diese Zahl w\u00e4re der Hinweis \u201ewird \u00fcbertragen, sobald die Verbindung steht\u201c eine Behauptung, die niemand nachpr\u00fcfen k\u00f6nnte. Der rohe Zustand des Providers taugt als Anzeige nichts: Er meldet jeden Wiederverbindungsversuch, und das Abzeichen zappelt ungefiltert im Sekundentakt. Deshalb wird er gegl\u00e4ttet, mit etwa 1,2 Sekunden Karenz vor \u201eGetrennt\u201c.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Yjs-WebSocket in Fastify einbetten<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Als elementarer Baustein f\u00fcr die Kernfunktion unserer Anwendung kommt wie bereits beschrieben ein WebSocket zum Einsatz. Da wir f\u00fcr die Synchronisierung CRDTs mit Yjs verwenden, bot es sich an, den Websocket im Backend mithilfe von y-websocket zu implementieren, da dies vom gleichen Entwickler wie Yjs ist und damit volle Kompatibilit\u00e4t garantiert.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Jedoch musste ich bei der Implementierung schnell feststellen, dass man <code class=\"\" data-line=\"\">y-websocket<\/code> Server und deren Connection Handler nicht ohne weiteres mit Fastify integrieren kann.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Die <code class=\"\" data-line=\"\">setupWSConnection<\/code> Funktion von <code class=\"\" data-line=\"\">y-websocket<\/code> Server, welche f\u00fcr den Aufbau einer WebSocket-Verbindung zust\u00e4ndig ist, wird nicht als offizieller Export deklariert und kann deshalb nicht direkt im Fastify-Backend importiert und genutzt werden. Ein klassischer Import wie <code class=\"\" data-line=\"\">import { setupWSConnection } from &#039;y-websocket&#039;;<\/code> &nbsp;ist also nicht m\u00f6glich.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Auch ein direkter Import \u00fcber interne Dateien wie <code class=\"\" data-line=\"\">utils.js (import { setupWSConnection } from &#039;y-websocket\/bin\/utils.js&#039;;<\/code> wird von Node.js blockiert, da dies bei ESM-Modulen nicht erlaubt ist wenn nicht direkt in package.json vorgesehen. Deshalb f\u00fchrt dies zum Error: <code class=\"\" data-line=\"\">Error [ERR_PACKAGE_PATH_NOT_EXPORTED]: Package subpath &#039;.\/bin\/utils.js&#039; is not defined by &quot;exports&quot;...<\/code><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Dazu kommt noch, dass y-websocket keine Types deklariert und damit nicht besonders gut mit TypeScript kompatibel ist.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ein L\u00f6sungsansatz w\u00e4re y-websocket als komplett separaten Prozess\/Container auszulagern und damit Import Konflikte zu vermeiden. Dies bedeutet jedoch zus\u00e4tzlichen Deployment und Monitoring-Aufwand, weshalb es besser war nach einer Alternative zu suchen, welche sich mit Fastify und Typescript zu einem eleganten singul\u00e4ren Backend kombinieren l\u00e4sst.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Diese Bedingungen werden durch Hocuspocus erf\u00fcllt (und der Name ist lustig).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Leider stellte sich heraus, dass sich auch Hocuspocus nicht ohne weiteres mit dem Fastify Backend verschmelzen lassen will. Standardm\u00e4\u00dfig nutzt Hocuspocus seinen eigenen Server Wrapper und damit ein eigenes Netzwerk. Dieser Server Wrapper macht die handleConnection Funktion, welche f\u00fcr die eigentliche WebSocket Verbindung zust\u00e4ndig ist nicht zug\u00e4nglich.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Zum Gl\u00fcck funktioniert hier jedoch der direkte Import des Hocuspocus Core, welches die handleConnection Funktion enth\u00e4lt, <code class=\"\" data-line=\"\">mit import { Hocuspocus } from &#039;@hocuspocus\/server .<\/code><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Damit l\u00e4sst sich ein Fastify REST Endpoint implementieren, welcher ein Upgrade auf eine WebSocket-Verbindung mit Hocuspocus durchf\u00fchrt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Ablauf:<\/p>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li>Client ruft Sheet Editor auf und fr\u00e4gt damit WebSocket Upgrade per <code class=\"\" data-line=\"\">\/sheets\/:id\/sync<\/code> REST-Endpoint an. Fastify pr\u00fcft die Header und Autorisierung.<\/li>\n\n\n\n<li>Fastify erstellt dann einen WebSocket zwischen dem Client und dem Backend in der AWS Cloud.<\/li>\n\n\n\n<li>Sobald der Socket besteht \u00fcbergibt Fastify ihn an Hocuspocus mit der <code class=\"\" data-line=\"\">handleConnection<\/code> Funktion.<\/li>\n\n\n\n<li>Hocuspocus pr\u00fcft noch einmal die Identit\u00e4t (<code class=\"\" data-line=\"\">onAuthenticate<\/code> Hook) und Rolle des Clients und l\u00e4dt das angefragte Sheet (<code class=\"\" data-line=\"\">onLoadDocument <\/code>Hook) und k\u00fcmmert sich um Synchronisation \u00fcber mehrere Clients und das Persistieren von \u00c4nderungen (<code class=\"\" data-line=\"\">onStoreDocument<\/code> Hook).<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Als \u00dcbergabewert erwartet <code class=\"\" data-line=\"\">handleConnection <\/code>ein <code class=\"\" data-line=\"\">Standard Request Object (Fetch-Style)<\/code> von Fastify welches nicht mit dem Fastify eigenen Request-Format \u00fcbereinstimmt. Es muss also zun\u00e4chst in das richtige Format gebracht werden:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\">\/\/in routes\/sheets-ws.ts:\n      \/\/ create standard request object (Fetch-style) from fastify request object\n      const protocol = request.headers&#091;&#039;x-forwarded-proto&#039;] === &#039;https&#039; ? &#039;https&#039; : &#039;http&#039;;\n      const webRequest = new Request(`${protocol}:\/\/${request.headers.host}${request.url}`, {\n        headers: new Headers(request.headers as Record&lt;string, string&gt;), \/\/key value format\n        method: request.method,\n      });\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Damit sind alle API Endpoints und das gesamte Backend mit Fastify integriert.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Identit\u00e4ts-L\u00fccke im Sharing-Endpoint (Cognito vs. User-Table)<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Eine weitere Herausforderung, die es in der Entwicklung des Backends zu bew\u00e4ltigen durfte, war der Invite\/Share Flow, bei welchem User andere mithilfe von Rollen Zugriff erteilen, gemeinsam an einer Tabelle zu arbeiten.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Da das Einladen\/Rechte erteilen \u00fcber die E-Mail Adresse des Users, dem man Zugriff erteilen m\u00f6chte, funktionieren soll, muss man im Backend auf diese E-Mail Adresse zugreifen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Aufgrund unserer Entscheidung, die Registrierung von Usern \u00fcber AWS Cognito abzuwickeln, m\u00fcsste das Backend also bei jedem Einladevorgang eine Anfrage an Cognito mithilfe der <code class=\"\" data-line=\"\">ListUsers API<\/code> stellen, was wegen R\u00fcckgabe aller User langsam ist, API Kosten verursacht und saubere relationale Fremdschl\u00fcssel-Beziehungen in der Datenbank erschwert.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Das Access-Token, welches das Backend von Cognito zur Authentifizierung des Clients und der Anfrage genutzt wird, enth\u00e4lt leider ebenfalls nicht die E-Mail Adresse in der Payload.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Man kann das Access-Token zwar mithilfe einer Lambda-Funktion customizen, um die E-Mail Adresse hinzuzuf\u00fcgen, jedoch ist dies sehr komplex und erzeugt Latenz.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Die von uns schlussendlich gew\u00e4hlte L\u00f6sung ist eine zus\u00e4tzliche User-Tabelle in PostgreSQL, in welcher User zus\u00e4tzlich nach Registrierung gespeichert werden.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Dies ist direkt in der <code class=\"\" data-line=\"\">verifyUser()<\/code> Funktion implementiert, welche sowieso bei jeder Anfrage an einen Endpoint genutzt wird, um die Authentizit\u00e4t mithilfe des JWT-Tokens von Cognito zu pr\u00fcfen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Wenn der User, der die Anfrage stellt, noch nicht in der User-Tabelle enthalten ist, werden die Infos wie E-Mail Adresse von Cognito mithilfe des <code class=\"\" data-line=\"\">\/userInfo<\/code> Endpoints angefragt und die User Tabelle mit dem User und seiner E-Mail Adresse aktualisiert.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Da dies nur einmal pro User gemacht wird, ist es wesentlich schneller und g\u00fcnstiger, als jedes mal alle User mit der <code class=\"\" data-line=\"\">ListUsers API <\/code>anzufordern. Man spart sich also Anfragen an die User-Datenhaltung von Cognito.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\">\/\/in utils\/verifyUser.ts:\nconst user_exists = await db(&#039;users&#039;).where({ id: payload.sub }).first();\n        if(!user_exists) {\n            \/\/first authentication of this user: add user to DB user table with fetched email adress\n            let email = payload.email\n            if(!email) {\n                let response = await fetch(`${cognitoDomain}\/oauth2\/userInfo`, {\n                    method: &#039;GET&#039;,\n                    headers: {\n                        &quot;Authorization&quot;: `Bearer ${token}`,\n                    }\n                } );\n                let additional_userdata = await response.json();\n                email = additional_userdata.email\n            }\n            await db(&#039;users&#039;).insert({\n            id: payload.sub,\n            email: email\n            });\n        }\n\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00fcr die Anfrage an den \/userInfo Cognito Endpoint ben\u00f6tigt das Backend die exakte URL der Cognito-Domain, welche jedoch im separaten CDK-Stack erzeugt wird.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Dieses Cross-Stack-Problem haben wir folgenderma\u00dfen gel\u00f6st:<\/p>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li>Export im Quell-Stack: Die relevanten Werte werden im AuthStack als f\u00fcr andere Stacks auslesbar exportiert:<\/li>\n<\/ol>\n\n\n\n<pre class=\"wp-block-code\"><code class=\"\" data-line=\"\">\/\/ frontend-stack.ts\npublic readonly bucket: IBucket;\npublic readonly distribution: cloudfront.Distribution;\n\n\/\/ auth-stack.ts\npublic readonly userPoolId: string;\npublic readonly userPoolClientId: string;\npublic readonly cognitoDomain: string;\n\n\/\/ ecs-express-stack.ts\npublic readonly endpoint: string;<\/code><\/pre>\n\n\n\n<ol start=\"2\" class=\"wp-block-list\">\n<li>\u00dcbergabe der Werte: Bei der Instanziierung von EcsExpressStack (Backend) werden die Werte als Umgebungsvariablen direkt in die Container-Definition des ECS Express Services injiziert.<\/li>\n<\/ol>\n\n\n\n<div style=\"background:#f6f2ee;border-radius:8px;padding:1em 1.1em;margin:1.5em 0\"><div style=\"display:flex;justify-content:space-between;gap:1em;font-family:ui-monospace,SFMono-Regular,&apos;SF Mono&apos;,Menlo,Consolas,&apos;Liberation Mono&apos;,monospace;font-size:12px;line-height:1.4;margin:0 0 0.7em 0\"><span style=\"color:#9a938a\">infra\/hello-cdk\/lib\/ecs-express-stack.ts<\/span><span style=\"color:#9a938a\">TypeScript<\/span><\/div><pre style=\"margin:0;padding:0;background:none;border:none;overflow:auto;white-space:pre;font-family:ui-monospace,SFMono-Regular,&apos;SF Mono&apos;,Menlo,Consolas,&apos;Liberation Mono&apos;,monospace;font-size:14px;line-height:1.6;color:#2f3437\"><code class=\"\" data-line=\"\">&lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;const&lt;\/span&gt; service &lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;=&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;new&lt;\/span&gt; CfnExpressGatewayService&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;(&lt;\/span&gt;&lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;this&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt; &lt;span style=&quot;color: #4E9A06&quot;&gt;&#039;ExpressService&#039;&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt; &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;{&lt;\/span&gt;\n  &lt;span style=&quot;color: #8F5902;font-style: italic&quot;&gt;\/\/ Unique per environment: a prod deploy must not adopt the dev service.&lt;\/span&gt;\n  serviceName&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;scopedName&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;(&lt;\/span&gt;&lt;span style=&quot;color: #4E9A06&quot;&gt;&#039;cloudsheets-backend&#039;&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt; config&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;.&lt;\/span&gt;environment&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;),&lt;\/span&gt;\n  executionRoleArn&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;executionRole.roleArn&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt;\n  infrastructureRoleArn&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;infrastructureRole.roleArn&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt;\n  primaryContainer&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;{&lt;\/span&gt;\n    image&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;config.imageUri&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt;\n    containerPort&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt;\n    environment&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;[&lt;\/span&gt;\n      &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;{&lt;\/span&gt; name&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #4E9A06&quot;&gt;&#039;COGNITO_USER_POOL_ID&#039;&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt; value&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;config.cognitoUserPoolId&lt;\/span&gt; &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;},&lt;\/span&gt;\n      &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;{&lt;\/span&gt; name&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #4E9A06&quot;&gt;&#039;COGNITO_CLIENT_ID&#039;&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt; value&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;config.cognitoClientId&lt;\/span&gt; &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;},&lt;\/span&gt;\n      &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;{&lt;\/span&gt; name&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #4E9A06&quot;&gt;&#039;COGNITO_DOMAIN&#039;&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt; value&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;config.cognitoDomain&lt;\/span&gt; &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;},&lt;\/span&gt;\n      &lt;span style=&quot;color: #8F5902;font-style: italic&quot;&gt;\/\/ \u2026&lt;\/span&gt;\n    &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;],&lt;\/span&gt;\n  &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;},&lt;\/span&gt;\n&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;});&lt;\/span&gt;<\/code><\/pre><\/div>\n\n\n\n<ol start=\"3\" class=\"wp-block-list\">\n<li>Auslesen beim Start im Backend mit dotenv in <code class=\"\" data-line=\"\">index.ts<\/code>. Danach kann mit <code class=\"\" data-line=\"\">process.env.COGNITO_DOMAIN<\/code> die Cognito Domain in <code class=\"\" data-line=\"\">verifyUser.ts<\/code> verwendet werden.<\/li>\n<\/ol>\n\n\n\n<h3 class=\"wp-block-heading\">Cognito sagt nein<\/h3>\n\n\n\n<figure class=\"wp-block-image size-full is-resized\"><a href=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/grafik-1.png\"><img loading=\"lazy\" decoding=\"async\" width=\"469\" height=\"311\" data-attachment-id=\"29356\" data-permalink=\"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/09\/13\/cloudsheets-copycat-von-googlesheets\/grafik-37\/\" data-orig-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/grafik-1.png\" data-orig-size=\"469,311\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;alt&quot;:&quot;&quot;}\" data-image-title=\"grafik\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/grafik-1.png\" src=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/grafik-1.png\" alt=\"\" class=\"wp-image-29356\" style=\"width:596px;height:auto\" srcset=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/grafik-1.png 469w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/grafik-1-300x199.png 300w\" sizes=\"auto, (max-width: 469px) 100vw, 469px\" \/><\/a><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Cognito braucht SSL-Verschl\u00fcsselung um die Tokens sicher zu transportieren.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Mit Cloudfront davor kommen gleich 3 Vorteile mit:<\/p>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li>Cloudfront bietet native SSL-Verschl\u00fcsselung. Dadurch werden keine Teuren SSL-Zertifikate ben\u00f6tigt<\/li>\n\n\n\n<li>Cloudfront cached den Inhalt, der abgefragt wird und dadurch verbessert sich indirekt die Performance der Seite<\/li>\n\n\n\n<li>Der \u00f6ffentliche Lesezugriff auf den S3 Bucket kann eingeschr\u00e4nkt werden, was die Sicherheit erh\u00f6ht<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Allerdings kauft man sich gleich 3 potenzielle Nachteile wieder ein:<\/p>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li>Cloudfront erzeugt zus\u00e4tzliche Kosten und braucht zeit beim deploy<\/li>\n\n\n\n<li>Die Propagationen dauern<\/li>\n\n\n\n<li>Falsches Caching-Verhalten kann zu nervigen Bugs f\u00fchren und zeit beim Debuggen kosten<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Ein weiteres Problem, das mit Cognito mitkommt, ist die Konfiguration an sich.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Mit jedem Deploy \u00e4ndern sich die Konfigurationsdaten. Das Frontend braucht die n\u00f6tigen Daten f\u00fcr den Austausch der verschiedenen Tokens usw. mit dem idp. Das Problem ist, dass sich mit jedem Deploy diese Daten \u00e4ndern. Das Frontend braucht also irgendwie automatisiert die Daten. Allerdings kann man dem Frontend nicht einfach Zugriff auf die gesamte Cloud geben. Die L\u00f6sung war eine extra json die vom Client aus dem Bucket gefetcht wird. Dadurch ben\u00f6tigt das Frontend keinen Zugriff auf AWS selbst, sondern nur auf den Bucket, was es schon hat. Es aktualisiert sich von selbst.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Henne-Ei-Problem: And it goes round \u2018n round<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Zun\u00e4chst gab es einige zirkul\u00e4re Beziehungen, die Probleme beim Verbinden des Frontends mit dem Backend erzeugt haben.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Im ersten Ansatz wurde dieses Problem versucht mit Custom Ressources im Frontend und Backend-Stack zu l\u00f6sen. Entscheidend war jedoch die Anpassung der Build-Reihenfolge.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Sobald die n\u00f6tigen exportierten Variablen existieren, kann in einem separaten Frontend-Config-Stack eine JSON erzeugt werden, welche in den S3-Bucket gepusht wird.<\/p>\n\n\n\n<div style=\"background:#f6f2ee;border-radius:8px;padding:1em 1.1em;margin:1.5em 0\"><div style=\"display:flex;justify-content:space-between;gap:1em;font-family:ui-monospace,SFMono-Regular,&apos;SF Mono&apos;,Menlo,Consolas,&apos;Liberation Mono&apos;,monospace;font-size:12px;line-height:1.4;margin:0 0 0.7em 0\"><span style=\"color:#9a938a\">infra\/hello-cdk\/lib\/frontend-config-stack.ts<\/span><span style=\"color:#9a938a\">TypeScript<\/span><\/div><pre style=\"margin:0;padding:0;background:none;border:none;overflow:auto;white-space:pre;font-family:ui-monospace,SFMono-Regular,&apos;SF Mono&apos;,Menlo,Consolas,&apos;Liberation Mono&apos;,monospace;font-size:14px;line-height:1.6;color:#2f3437\"><code class=\"\" data-line=\"\">&lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;export&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;interface&lt;\/span&gt; FrontendConfigStackConfig &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;{&lt;\/span&gt;\n  frontendBucket&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;IBucket&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;;&lt;\/span&gt;\n  frontendDistribution&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;cloudfront.Distribution&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;;&lt;\/span&gt;\n  userPoolId&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;string&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;;&lt;\/span&gt;\n  userPoolClientId&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;string&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;;&lt;\/span&gt;\n  cognitoDomain&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;string&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;;&lt;\/span&gt;\n  callbackUrl&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;string&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;;&lt;\/span&gt;\n  apiUrl&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;string&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;;&lt;\/span&gt;\n&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;}&lt;\/span&gt;\n\n&lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;export&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;class&lt;\/span&gt; FrontendConfigStack &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;extends&lt;\/span&gt; Stack &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;{&lt;\/span&gt;\n  &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;constructor&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;(&lt;\/span&gt;\n    scope&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;Construct&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt;\n    id&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;string&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt;\n    config&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;FrontendConfigStackConfig&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt;\n    props?&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;StackProps&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt;\n  &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;)&lt;\/span&gt; &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;{&lt;\/span&gt;\n    &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;super&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;(&lt;\/span&gt;scope&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt; id&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt; props&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;);&lt;\/span&gt;\n\n    &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;new&lt;\/span&gt; s3deploy&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;.&lt;\/span&gt;BucketDeployment&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;(&lt;\/span&gt;&lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;this&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt; &lt;span style=&quot;color: #4E9A06&quot;&gt;&#039;DeployFrontendConfig&#039;&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt; &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;{&lt;\/span&gt;\n      sources&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;[&lt;\/span&gt;\n        s3deploy&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;.&lt;\/span&gt;Source&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;.&lt;\/span&gt;jsonData&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;(&lt;\/span&gt;&lt;span style=&quot;color: #4E9A06&quot;&gt;&#039;config.json&#039;&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt; &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;{&lt;\/span&gt;\n          authority&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #4E9A06&quot;&gt;`https:\/\/cognito-idp.${&lt;\/span&gt;&lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;this&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;.&lt;\/span&gt;region&lt;span style=&quot;color: #4E9A06&quot;&gt;}.amazonaws.com\/${&lt;\/span&gt;config&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;.&lt;\/span&gt;userPoolId&lt;span style=&quot;color: #4E9A06&quot;&gt;}`&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt;\n          clientId&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;config.userPoolClientId&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt;\n          callbackUrl&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;config.callbackUrl&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt;\n          logoutUrl&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;config.callbackUrl&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt;\n          cognitoDomain&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;config.cognitoDomain&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt;\n          apiUrl&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;config.apiUrl&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt;\n        &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;}),&lt;\/span&gt;\n      &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;],&lt;\/span&gt;\n      destinationBucket&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;config.frontendBucket&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt;\n      distribution&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;config.frontendDistribution&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt;\n      distributionPaths&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;[&lt;\/span&gt;&lt;span style=&quot;color: #4E9A06&quot;&gt;&#039;\/config.json&#039;&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;],&lt;\/span&gt;\n      prune&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;false&lt;\/span&gt;&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;,&lt;\/span&gt;\n    &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;});&lt;\/span&gt;\n  &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;}&lt;\/span&gt;\n&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;}&lt;\/span&gt;<\/code><\/pre><\/div>\n\n\n\n<p class=\"wp-block-paragraph\">Diese kann dann vom Frontend gefetcht werden. Dadurch sind die Stacks entkoppelt. Man kauft sich allerdings einen Nachteil ein:<br>Wenn einer der Stacks beim deploy fehlschl\u00e4gt, dann erzeugt dies Probleme. Dies geht auch mit der Architektur einher, die zu Beginn thematisiert wurde.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\">Money, Money, Money<\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">In die Cloud zu deployen kann teuer werden. Speziell dann, wenn man vergisst, die verwendeten Services nach der Pr\u00e4sentation zu l\u00f6schen und man dann pl\u00f6tzlich eine Mail bekommt: \u201cIhr Budget wurde \u00fcberschritten\u201d.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><a href=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/2026-09-13-15_09_05-WEB.DE-FreeMail-\u2014-Mozilla-Firefox.png\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"588\" data-attachment-id=\"29421\" data-permalink=\"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/09\/13\/cloudsheets-copycat-von-googlesheets\/2026-09-13-15_09_05-web-de-freemail-mozilla-firefox\/\" data-orig-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/2026-09-13-15_09_05-WEB.DE-FreeMail-\u2014-Mozilla-Firefox.png\" data-orig-size=\"1082,621\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;alt&quot;:&quot;&quot;}\" data-image-title=\"2026-09-13 15_09_05-WEB.DE FreeMail \u2014 Mozilla Firefox\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/2026-09-13-15_09_05-WEB.DE-FreeMail-\u2014-Mozilla-Firefox-1024x588.png\" src=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/2026-09-13-15_09_05-WEB.DE-FreeMail-\u2014-Mozilla-Firefox-1024x588.png\" alt=\"\" class=\"wp-image-29421\" srcset=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/2026-09-13-15_09_05-WEB.DE-FreeMail-\u2014-Mozilla-Firefox-1024x588.png 1024w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/2026-09-13-15_09_05-WEB.DE-FreeMail-\u2014-Mozilla-Firefox-300x172.png 300w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/2026-09-13-15_09_05-WEB.DE-FreeMail-\u2014-Mozilla-Firefox-768x441.png 768w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/2026-09-13-15_09_05-WEB.DE-FreeMail-\u2014-Mozilla-Firefox.png 1082w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/a><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">In diesem Fall muss man sich Gedanken machen, wann es Sinn macht, eine Anwendung in die Cloud zu deployen. Benutzen wenige User die Anwendung, so reicht es unter Umst\u00e4nden aus, einen einfachen Webserver zu verwenden. Muss die Anwendung sich an die Nutzlast anpassen und wird kollaborativ (unter Umst\u00e4nden in mehreren Regionen) verwendet, so kann es Sinn machen, sie in die Cloud auszulagern. Dies bietet zus\u00e4tzlich den Vorteil, dass man sich um die Wartung der Infrastruktur nicht k\u00fcmmern muss.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">\u00dcber die Zeit hinweg hat sich bei uns ein Service als gr\u00f6\u00dfter Kostenfaktor gezeigt: EC2. Selbst im Ruhezustand produzierte dieser Kosten. Zu Spitzenzeiten sogar 48,36$, in Verbindung mit mit dem Loadbalancer sogar etwas mehr als 60$ in etwas mehr als einer Woche. Dies kann Kosten von etwa 200$ bedeuten.<\/p>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/image.jpg\"><img loading=\"lazy\" decoding=\"async\" width=\"751\" height=\"293\" data-attachment-id=\"29358\" data-permalink=\"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/09\/13\/cloudsheets-copycat-von-googlesheets\/image-148\/\" data-orig-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/image.jpg\" data-orig-size=\"751,293\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;alt&quot;:&quot;&quot;}\" data-image-title=\"image\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/image.jpg\" src=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/image.jpg\" alt=\"\" class=\"wp-image-29358\" srcset=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/image.jpg 751w, https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/image-300x117.jpg 300w\" sizes=\"auto, (max-width: 751px) 100vw, 751px\" \/><\/a><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">In diesem Fall muss man sich fragen, ob es nicht besser w\u00e4re, statt EC2 FARGATE als Launch-Type zu nutzen, da dies Preis\/Leistungs-Technisch deutlich besser ist. Andere Alternativen sind beispielsweise Fargate Spot. Dies ist aber nicht so stabil wie Fargate selbst. F\u00fcr dev Umgebungen k\u00f6nnte dies unter Umst\u00e4nden sogar reichen. Nach einer genaueren Recherche stellte sich heraus, dass unter EC2-Other der gr\u00f6\u00dfte Kostenfaktor das ungenutzte NAT-Gateway war. Innerhalb von 11 Tagen sind 38,55$ an Kosten angefallen aufgrund des ungenutzten Nat-Gateways. Auf einen Monat hochgerechnet sind das etwas mehr als 100$.<\/p>\n\n\n\n<div style=\"background:#f6f2ee;border-radius:8px;padding:1em 1.1em;margin:1.5em 0\"><div style=\"display:flex;justify-content:space-between;gap:1em;font-family:ui-monospace,SFMono-Regular,&apos;SF Mono&apos;,Menlo,Consolas,&apos;Liberation Mono&apos;,monospace;font-size:12px;line-height:1.4;margin:0 0 0.7em 0\"><span style=\"color:#9a938a\">lib\/backend-stack.ts<\/span><span style=\"color:#9a938a\">TypeScript<\/span><\/div><pre style=\"margin:0;padding:0;background:none;border:none;overflow:auto;white-space:pre;font-family:ui-monospace,SFMono-Regular,&apos;SF Mono&apos;,Menlo,Consolas,&apos;Liberation Mono&apos;,monospace;font-size:14px;line-height:1.6;color:#2f3437\"><code class=\"\" data-line=\"\">&lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;this&lt;\/span&gt;.vpc &lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;=&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;new&lt;\/span&gt; Vpc(&lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;this&lt;\/span&gt;&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;,&lt;\/span&gt; &lt;span style=&quot;color: #4E9A06&quot;&gt;&#039;BackendVpc&#039;&lt;\/span&gt;&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;,&lt;\/span&gt; &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;{&lt;\/span&gt; maxAzs&lt;span style=&quot;color: #CE5C00;font-weight: bold&quot;&gt;:&lt;\/span&gt; &lt;span style=&quot;color: #204A87;font-weight: bold&quot;&gt;2&lt;\/span&gt; &lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;}&lt;\/span&gt;)&lt;span style=&quot;color: #000;font-weight: bold&quot;&gt;;&lt;\/span&gt;<\/code><\/pre><\/div>\n\n\n\n<figure class=\"wp-block-image size-full\"><a href=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/image.gif\"><img loading=\"lazy\" decoding=\"async\" width=\"752\" height=\"490\" data-attachment-id=\"29359\" data-permalink=\"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/09\/13\/cloudsheets-copycat-von-googlesheets\/image-148\/\" data-orig-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/image.gif\" data-orig-size=\"752,490\" data-comments-opened=\"1\" data-image-meta=\"{&quot;aperture&quot;:&quot;0&quot;,&quot;credit&quot;:&quot;&quot;,&quot;camera&quot;:&quot;&quot;,&quot;caption&quot;:&quot;&quot;,&quot;created_timestamp&quot;:&quot;0&quot;,&quot;copyright&quot;:&quot;&quot;,&quot;focal_length&quot;:&quot;0&quot;,&quot;iso&quot;:&quot;0&quot;,&quot;shutter_speed&quot;:&quot;0&quot;,&quot;title&quot;:&quot;&quot;,&quot;orientation&quot;:&quot;0&quot;,&quot;alt&quot;:&quot;&quot;}\" data-image-title=\"image\" data-image-description=\"\" data-image-caption=\"\" data-large-file=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/image.gif\" src=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/image.gif\" alt=\"\" class=\"wp-image-29359\" \/><\/a><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">Zur Kostenoptimierung w\u00fcrde es sich also anbieten das NatGateway zu entfernen. <\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Weitere Themen, die man sich noch anschauen k\u00f6nnte, sind die VPC-Kosten. Allerdings sind diese nicht weiter optimierbar, da die Kostenursache die Public IP-Address ist und diese f\u00fcr den Loadbalancer ben\u00f6tigt wird.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Next Steps<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">F\u00fcr die Zukunft w\u00fcrden sich noch folgende Themen anbieten<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Horizontale Skalierung mit Redis -&gt; wenn User von verschiedenen Backends\/Servern dasselbe Sheet bearbeiten wollen<\/li>\n\n\n\n<li>feste Domain (damit sich nicht nach jedem Deployment jedes mal der Link \u00e4ndert)<\/li>\n\n\n\n<li>RDS-Proxy f\u00fcr DB-Skalierung<\/li>\n\n\n\n<li>Multi-Region-Deployment<\/li>\n\n\n\n<li>Kostenoptimierung<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Learnings<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Ein gr\u00fcner Deploy ist kein Nachweis f\u00fcr eine funktionierende Anwendung.<\/strong>&nbsp; CloudFormation meldet Erfolg, sobald die Ressourcen angelegt sind. Ob dahinter tats\u00e4chlich etwas erreichbar ist, bleibt eine andere Frage. Der Smoke-Test in der Pipeline war die einzige Pr\u00fcfung, die den internen Load Balancer bemerkt hat, kein Unit-Test h\u00e4tte das gefunden.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Die Art des Fehlers zeigt, wo man suchen muss.<\/strong> Timeout statt Fehlercode war das eigentliche Signal. H\u00e4tten wir zuerst in die Container-Logs geschaut, w\u00e4ren wir lange besch\u00e4ftigt gewesen. Die sahen n\u00e4mlich v\u00f6llig normal aus.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Fr\u00fcher deployed \u2013 localhost l\u00fcgt.<\/strong> Fast alles, was uns aufgehalten hat, lag ausschlie\u00dflich in der Cloud: das fehlende databaseName, die knex-CLI, der interne Load Balancer. Lokal lief jedes Mal alles. Einmal hat localhost sogar wortw\u00f6rtlich gelogen: Die CI starb an ECONNREFUSED ::1:5432, weil Node ab Version 17 localhost zuerst auf IPv6 aufl\u00f6st, der Postgres-Container im Actions-Runner seinen Port aber nur auf IPv4 ver\u00f6ffentlicht.&nbsp; Die L\u00f6sung waren neun Zeichen: localhost \u2192 127.0.0.1. R\u00fcckblickend h\u00e4tte ein durchgehender Deployment-Pfad, notfalls mit einem Dummy-Frontend, Wochen vor der eigentlichen Funktionalit\u00e4t stehen sollen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Absicherungen dort, wo sie greifen.<\/strong> Ein Kommentar \u00fcber drei Config-Zeilen hat noch nie jemanden aufgehalten. Ein Skript, das die Ausf\u00fchrung abbricht, schon. Wir haben die betroffenen Stellen so umgebaut, dass der falsche Zustand gar nicht erst entstehen kann, statt nur davor zu warnen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Die Build-Reihenfolge entscheidet<\/strong>: Die Build-Reihenfolge entscheidet, wie sich Dinge miteinander verkn\u00fcpfen lassen. Und kann einen Prozess vereinfachen oder auch erschweren. Oder eine zirkul\u00e4re Beziehung verursachen \ud83d\ude42<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Services verstehen: <\/strong>Es erleichtert Vieles, wenn man sich zuerst mit der Konsole eines Services besch\u00e4ftigt. So bekommt man ein grundlegendes Verst\u00e4ndnis der Services und kann die einzelnen Parameter sp\u00e4ter leichter im cdk umsetzen. Auch h\u00e4tte das uns die vielen Kosten in Verbindung mit dem ungenutzten Natgateway gespart.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Fr\u00fcher Deployed &#8211; Localhost l\u00fcgt (Teil 2):<\/strong> Cognito funktioniert mit Localhost problemlos ohne SSL-Verschl\u00fcsselung. Sobald die Webseite aber selbst deployed wird, ver\u00e4ndert sich die Situation. Cognito wirft dann eine Fehlermeldung, weil die Token ohne Verschl\u00fcsselung nicht \u00fcbertragen werden k\u00f6nnen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Kosten, die bis ins Astronomische gehen: <\/strong>Dies geht mit dem Punkt &#8220;Services verstehen&#8221; einher. Die Art, wie Services genutzt werden, kann sich auf den Faktor Kosten auswirken (siehe ungenutztes NAT-Gateway).<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Ein zentrales einziges Backend ist nicht trivial:<\/strong> Bei der Nutzung externer Libraries und Tools ist es oft nicht selbstverst\u00e4ndlich, dass sie sich nahtlos zu einem einheitlichen Backend zusammenf\u00fcgen lassen. Es hilft, diese Anforderung schon bei der Tool-Auswahl und -Recherche bereits im Hinterkopf zu haben oder auf ein einheitliches Backend zu verzichten und stattdessen mehrere Services\/Container zu verkn\u00fcpfen.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Cross-Stack-Variablen:<\/strong> Das dynamische Durchreichen von Variablen \u00fcber verschiedene Stacks hinweg ist mit Infrastructure as Code erstaunlich unkompliziert.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Fazit<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Dieses Projekt hat seinen Zweck voll erf\u00fcllt. Wir konnten erste praktische Erfahrungen mit AWS sammeln und eine Menge dabei lernen. Gleichzeitig haben wir gemerkt, wie viel Tiefe und Lernpotenzial AWS dar\u00fcber hinaus noch bietet.<br>Mit etwas mehr Zeit h\u00e4tten wir uns beispielsweise gerne noch die Synchronisation mehrerer Backend-Instanzen mithilfe von Redis\/Caching angeschaut und die Skalierung ausgiebiger getestet. Gerade weil wir auf eine recht klassische Schichtenarchitektur gesetzt haben, w\u00e4ren wir hier vermutlich schnell auf weitere spannende Herausforderungen und Learnings gesto\u00dfen.<br>Besonders \u00fcberrascht hat uns, wie schnell die Kosten bei AWS in die H\u00f6he schie\u00dfen k\u00f6nnen. In zuk\u00fcnftigen Projekten werden wir hier definitiv noch wachsamer sein.<br>Abseits der Technik gab es, wie so oft bei praktischen Gruppenarbeiten, auch wichtige Learnings f\u00fcr die Zusammenarbeit. Neben transparenter Kommunikation geh\u00f6rt dazu vor allem Git. R\u00fcckblickend h\u00e4tten wir gerne schon etwas fr\u00fcher im Projekt eine Pipeline f\u00fcr automatisiertes Testen und Deployen aufgesetzt. Dies h\u00e4tte einige Prozesse vermutlich enorm beschleunigt, da Fehler fr\u00fcher aufgefallen w\u00e4ren.<br>Wir hoffen, wir konnten mit diesem Blogpost unseren Lernprozess und die gel\u00f6sten H\u00fcrden gut nachvollziehbar machen!<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Quellen<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><a href=\"https:\/\/tiptap.dev\/docs\/hocuspocus\/\">https:\/\/tiptap.dev\/docs\/hocuspocus\/<\/a> Abgerufen am: 18.06.2026 10:40<\/li>\n\n\n\n<li><a href=\"https:\/\/www.telerik.com\/blogs\/modern-apis-fastify-node\">https:\/\/www.telerik.com\/blogs\/modern-apis-fastify-node<\/a> Abgerufen am: 18.06.2026 10:40<\/li>\n\n\n\n<li><a href=\"https:\/\/www.fradev.io\/articles\/kubernetes\/create-react-project-with-vite\/\">https:\/\/www.fradev.io\/articles\/kubernetes\/create-react-project-with-vite\/<\/a> Abgerufen am: 18.06.2026 10:40<\/li>\n\n\n\n<li><a href=\"https:\/\/yjs.dev\/\">https:\/\/yjs.dev\/<\/a> Abgerufen am: 18.06.2026 10:41<\/li>\n\n\n\n<li><a href=\"https:\/\/en.wikipedia.org\/wiki\/Conflict-free_replicated_data_type\">https:\/\/en.wikipedia.org\/wiki\/Conflict-free_replicated_data_type<\/a>&nbsp; Abgerufen am: 18.06.2026 10:43<\/li>\n\n\n\n<li><a href=\"https:\/\/docs.aws.amazon.com\/acm\/latest\/userguide\/acm-overview.html\" target=\"_blank\" rel=\"noopener\" title=\"kostenloses SSL Zertifikat\">https:\/\/docs.aws.amazon.com\/acm\/latest\/userguide\/acm-overview.html<\/a>, Abgerufen am 13.09.2026 16:35<\/li>\n\n\n\n<li><a href=\"https:\/\/aws.amazon.com\/de\/about-aws\/whats-new\/2016\/11\/announcing-regional-edge-caches-for-amazon-cloudfront\/\" target=\"_blank\" rel=\"noopener\">https:\/\/aws.amazon.com\/de\/about-aws\/whats-new\/2016\/11\/announcing-regional-edge-caches-for-amazon-cloudfront\/<\/a>, Abgerufen am 13.09.2026 16:38<\/li>\n\n\n\n<li><a href=\"https:\/\/docs.aws.amazon.com\/AmazonCloudFront\/latest\/DeveloperGuide\/private-content-restricting-access-to-s3.html\">https:\/\/docs.aws.amazon.com\/AmazonCloudFront\/latest\/DeveloperGuide\/private-content-restricting-access-to-s3.html<\/a>, Abgerufen am 13.09.2026 16:39<\/li>\n\n\n\n<li><a href=\"https:\/\/aws.amazon.com\/de\/cloudfront\/pricing\/\">https:\/\/aws.amazon.com\/de\/cloudfront\/pricing\/<\/a>, Abgerufen am 13.09.2026 16:40<\/li>\n\n\n\n<li><a href=\"https:\/\/docs.aws.amazon.com\/AmazonCloudFront\/latest\/DeveloperGuide\/Expiration.html\">https:\/\/docs.aws.amazon.com\/AmazonCloudFront\/latest\/DeveloperGuide\/Expiration.html<\/a>, Abgerufen am 13.09.2026 16:42<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Eine Anwendung von Lenny Ziegler (lz045), Jonas Gehrung (jg175), Adrian Rossa(ar162) Was ist Cloudsheets? CloudSheets ist eine Cloud-basierte Web Applikation f\u00fcr die kollaborative Bearbeitung von Tabellen. Ziel des Projekts ist es, die Grundfunktionalit\u00e4t von Google Sheets in einer schlanken Anwendung nachzubauen. Gleichzeitig soll durch den gezielten Einsatz m\u00f6glichst vieler AWS-Services ein cloud-nativer Entwicklungsprozess und praktischer [&hellip;]<\/p>\n","protected":false},"author":1267,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_jetpack_newsletter_access":"","_jetpack_dont_email_post_to_subs":false,"_jetpack_newsletter_tier_id":0,"_jetpack_memberships_contains_paywalled_content":false,"_jetpack_feature_clip_id":0,"_jetpack_memberships_contains_paid_content":false,"footnotes":"","jetpack_post_was_ever_published":false,"_ppma_block_editor_authors":""},"categories":[1],"tags":[],"ppma_author":[1117],"class_list":["post-29340","post","type-post","status-publish","format-standard","hentry","category-allgemein"],"aioseo_notices":[],"aioseo_head":"\n\t\t<!-- All in One SEO 5.0.1.1 - aioseo.com -->\n\t<meta name=\"description\" content=\"Eine Anwendung von Lenny Ziegler (lz045), Jonas Gehrung (jg175), Adrian Rossa(ar162) Was ist Cloudsheets? CloudSheets ist eine Cloud-basierte Web Applikation f\u00fcr die kollaborative Bearbeitung von Tabellen. Ziel des Projekts ist es, die Grundfunktionalit\u00e4t von Google Sheets in einer schlanken Anwendung nachzubauen. Gleichzeitig soll durch den gezielten Einsatz m\u00f6glichst vieler AWS-Services ein cloud-nativer Entwicklungsprozess und praktischer\" \/>\n\t<meta name=\"robots\" content=\"max-image-preview:large\" \/>\n\t<meta name=\"author\" content=\"Jonas Gehrung\"\/>\n\t<link rel=\"canonical\" href=\"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/09\/13\/cloudsheets-copycat-von-googlesheets\/\" \/>\n\t<meta name=\"generator\" content=\"All in One SEO (AIOSEO) 5.0.1.1\" \/>\n\t\t<meta property=\"og:locale\" content=\"en_US\" \/>\n\t\t<meta property=\"og:site_name\" content=\"Computer Science Blog\" \/>\n\t\t<meta property=\"og:type\" content=\"article\" \/>\n\t\t<meta property=\"og:title\" content=\"Cloudsheets \u2013 Implementierung einer cloud-basierten Echtzeit-Kollaborationsl\u00f6sung | Computer Science Blog @ HdM Stuttgart\" \/>\n\t\t<meta property=\"og:description\" content=\"Eine Anwendung von Lenny Ziegler (lz045), Jonas Gehrung (jg175), Adrian Rossa(ar162) Was ist Cloudsheets? CloudSheets ist eine Cloud-basierte Web Applikation f\u00fcr die kollaborative Bearbeitung von Tabellen. Ziel des Projekts ist es, die Grundfunktionalit\u00e4t von Google Sheets in einer schlanken Anwendung nachzubauen. Gleichzeitig soll durch den gezielten Einsatz m\u00f6glichst vieler AWS-Services ein cloud-nativer Entwicklungsprozess und praktischer\" \/>\n\t\t<meta property=\"og:url\" content=\"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/09\/13\/cloudsheets-copycat-von-googlesheets\/\" \/>\n\t\t<meta property=\"og:image\" content=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/grafik.png\" \/>\n\t\t<meta property=\"og:image:secure_url\" content=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/grafik.png\" \/>\n\t\t<meta property=\"og:image:width\" content=\"940\" \/>\n\t\t<meta property=\"og:image:height\" content=\"392\" \/>\n\t\t<meta property=\"article:published_time\" content=\"2026-09-13T12:03:55+00:00\" \/>\n\t\t<meta property=\"article:modified_time\" content=\"2026-09-13T15:47:26+00:00\" \/>\n\t\t<meta name=\"twitter:card\" content=\"summary\" \/>\n\t\t<meta name=\"twitter:title\" content=\"Cloudsheets \u2013 Implementierung einer cloud-basierten Echtzeit-Kollaborationsl\u00f6sung | Computer Science Blog @ HdM Stuttgart\" \/>\n\t\t<meta name=\"twitter:description\" content=\"Eine Anwendung von Lenny Ziegler (lz045), Jonas Gehrung (jg175), Adrian Rossa(ar162) Was ist Cloudsheets? CloudSheets ist eine Cloud-basierte Web Applikation f\u00fcr die kollaborative Bearbeitung von Tabellen. Ziel des Projekts ist es, die Grundfunktionalit\u00e4t von Google Sheets in einer schlanken Anwendung nachzubauen. Gleichzeitig soll durch den gezielten Einsatz m\u00f6glichst vieler AWS-Services ein cloud-nativer Entwicklungsprozess und praktischer\" \/>\n\t\t<meta name=\"twitter:image\" content=\"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/grafik.png\" \/>\n\t\t<script type=\"application\/ld+json\" class=\"aioseo-schema\">\n\t\t\t{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"Article\",\"@id\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/index.php\\\/2026\\\/09\\\/13\\\/cloudsheets-copycat-von-googlesheets\\\/#article\",\"name\":\"Cloudsheets \\u2013 Implementierung einer cloud-basierten Echtzeit-Kollaborationsl\\u00f6sung | Computer Science Blog @ HdM Stuttgart\",\"headline\":\"Cloudsheets &#8211; Implementierung einer cloud-basierten Echtzeit-Kollaborationsl\\u00f6sung\",\"author\":{\"@id\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/index.php\\\/author\\\/jonas_gehrung\\\/#author\"},\"publisher\":{\"@id\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/#organization\"},\"image\":{\"@type\":\"ImageObject\",\"url\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/wp-content\\\/uploads\\\/2026\\\/09\\\/grafik.png\",\"@id\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/index.php\\\/2026\\\/09\\\/13\\\/cloudsheets-copycat-von-googlesheets\\\/#articleImage\",\"width\":940,\"height\":392},\"datePublished\":\"2026-09-13T14:03:55+02:00\",\"dateModified\":\"2026-09-13T17:47:26+02:00\",\"inLanguage\":\"en-US\",\"mainEntityOfPage\":{\"@id\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/index.php\\\/2026\\\/09\\\/13\\\/cloudsheets-copycat-von-googlesheets\\\/#webpage\"},\"isPartOf\":{\"@id\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/index.php\\\/2026\\\/09\\\/13\\\/cloudsheets-copycat-von-googlesheets\\\/#webpage\"},\"articleSection\":\"Allgemein, Jonas Gehrung\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/index.php\\\/2026\\\/09\\\/13\\\/cloudsheets-copycat-von-googlesheets\\\/#breadcrumblist\",\"itemListElement\":[{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de#listItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/index.php\\\/category\\\/allgemein\\\/#listItem\",\"name\":\"Allgemein\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/index.php\\\/category\\\/allgemein\\\/#listItem\",\"position\":2,\"name\":\"Allgemein\",\"item\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/index.php\\\/category\\\/allgemein\\\/\",\"nextItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/index.php\\\/2026\\\/09\\\/13\\\/cloudsheets-copycat-von-googlesheets\\\/#listItem\",\"name\":\"Cloudsheets &#8211; Implementierung einer cloud-basierten Echtzeit-Kollaborationsl\\u00f6sung\"},\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de#listItem\",\"name\":\"Home\"}},{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/index.php\\\/2026\\\/09\\\/13\\\/cloudsheets-copycat-von-googlesheets\\\/#listItem\",\"position\":3,\"name\":\"Cloudsheets &#8211; Implementierung einer cloud-basierten Echtzeit-Kollaborationsl\\u00f6sung\",\"previousItem\":{\"@type\":\"ListItem\",\"@id\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/index.php\\\/category\\\/allgemein\\\/#listItem\",\"name\":\"Allgemein\"}}]},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/#organization\",\"name\":\"Computer Science Blog @ HdM Stuttgart\",\"description\":\"on computer science and media topics\",\"url\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/\"},{\"@type\":\"Person\",\"@id\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/index.php\\\/author\\\/jonas_gehrung\\\/#author\",\"url\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/index.php\\\/author\\\/jonas_gehrung\\\/\",\"name\":\"Jonas Gehrung\",\"image\":{\"@type\":\"ImageObject\",\"@id\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/index.php\\\/2026\\\/09\\\/13\\\/cloudsheets-copycat-von-googlesheets\\\/#authorImage\",\"url\":\"https:\\\/\\\/secure.gravatar.com\\\/avatar\\\/923dcc47b11b657fb4ce19332a1f24c43000c4e01cc7046ac8b04e1e12677bf6?s=96&d=mm&r=g\",\"width\":96,\"height\":96,\"caption\":\"Jonas Gehrung\"}},{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/index.php\\\/2026\\\/09\\\/13\\\/cloudsheets-copycat-von-googlesheets\\\/#webpage\",\"url\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/index.php\\\/2026\\\/09\\\/13\\\/cloudsheets-copycat-von-googlesheets\\\/\",\"name\":\"Cloudsheets \\u2013 Implementierung einer cloud-basierten Echtzeit-Kollaborationsl\\u00f6sung | Computer Science Blog @ HdM Stuttgart\",\"description\":\"Eine Anwendung von Lenny Ziegler (lz045), Jonas Gehrung (jg175), Adrian Rossa(ar162) Was ist Cloudsheets? CloudSheets ist eine Cloud-basierte Web Applikation f\\u00fcr die kollaborative Bearbeitung von Tabellen. Ziel des Projekts ist es, die Grundfunktionalit\\u00e4t von Google Sheets in einer schlanken Anwendung nachzubauen. Gleichzeitig soll durch den gezielten Einsatz m\\u00f6glichst vieler AWS-Services ein cloud-nativer Entwicklungsprozess und praktischer\",\"inLanguage\":\"en-US\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/#website\"},\"breadcrumb\":{\"@id\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/index.php\\\/2026\\\/09\\\/13\\\/cloudsheets-copycat-von-googlesheets\\\/#breadcrumblist\"},\"author\":{\"@id\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/index.php\\\/author\\\/jonas_gehrung\\\/#author\"},\"creator\":{\"@id\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/index.php\\\/author\\\/jonas_gehrung\\\/#author\"},\"datePublished\":\"2026-09-13T14:03:55+02:00\",\"dateModified\":\"2026-09-13T17:47:26+02:00\"},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/#website\",\"url\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/\",\"name\":\"Computer Science Blog @ HdM Stuttgart\",\"description\":\"on computer science and media topics\",\"inLanguage\":\"en-US\",\"publisher\":{\"@id\":\"https:\\\/\\\/blog.mi.hdm-stuttgart.de\\\/#organization\"}}]}\n\t\t<\/script>\n\t\t<!-- All in One SEO -->\n\n","aioseo_head_json":{"title":"Cloudsheets \u2013 Implementierung einer cloud-basierten Echtzeit-Kollaborationsl\u00f6sung | Computer Science Blog @ HdM Stuttgart","description":"Eine Anwendung von Lenny Ziegler (lz045), Jonas Gehrung (jg175), Adrian Rossa(ar162) Was ist Cloudsheets? CloudSheets ist eine Cloud-basierte Web Applikation f\u00fcr die kollaborative Bearbeitung von Tabellen. Ziel des Projekts ist es, die Grundfunktionalit\u00e4t von Google Sheets in einer schlanken Anwendung nachzubauen. Gleichzeitig soll durch den gezielten Einsatz m\u00f6glichst vieler AWS-Services ein cloud-nativer Entwicklungsprozess und praktischer","canonical_url":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/09\/13\/cloudsheets-copycat-von-googlesheets\/","robots":"max-image-preview:large","keywords":"","webmasterTools":{"miscellaneous":""},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"Article","@id":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/09\/13\/cloudsheets-copycat-von-googlesheets\/#article","name":"Cloudsheets \u2013 Implementierung einer cloud-basierten Echtzeit-Kollaborationsl\u00f6sung | Computer Science Blog @ HdM Stuttgart","headline":"Cloudsheets &#8211; Implementierung einer cloud-basierten Echtzeit-Kollaborationsl\u00f6sung","author":{"@id":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/author\/jonas_gehrung\/#author"},"publisher":{"@id":"https:\/\/blog.mi.hdm-stuttgart.de\/#organization"},"image":{"@type":"ImageObject","url":"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/grafik.png","@id":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/09\/13\/cloudsheets-copycat-von-googlesheets\/#articleImage","width":940,"height":392},"datePublished":"2026-09-13T14:03:55+02:00","dateModified":"2026-09-13T17:47:26+02:00","inLanguage":"en-US","mainEntityOfPage":{"@id":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/09\/13\/cloudsheets-copycat-von-googlesheets\/#webpage"},"isPartOf":{"@id":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/09\/13\/cloudsheets-copycat-von-googlesheets\/#webpage"},"articleSection":"Allgemein, Jonas Gehrung"},{"@type":"BreadcrumbList","@id":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/09\/13\/cloudsheets-copycat-von-googlesheets\/#breadcrumblist","itemListElement":[{"@type":"ListItem","@id":"https:\/\/blog.mi.hdm-stuttgart.de#listItem","position":1,"name":"Home","item":"https:\/\/blog.mi.hdm-stuttgart.de","nextItem":{"@type":"ListItem","@id":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/category\/allgemein\/#listItem","name":"Allgemein"}},{"@type":"ListItem","@id":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/category\/allgemein\/#listItem","position":2,"name":"Allgemein","item":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/category\/allgemein\/","nextItem":{"@type":"ListItem","@id":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/09\/13\/cloudsheets-copycat-von-googlesheets\/#listItem","name":"Cloudsheets &#8211; Implementierung einer cloud-basierten Echtzeit-Kollaborationsl\u00f6sung"},"previousItem":{"@type":"ListItem","@id":"https:\/\/blog.mi.hdm-stuttgart.de#listItem","name":"Home"}},{"@type":"ListItem","@id":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/09\/13\/cloudsheets-copycat-von-googlesheets\/#listItem","position":3,"name":"Cloudsheets &#8211; Implementierung einer cloud-basierten Echtzeit-Kollaborationsl\u00f6sung","previousItem":{"@type":"ListItem","@id":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/category\/allgemein\/#listItem","name":"Allgemein"}}]},{"@type":"Organization","@id":"https:\/\/blog.mi.hdm-stuttgart.de\/#organization","name":"Computer Science Blog @ HdM Stuttgart","description":"on computer science and media topics","url":"https:\/\/blog.mi.hdm-stuttgart.de\/"},{"@type":"Person","@id":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/author\/jonas_gehrung\/#author","url":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/author\/jonas_gehrung\/","name":"Jonas Gehrung","image":{"@type":"ImageObject","@id":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/09\/13\/cloudsheets-copycat-von-googlesheets\/#authorImage","url":"https:\/\/secure.gravatar.com\/avatar\/923dcc47b11b657fb4ce19332a1f24c43000c4e01cc7046ac8b04e1e12677bf6?s=96&d=mm&r=g","width":96,"height":96,"caption":"Jonas Gehrung"}},{"@type":"WebPage","@id":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/09\/13\/cloudsheets-copycat-von-googlesheets\/#webpage","url":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/09\/13\/cloudsheets-copycat-von-googlesheets\/","name":"Cloudsheets \u2013 Implementierung einer cloud-basierten Echtzeit-Kollaborationsl\u00f6sung | Computer Science Blog @ HdM Stuttgart","description":"Eine Anwendung von Lenny Ziegler (lz045), Jonas Gehrung (jg175), Adrian Rossa(ar162) Was ist Cloudsheets? CloudSheets ist eine Cloud-basierte Web Applikation f\u00fcr die kollaborative Bearbeitung von Tabellen. Ziel des Projekts ist es, die Grundfunktionalit\u00e4t von Google Sheets in einer schlanken Anwendung nachzubauen. Gleichzeitig soll durch den gezielten Einsatz m\u00f6glichst vieler AWS-Services ein cloud-nativer Entwicklungsprozess und praktischer","inLanguage":"en-US","isPartOf":{"@id":"https:\/\/blog.mi.hdm-stuttgart.de\/#website"},"breadcrumb":{"@id":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/09\/13\/cloudsheets-copycat-von-googlesheets\/#breadcrumblist"},"author":{"@id":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/author\/jonas_gehrung\/#author"},"creator":{"@id":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/author\/jonas_gehrung\/#author"},"datePublished":"2026-09-13T14:03:55+02:00","dateModified":"2026-09-13T17:47:26+02:00"},{"@type":"WebSite","@id":"https:\/\/blog.mi.hdm-stuttgart.de\/#website","url":"https:\/\/blog.mi.hdm-stuttgart.de\/","name":"Computer Science Blog @ HdM Stuttgart","description":"on computer science and media topics","inLanguage":"en-US","publisher":{"@id":"https:\/\/blog.mi.hdm-stuttgart.de\/#organization"}}]},"og:locale":"en_US","og:site_name":"Computer Science Blog","og:type":"article","og:title":"Cloudsheets \u2013 Implementierung einer cloud-basierten Echtzeit-Kollaborationsl\u00f6sung | Computer Science Blog @ HdM Stuttgart","og:description":"Eine Anwendung von Lenny Ziegler (lz045), Jonas Gehrung (jg175), Adrian Rossa(ar162) Was ist Cloudsheets? CloudSheets ist eine Cloud-basierte Web Applikation f\u00fcr die kollaborative Bearbeitung von Tabellen. Ziel des Projekts ist es, die Grundfunktionalit\u00e4t von Google Sheets in einer schlanken Anwendung nachzubauen. Gleichzeitig soll durch den gezielten Einsatz m\u00f6glichst vieler AWS-Services ein cloud-nativer Entwicklungsprozess und praktischer","og:url":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/09\/13\/cloudsheets-copycat-von-googlesheets\/","og:image":"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/grafik.png","og:image:secure_url":"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/grafik.png","og:image:width":940,"og:image:height":392,"article:published_time":"2026-09-13T12:03:55+00:00","article:modified_time":"2026-09-13T15:47:26+00:00","twitter:card":"summary","twitter:title":"Cloudsheets \u2013 Implementierung einer cloud-basierten Echtzeit-Kollaborationsl\u00f6sung | Computer Science Blog @ HdM Stuttgart","twitter:description":"Eine Anwendung von Lenny Ziegler (lz045), Jonas Gehrung (jg175), Adrian Rossa(ar162) Was ist Cloudsheets? CloudSheets ist eine Cloud-basierte Web Applikation f\u00fcr die kollaborative Bearbeitung von Tabellen. Ziel des Projekts ist es, die Grundfunktionalit\u00e4t von Google Sheets in einer schlanken Anwendung nachzubauen. Gleichzeitig soll durch den gezielten Einsatz m\u00f6glichst vieler AWS-Services ein cloud-nativer Entwicklungsprozess und praktischer","twitter:image":"https:\/\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2026\/09\/grafik.png"},"aioseo_meta_data":{"post_id":"29340","title":null,"description":null,"keywords":null,"keyphrases":{"focus":{"keyphrase":"","score":0,"analysis":{"keyphraseInTitle":{"score":0,"maxScore":9,"error":1}}},"additional":[]},"primary_term":null,"canonical_url":null,"og_title":null,"og_description":null,"og_object_type":"default","og_image_type":"default","og_image_url":null,"og_image_width":null,"og_image_height":null,"og_image_custom_url":null,"og_image_custom_fields":null,"og_video":"","og_custom_url":null,"og_article_section":null,"og_article_tags":null,"twitter_use_og":false,"twitter_card":"default","twitter_image_type":"default","twitter_image_url":null,"twitter_image_custom_url":null,"twitter_image_custom_fields":null,"twitter_title":null,"twitter_description":null,"schema":{"blockGraphs":[],"customGraphs":[],"default":{"data":{"Article":[],"Course":[],"Dataset":[],"FAQPage":[],"Movie":[],"Person":[],"Product":[],"ProductReview":[],"Car":[],"Recipe":[],"Service":[],"SoftwareApplication":[],"WebPage":[]},"graphName":"Article","isEnabled":true},"graphs":[]},"schema_type":"default","schema_type_options":null,"pillar_content":false,"robots_default":true,"robots_noindex":false,"robots_noarchive":false,"robots_nosnippet":false,"robots_nofollow":false,"robots_noimageindex":false,"robots_noodp":false,"robots_notranslate":false,"robots_max_snippet":"-1","robots_max_videopreview":"-1","robots_max_imagepreview":"large","priority":null,"frequency":"default","local_seo":null,"breadcrumb_settings":null,"limit_modified_date":false,"ai":{"faqs":[],"keyPoints":[],"schemas":[],"titles":[],"descriptions":[],"socialPosts":{"email":{"subject":"","preview":"","content":""},"linkedin":[],"twitter":[],"facebook":[],"instagram":[]}},"created":"2026-09-12 11:59:55","updated":"2026-09-13 15:47:26","seo_analyzer_scan_date":null,"focus_keyword":null,"additional_keywords":null,"truseo_locale":null},"aioseo_breadcrumb":"<div class=\"aioseo-breadcrumbs\"><span class=\"aioseo-breadcrumb\">\n\t\t\t<a href=\"https:\/\/blog.mi.hdm-stuttgart.de\" title=\"Home\">Home<\/a>\n\t\t<\/span><span class=\"aioseo-breadcrumb-separator\">&raquo;<\/span><span class=\"aioseo-breadcrumb\">\n\t\t\t<a href=\"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/category\/allgemein\/\" title=\"Allgemein\">Allgemein<\/a>\n\t\t<\/span><span class=\"aioseo-breadcrumb-separator\">&raquo;<\/span><span class=\"aioseo-breadcrumb\">\n\t\t\tCloudsheets \u2013 Implementierung einer cloud-basierten Echtzeit-Kollaborationsl\u00f6sung\n\t\t<\/span><\/div>","aioseo_breadcrumb_json":[{"label":"Home","link":"https:\/\/blog.mi.hdm-stuttgart.de"},{"label":"Allgemein","link":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/category\/allgemein\/"},{"label":"Cloudsheets &#8211; Implementierung einer cloud-basierten Echtzeit-Kollaborationsl\u00f6sung","link":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2026\/09\/13\/cloudsheets-copycat-von-googlesheets\/"}],"jetpack-related-posts":[{"id":21693,"url":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2021\/09\/20\/recall-trainer-eine-serverless-web-app-mit-aws\/","url_meta":{"origin":29340,"position":0},"title":"Recall Trainer &#8211;  Eine serverless Web-App mit AWS","author":"lr066","date":"20. September 2021","format":false,"excerpt":"Einleitung Im Rahmen der Vorlesung \u201cSoftware Development for Cloud Computing\u201d habe ich im vergangenen Semester eine Einf\u00fchrung in die Welt des Cloud Computings incl. der relevanten Konzepte und Technologien erhalten. Einige dieser Konzepte habe ich versucht in meinem Abschlussprojekt umzusetzen, das ich im Nachfolgenden vorstellen m\u00f6chte.\u00a0 Idee\/Projekt Die Idee war\u2026","rel":"","context":"In &quot;Cloud Technologies&quot;","block_context":{"text":"Cloud Technologies","link":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/category\/scalable-systems\/cloud-technologies\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2021\/09\/signup1-150x150.png?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2021\/09\/signup1-150x150.png?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2021\/09\/signup1-150x150.png?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2021\/09\/signup1-150x150.png?resize=700%2C400&ssl=1 2x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2021\/09\/signup1-150x150.png?resize=1050%2C600&ssl=1 3x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2021\/09\/signup1-150x150.png?resize=1400%2C800&ssl=1 4x"},"classes":[]},{"id":25800,"url":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2023\/09\/14\/splid-2-0-die-zukunft-des-gemeinsamen-ausgabenmanagements\/","url_meta":{"origin":29340,"position":1},"title":"Splid 2.0 &#8211; Die Zukunft des gemeinsamen Ausgabenmanagements","author":"David Christoph Scheifers","date":"14. September 2023","format":false,"excerpt":"Im Rahmen der Vorlesung \u201cSoftware Development for Cloud Computing\u201d haben wir uns daf\u00fcr entschieden, einen Klon der App Splid auf Basis unterschiedlicher Cloud Technologien als Web App zu entwickeln, um uns so die Grundkenntnisse des Cloud Computings anzueignen. Projektidee Bei gemeinsamen Aktivit\u00e4ten und Gruppenausgaben ist es sehr hilfreich, einfache und\u2026","rel":"","context":"In &quot;Allgemein&quot;","block_context":{"text":"Allgemein","link":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/category\/allgemein\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2023\/09\/image6.jpg?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2023\/09\/image6.jpg?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2023\/09\/image6.jpg?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2023\/09\/image6.jpg?resize=700%2C400&ssl=1 2x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2023\/09\/image6.jpg?resize=1050%2C600&ssl=1 3x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2023\/09\/image6.jpg?resize=1400%2C800&ssl=1 4x"},"classes":[]},{"id":28011,"url":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2025\/09\/11\/cloud-native-security-scanner\/","url_meta":{"origin":29340,"position":2},"title":"Cloud-native Security Scanner","author":"Tim Ruff","date":"11. September 2025","format":false,"excerpt":"Dieses Projekt wurde im Rahmen der Vorlesung \u201eSoftware Development for Cloud Computing\u201c umgesetzt. Ausgangslage und Projektidee Unser bisheriger Fokus im Studium lag haupts\u00e4chlich auf Themen der IT-Security und Machine Learning, weshalb wir beide bis auf die grundlegenden Vorlesungen zum Thema Software Entwicklung kaum Erfahrungen in diesem Bereich gesammelt haben. Aus\u2026","rel":"","context":"In &quot;Allgemein&quot;","block_context":{"text":"Allgemein","link":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/category\/allgemein\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2025\/09\/image.jpeg?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2025\/09\/image.jpeg?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2025\/09\/image.jpeg?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2025\/09\/image.jpeg?resize=700%2C400&ssl=1 2x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2025\/09\/image.jpeg?resize=1050%2C600&ssl=1 3x"},"classes":[]},{"id":27142,"url":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2025\/02\/27\/entwicklung-eines-skalierbaren-file-share-services-mit-aws\/","url_meta":{"origin":29340,"position":3},"title":"Entwicklung eines skalierbaren File-Share-Services mit AWS","author":"Max Tyrchan","date":"27. February 2025","format":false,"excerpt":"tl;dr: Unser Semester-Projekt bestand im Aufbau einer skalierbaren File-Share-L\u00f6sung auf AWS auf Basis von NextCloud. Unsere Motivation bestand darin die volle Kontrolle \u00fcber die eigenen Daten zu erlangen, individuelle Anpassbarkeit zu erm\u00f6glichen und eine Kosteneffizienz zu erreichen. Es wurden klare Ziele in den Bereichen Verf\u00fcgbarkeit, Performanz, Sicherheit und Skalierbarkeit definiert,\u2026","rel":"","context":"In &quot;System Engineering&quot;","block_context":{"text":"System Engineering","link":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/category\/system-designs\/system-engineering\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2025\/02\/logo_nextcloud_blue-2.png?resize=350%2C200&ssl=1","width":350,"height":200,"srcset":"https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2025\/02\/logo_nextcloud_blue-2.png?resize=350%2C200&ssl=1 1x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2025\/02\/logo_nextcloud_blue-2.png?resize=525%2C300&ssl=1 1.5x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2025\/02\/logo_nextcloud_blue-2.png?resize=700%2C400&ssl=1 2x, https:\/\/i0.wp.com\/blog.mi.hdm-stuttgart.de\/wp-content\/uploads\/2025\/02\/logo_nextcloud_blue-2.png?resize=1050%2C600&ssl=1 3x"},"classes":[]},{"id":28084,"url":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2025\/09\/14\/springboot-zu-serverless-probleme-und-paradigmen\/","url_meta":{"origin":29340,"position":4},"title":"Springboot zu Serverless: Probleme und Paradigmen","author":"Julian Schniepp","date":"14. September 2025","format":false,"excerpt":"Im Rahmen der Vorlesung \u201eSoftware Development for Cloud\u00a0Computing\u201c sollte jedes Team ein eigenes Cloud\u2011Projekt umsetzen. Unser Projekt Taskflow sollte dabei ein serverloses Todo\u2011Management\u2011System werden. Ziel war es dabei, neue und vor allem industrierelevante Cloud\u2011Technologien praktisch zu erlernen. Der Backend\u2011Teil ist mit Spring\u2011Boot realisiert, welcher \u00fcber AWS\u00a0Lambda und API\u00a0Gateway bereitgestellt modular\u2026","rel":"","context":"In &quot;Allgemein&quot;","block_context":{"text":"Allgemein","link":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/category\/allgemein\/"},"img":{"alt_text":"","src":"","width":0,"height":0},"classes":[]},{"id":12032,"url":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/2020\/09\/30\/admin-panel-web-app-in-der-aws-cloud\/","url_meta":{"origin":29340,"position":5},"title":"Admin Panel (Web App) in der AWS Cloud","author":"ss447","date":"30. September 2020","format":false,"excerpt":"1. Einleitung Im Rahmen der Vorlesung \u201eSoftware Development for Cloud Computing\u201c haben wir uns als Gruppe dazu entschieden aufbauend auf teilweise bereits vorhandener Codebasis an einem Startup-Projekt weiterzuarbeiten. Der Hauptfokus lag bei uns auf dem Ausbau von DevOps-Aspekten und auf dem eines stabilen und sicheren Systems, welches auch in der\u2026","rel":"","context":"In &quot;Cloud Technologies&quot;","block_context":{"text":"Cloud Technologies","link":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/category\/scalable-systems\/cloud-technologies\/"},"img":{"alt_text":"","src":"https:\/\/i0.wp.com\/img.youtube.com\/vi\/qw9ZkWnvR4M\/0.jpg?resize=350%2C200","width":350,"height":200},"classes":[]}],"jetpack_sharing_enabled":true,"authors":[{"term_id":1117,"user_id":1267,"is_guest":0,"slug":"jonas_gehrung","display_name":"Jonas Gehrung","avatar_url":"https:\/\/secure.gravatar.com\/avatar\/923dcc47b11b657fb4ce19332a1f24c43000c4e01cc7046ac8b04e1e12677bf6?s=96&d=mm&r=g","author_category":"","user_url":"","last_name":"Gehrung","first_name":"Jonas","job_title":"","description":""}],"jetpack_featured_media_url":"","_links":{"self":[{"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/posts\/29340","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/users\/1267"}],"replies":[{"embeddable":true,"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/comments?post=29340"}],"version-history":[{"count":44,"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/posts\/29340\/revisions"}],"predecessor-version":[{"id":29440,"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/posts\/29340\/revisions\/29440"}],"wp:attachment":[{"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/media?parent=29340"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/categories?post=29340"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/tags?post=29340"},{"taxonomy":"author","embeddable":true,"href":"https:\/\/blog.mi.hdm-stuttgart.de\/index.php\/wp-json\/wp\/v2\/ppma_author?post=29340"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}