Serverless kaskadiert
In unseren modernen Cloud-affinen Zeiten gehen viele Anwendungsfälle in Richtung Serverless Computing.
Auch bei unseren trion-internen Systemen setzen wir zunehmend darauf, u.a. im Kontext Kommunikationskanäle von und zu Kunden.
Die Gründe für den Einsatz von Serverless Computing sind naheliegend:
-
Keine Verwaltungskosten und Aufwand für Aufbau / Betrieb der Infrastruktur
-
Schnelles, effizientes Deployment / Release durch optimiertes Toolset des Cloud-Anbieters
-
Infrastrukturaspekte wie Monitoring, Logging sind Systembestandteile des Cloud-Anbieters
-
Lastspitzen können automatisch skaliert werden
-
Hochverfügbarkeit der Dienste ist gewährleistet
Bei der Auswahl des Cloud-Anbieters sind wir bei Cloudflare Workers fündig geworden.
Beispiel: Kaskadierte Services
Im Folgenden werden wir anhand eines kleinen explorativen Beispiels eine serverless Servicekaskade aufbauen:
-
Ein Cloudflare-Worker nimmt HTTP-Requests entgegen
-
Aus dem Cloudflare-Worker heraus wird mittels Mailgun eine Beispiel-Mail versendet (Mailgun bietet entsprechende serverless Mailing-Dienste).
Warum diese Kaskade?
-
Der Worker soll möglichst ohne lange Initialisierungsphase (kein Mailing-Subsystem!) auskommen und entsprechend instantan Requests verarbeiten können.
-
Der Worker bleibt schlank und überschaubar. Also einfachere Wartung, geringere Entwicklungszeiten, kürzere Releasezyklen, etc.
Das Setup
Unser Worker soll als Typescript-Projekt aufgesetzt werden. Also benötigen wir passende Tools (insbesondere npm, nodejs, Cloudflare Worker CLI.
Eine minimalinvasive Möglichkeit, aktuelles npm und nodejs auf ein Linuxsystem zu bringen ist die Verwendung Node Version Managers nvm.
$ curl -o- \
https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.1/install.sh \
| bash
$ . ~/.bashrc
$ nvm ls-remote
$ nvm install v16.14.0
$ whereis npm
npm: <USER_HOME>/.nvm/versions/node/v16.14.0/bin/npm
$ node -v v16.14.0
$ npm -v
8.3.1
Die Worker CLI heisst wrangler und wird mittels npm installiert:
$ npm install -g @cloudflare/wrangler
$ ls ~/.nvm/versions/node/v16.14.0/lib/ \
node_modules/@cloudflare/wrangler/
$ wrangler --version
wrangler 1.19.8
Die Authentifizierung / Autorisierung des Workers basiert auf OAuth. OAuth ist nicht ganz trivial. Zum Glück holt uns Wrangler - nachdem man ein passende Konto angelegt hat - einen Zugriffstoken automatisiert aus der Cloud und legt diesen lokal ab:
$ wrangler login
$ cat ~/.wrangler/config/default.toml
oauth_token = "<bearer_toker>"
refresh_token = "<refresh_token>"
expiration_time = "<date>"
$ wrangler whoami
...
+----------------+---------------+
| Account Name | Account ID |
+----------------+---------------+
| <accountname> | <accountid> |
+----------------+---------------+
Das Projekt
Es ist heutzutage üblich, mit CLI-Werkzeugen anhand von Templates Projektverzeichnisse mit lauffähigem Code generieren zu lassen. Bei wrangler ist dies nicht anders. Standardmässig wird ein reines Javascript Projekt generiert. Da der Autor Typisierung favorisiert, wird stattdessen ein TypeScript Projekt generiert:
$ wrangler generate mailing-worker \
https://github.com/cloudflare/worker-typescript-template
$ ls mailing-worker
jestconfig.json LICENSE_APACHE LICENSE_MIT
package.json package-lock.json README.md
src test
tsconfig.json webpack.config.js wrangler.toml
$ nano wrangler.toml
<Wichtig: account_id (s.o.) setzen!>
Neben Javascript und Typescript werden von Cloudflare Workers übrigens diverse andere Sprachen unterstützt (u.a. Rust, Python, Kotlin, Cobol (sic!)).
Lokales Deployment
Das Projekt stellt initial einen (sehr einfachen) Request-Listener zu Verfügung:
import { handleRequest } from './handler'
addEventListener('fetch', (event) => {
event.respondWith(handleRequest(event.request))
})
Im Handler erfolgt lediglich eine einfache Response-Promise:
export async function handleRequest(request: Request):
Promise<Response> {
return new Response(`request method: ${request.method}`)
}
Mit Wrangler kann man das Projekt builden (unter der Haube wird dazu natürlich npm run build
verwendet) und (nodejs) ausführen lassen:
$ wrangler dev &
$ http :8787
[2022-02-11 13:25:26] GET ... HTTP/1.1 200 OK
...
request method: GET
Als http-Tool wird hier übrigens statt cURL das "bequemere" httpie verwendet. Dies ist typischerweise auf Linuxsystemen als Paket verfügbar.
Publizieren des Workers
Optional kann man in der Konfigurationsdatei wrangler.toml
webpack als Packer eintragen:
name = "mailing-worker"
type = "webpack"
zone_id = ""
account_id = "<account-id>"
route = ""
workers_dev = true
compatibility_date = "2022-02-11"
[build]
command = "npm install && npm run build"
[build.upload]
format = "service-worker"
Danach kann man in Richtung Cloud publizieren; da der Worker noch nicht existiert wird er in diesem Zuge initial bei Cloudflare mit einer dort konfigurierten URL angelegt (das kann man natürlich ggf. kostenplichtig auf seine eigene Produktiv-Domäne adaptieren):
$ wrangler publish
$ http https://mailing-worker.workerpoc.workers.dev
[2022-02-11 13:25:26] GET ... HTTP/1.1 200 OK
...
request method: GET
Das Ergebnis kann man sich in der Cloudflare Dash ausschauen:
$ x-www-browser https://dash.cloudflare.com/
Instantiierung des Mail-Services
Um den Mailgun Service zu erzeugen ist zunächst ein (kostenfreies) Konto anzulegen.
Ohne weitere (ggf. kostenpflichtige) Konfiguration kann der Dienst in einer Sandbox (mit nur einer Zielmailadresse!) verwendet werden.
Dazu ist ein API-Key und eine API-Base-URL notwendig, die man sich von der Mailgun Dash holen kann.
Ein Beispielzugriff / Mailtransfer kann folgendermassen aussehen:
curl -s --user 'api:${MAILGUN_API_KEY}' \
${MAILGUN_API_BASE_URL}/messages \
-F from='[email protected]' \
-F to='Christian Bittner <[email protected]>' \
-F subject='Eine Testmail' \
-F text='Dies ist eine Mailgun-Testmail!'
Anbindung von Mailgun
Die Mailgun Zugriffsdaten sollten nicht in Quellcode auftauchen. Wrangler kann diese "geheim" ablegen und im Quellcode als Umgebungsvariable verfügbar machen:
$ wrangler secret put MAILGUN_API_KEY
$ wrangler secret put MAILGUN_API_BASE_URL
Eine exemplarische Handler Implementierung kann wie folgt aussehen:
declare const MAILGUN_API_BASE_URL: string;
declare const MAILGUN_API_KEY: string;
interface MailData {
from: string;
to: string;
subject: string;
text: string;
}
function urlEncodeObject(obj: {[s: string]: any}) {
return Object.keys(obj)
.map(k => encodeURIComponent(k) + "="
+ encodeURIComponent(obj[k]))
.join("&");
}
function sendMail(data: MailData) {
const dataUrlEncoded = urlEncodeObject(data);
const opts = {
method: "POST",
headers: {
Authorization: "Basic " +
btoa("api:" + MAILGUN_API_KEY),
"Content-Type": "application/x-www-form-urlencoded",
"Content-Length": dataUrlEncoded.length.toString()
},
body: dataUrlEncoded,
}
return fetch(`${MAILGUN_API_BASE_URL}/messages`, opts);
}
export async function handleRequest(request: Request):
Promise<Response> {
const mailData: MailData = {
from: "[email protected]",
to: "[email protected]",
subject: "Mailgun Test",
text: "Dies ist eine Cloudflare/Mailgun Testmail!"
}
return sendMail(mailData);
}
Mails können dann entsprechend mit einem Worker gesendet werden!
$ npm run build
$ wrangler dev &
$ http :8787
HTTP/1.1 200 OK
...
{
"id": "<sandboxid>.mailgun.org>",
"message": "Queued. Thank you."
}
$ wrangler publish
$ http https://mailing-worker.workerpoc.workers.dev
HTTP/1.1 200 OK
...
{
"id": "<sandboxid>.mailgun.org>",
"message": "Queued. Thank you."
}
Fazit
-
Kleine Codestrecke
-
Geringer Aufwand
-
Hochverfügbares, skalierbares System
-
Der Anwendungsfall muss passen!
Zu den Themen Typescript, Webanwendungen, Angular (und vielen anderen) bieten wir sowohl Beratung, Entwicklungsunterstützung als auch passende Schulungen an:
Auch für Ihren individuellen Bedarf können wir Workshops und Schulungen anbieten. Sprechen Sie uns gerne an.