React SPA und server-side rendering (SSR) mit AWS Lambda, CloudFront und dem CDK

February 7, 2020

Wir verstehen uns als Berater die einen ganzheitlichen Blick auf Systeme und Architekturen werfen wollen. Mit Angeboten wie AWS Lambda, Amazon DynamoDB oder Amazon API Gateway bauen wir erfolgreich dynamische Systeme, die dank Bausteinen, die serverless bzw. servicefull sind, unkompliziert skalieren.

Oft liest man jedoch, dass diese Bausteine von AWS zwar für ein Backend oder den Betrieb einer API geeignet seien, aber man könne damit nur schwer eine Architektur erstellen um dynamisch generierte Inhalte mit HTML, CSS, JavaScript und vor allem React auszuliefern. Die meisten Projekte mit React finden ihren Anfang durch den Einsatz von create-react-app (abgekürzt: CRA). Der Ansatz von cra-serverless ist eine gute Grundlage um Anwendungen, die mit CRA erstellt wurden, ohne aufwendiges Umbauen mit server-side rendering zu erweitern.

Die Architektur für React SSR mit AWS besteht aus einer Amazon CloudFront Distribution, einer Funktion in AWS Lambda und den statischen Inhalten der React Anwendung in einem Amazon S3 Bucket. Das Amazon API Gateway dient als Proxy für die Lambda Funktion und damit diese unkompliziert im CloudFront CDN konfiguriert werden kann.

Serverless pre-rendering für React mit AWS

Durch die Konfiguration von CloudFront werden Anfragen auf Dateien, die dem Muster /*.* entsprechen, an den S3 Bucket mit den statischen Inhalten weitergeleitet (z.B. /robots.txt, /style.css oder auch /static/images/welcome.svg). Alle weiteren Anfragen mit dem Muster /* werden an die AWS Lambda Funktion geleitet (z.B. /home oder /details/foo/bar) und dort mit dem dynamisch generierten HTML Code beantwortet. Dies geschieht natürlich nur, wenn die Antwort nicht bereits im CDN Cache vorhanden ist.

Damit die Infrastruktur so servicefull wie möglich gestaltet werden kann, sollte man jede Eigenentwicklung tunlichst vermeiden. Nach dem Einsatz von API Gateway und AWS Lambda fehlt nur noch das Renderin von React je nach angefragter URL und Pfad. React bietet diese Funktion in den Tiefen des Frameworks bereits und auch Routing nach URL Pfaden ist ein ebenso gelöstes Problem, Frameworks wie Express oder koa erledigen diese Aufgabe bereits zuverlässig.

Server-Side Rendering mit React

Wie eingangs erwähnt, ist für die meisten Projekte der Einsatz von create-react-app eine gute Ausgangsbasis; so auch für cra-serverless. Der Großteil aller Anwendungen mit React setzen die Bibliothek react-router-dom für die Navigation innerhalb der Anwendung und haben irgendwo in der Codebasis folgende Zeilen Code:

import React from "react";
import { BrowserRouter } from "react-router-dom";

React.render(
  <BrowserRouter>
    <App />
  </BrowserRouter>,
  document.getElementById("root")
);

Wie man an der Namensgebung erahnen kann, dient die Komponente BrowserRouter für das Routing innerhalb eines Web Browsers. Wie auch React an sich, bietet auch react-router-dom bereits alle notwendigen Bausteine um das Rendern ohne Browser zu ermöglichen! Mit dem Einsatz des StaticRouter lässt sich die gesamte Anwendung ohne Web Browser und ohne Umbauten rendern:

import { renderToString } from "react-dom/server";
import { StaticRouter } from "react-router-dom";

const markup = renderToString(
  <StaticRouter location={path}>
    <App />
  </StaticRouter>
);

Damit all dies innerhalb der AWS Lambda Funktion wiederum möglichst ohne Eigenentwicklung genutzt werden kann, nutzt cra-serverless die Bibliotheken koa und serverless-http für die Integration mit dem Amazon API Gateway. Die zusätzlichen Zeilen für die AWS Lambda Funktion beinhaltet also einzig die Bibliothek zum Verarbeiten von HTTP-Requests, das Bauteil für ein Mapping der Eventstruktur des Amazon API Gateway und die genannten Funktionen von React und dem react-router-dom für das Rendering außerhalb eines Web Browsers.

import koa from "koa";
import http from "koa-route";
import { renderToString } from "react-dom/server";
import { StaticRouter } from "react-router-dom";
import serverless from "serverless-http";

import App from "../src/App"; // Pfad zu create-react-app

export const Router = new koa();

Router.use(
  http.get("*", (ctx: koa.Context) => {
    ctx.body = renderToString(
      <StaticRouter location={ctx.request.path}>
        <App />
      </StaticRouter>
    );
  })
);

export const handle = serverless(Router);

Natürlich wäre dieser Ansatz auch ohne ein HTTP Framework realisierbar. Durch den Einsatz von koa oder Express kann der Service für das Pre-rendering von React jedoch auch problem auf dem lokalen Computer genutzt werden.

import { Router } from "./handler";

console.log(`🎉 Starting HTTP server at http://localhost:3000`);

Router.listen(3000);

Mehr ist grundlegend nicht notwendig um eine unmodifizierte React Anwendung die mit create-react-app erstellt wurde um server-side Rendering zu erweitern. Auch Frameworks wie styled-components oder der Apollo Client für den Einsatz von GraphQL mit AWS AppSync lassen sich ohne spezielle Anpassungen außerhalb des Web Browsers nutzen.

Zwischenfazit: Mit diesem Ansatz und den gezeigten Techniken kann folglich der identische und bereits existierende Code einer React Anwendung für serverless pre-rendering genutzt werden. Dank AWS Lambda, Amazon API Gateway, und CloudFront funktioniert der Betrieb ohne etwaige Wartungsaufgaben und dank der pay-per-use Strategie von AWS wird nur in Rechnung gestellt, was auch tatsächlich genutzt wird.

Infrastruktur und Deployments

Für die Verwaltung der Infrastruktur setzt cra-serverless auf das AWS Cloud Development Kit. Mit dem CDK lassen sich Bausteine der Infrastruktur für AWS in diversen Sprachen programmieren und somit verlässlich verwalten und bereitstellen. Zusammen mit AWS CodePipeline und AWS CodeBuild lässt sich somit der gesamte Deployvorgang innerhalb des GitHub repositories abbilden und automatisieren. Durch die Integration von GitHub Webhooks kann die AWS CodePipeline automatisch bei Änderung im Repository erneut ausgeführt werden.

AWS CodePipeline

Damit einzelne Schritte der Pipeline strukturiert werden können, bietet AWS CodePipeline sogenannte Stages die nacheinander durchlaufen werden. Innerhalb einer Stage können mehrere Actions parallel oder sequenziell ausgeführt werden. Die erste Stage einer Pipeline beinhaltet meist das Laden und Bereitstellen des Quellcodes. Das Ergebnis bzw. die Dateien werden danach in einem S3 Bucket abgelegt und sind die Grundlage für die weiteren Stages.

AWS CodePipeline mit GitHub Webhooks

Nachdem der Quellcode im S3 Bucket abgelegt wurde, beginnen die eigentlichen Schritte für das Bauen der Applikation. Die Actions innerhalb dieser Stage nutzen:

Die Action zum Generieren der React Anwendung und die Action für die Generierung des Quellcodes für AWS Lambda müssen hintereinander ablaufen, da die statischen Dateien der React Anwendujng sowohl im finalen S3 Bucket verfügbar sein müssen, wie auch in der AWS Lambda Funktion benötigt werden. Das Generieren der CloudFormation Templates mit dem CDK wird parallel dazu ausgeführt. Die Ergebnisse jeder Action werden für die weitere Verarbeiten wieder in einem S3 Bucket abgelegt.

AWS CodeBuild für serverless pre-rendering mit React

Da nun alle Komponenten der Architektur und Infrastruktur vorbereitet wurden, kann das eigentliche Deployment gestartet werden. Für eine React Anwendung mit server-side Rendering des HTML codes ist hierbei auf die korrekte Reihenfolge zu achten damit es nicht zu einem temporären Ausfall der Applikation kommt!

Die erste Action der Deploy Stage kopiert die generierten statischen Dateien der create-react-app in den S3 Bucket der über Amazon CloudFront erreichbar ist. Danach wird die AWS Lambda Funktion durch das generierte CloudFormation Template mit dem erstellten Quellcode aus dem S3 Bucket aktualisiert und in der letzten Action eventuell das CloudFront CDN aktualisiert bzw. beim ersten Durchlauf der Pipeline angelegt und konfiguriert.

AWS CodePipeline Actions für Deployments einer React SPA mit serverless pre-rendering

Sobald alle drei Actions erfolgreich durchlaufen wurden, wird in der letzten Stage der Pipeline abschließen der bisherige Inhalt im CloudFront CDN invalidiert. So wird sichergestellt dass nun keine veralteten Ressourcen mehr ausgeliefert werden.

Fazit

Für den Einsatz von server-side rendering mit React, auch als SSR abgekürzt, gibt es eine Vielzahl an Frameworks und Lösungen. Durch die Basis aus create-react-app und den bereits existierenden Funktionen diverser zum Einsatz kommender Frameworks, erhöht eine Lösungen wie cra-serverless nur minimal die Komplexität einer bestehenden React Anwendung da kaum Eigenentwicklung beinhaltet ist. Durch den Einsatz von Bausteinen wie AWS Lambda müsste es aber serverless pre-rendering für React genannt werden.

Das cra-serverless Projekt ist auf GitHub zu finden. Falls die gesamte Architektur des Projekts zu komplex erscheinen sein sollte, sind kleinere und losgelöste Beispiele des React Renderings in unserem react-renderer Projekt auf GitHub zu finden. Dieser gesamte Artikel ist zusätzlich auf sbstjn.com in english zu finden und die finale Anwendung kann auf d31tuk9nqnnpkk.cloudfront.net ausprobiert werden.

photo of Sebastian

Sebastian is a Senior Cloud Consultant at superluminar GmbH, AWS Serverless Hero and AWS Certified Solutions Architect. He writes here in German about AWS, Serverless, Software development, Go, TypeScript and React. English Articles can be found on his Website sbstjn.com and he can be found on Twitter under @sbstjn.