Über den Wolken

Cloud-native entwickeln mit Go

Cloud-native entwickeln mit Go

Über den Wolken

Cloud-native entwickeln mit Go


Go ist die Sprache der Cloud: 14 von 19 Projekten der Cloud Native Computing Foundation im höchsten Reifegrad nutzen Go, darunter Kubernetes und Docker. Go ist blitzschnell, hat starke Typen und Go-Routinen, die all Ihre 64 Cores unter Dampf setzen. Sie lernen in diesem Artikel am Beispiel des Tarifrechners DogOP [1] mit Go Cloud-native zu entwickeln. Also Fallschirm anziehen, jetzt gehts in die Wolken!

Die Cloud liebt Go. Aber warum eigentlich? Go ist rasend schnell, benötigt wenig Hauptspeicher und sorgt für schlanke self-contained Binaries mit wenigen Megabytes. Das sind ideale Eigenschaften für die Cloud. In der Cloud sind Instanzen flüchtig. Jederzeit kann eine neue Instanz gestartet werden, jederzeit kann eine laufende Instanz abgeschossen werden. Dazu müssen Instanzen schnell starten und sofort einsatzbereit sein. Go kann das.

Eine Go-Anwendung startet innerhalb von Millisekunden. So ist es möglich, erst bei der ersten Anfrage an die Anwendung eine Instanz zu starten. Denn die paar Millisekunden zum Hochfahren merkt der Anwender gar nicht. Und bei hoher Last können bei Bedarf jederzeit weitere Instanzen gestartet werden.

Was ist Cloud-Native?

Die Cloud Native Computing Foundation [2] versteht unter Cloud-Native: „Cloud-Native-Technologien befähigen, skalierbare Anwendungen zu entwickeln und betreiben, in modernen, dynamischen Umgebungen.“ Aber was heißt das für mich in der Praxis?

Skalierbar heißt ...

  • Last wird auf zwei, drei oder mehr Instanzen verteilt

  • eine Instanz kann mehr CPU-Ressourcen und Speicher bekommen

  • ist nichts los, läuft auch nichts

Moderne, dynamische Umgebung heißt ...

  • Instanzen sind flüchtig

  • jederzeit kann eine neue Instanz gestartet werden

  • eine laufende Instanz kann immer abgeschlossen werden

  • IP-Adressen sind flüchtig

  • Hostnamen sind flüchtig

  • kein Dateisystem

Bauen wir auf die zwölf Prinzipien der Twelve-Factor-App [3], erhalten wir eine skalierbare Anwendung die in modernen, dynamischen Cloud-Umgebungen läuft. Also eine Cloud-Native-Anwendung.

Aber Performance ist nicht alles, was eine Cloud-Native-Anwendung ausmacht (Kasten: „Was ist Cloud-Native?“). Kevin Hoffmann hat mit der Twelve-Factor-App zwölf Prinzipien [3] für Cloud-Native-Anwendungen identifiziert (Kasten: „Die zwölf Prinzipien der Twelve-Factor-App“). Halten Sie sich daran, gibt es keine Turbulenzen, sondern es scheint die Sonne über Ihren Wolken. Go hilft Ihnen dabei, diese zwölf Prinzipien zu erfüllen. Wie Sie Ihre Go-Anwendung gemäß diesen Prinzipien bauen und was Sie sonst noch beachten müssen, lernen Sie am Beispiel des Tarifrechners DogOP.

Die zwölf Prinzipien der Twelve-Factor-App

  1. Codebasis: Eine Codebasis im Versionsmanagementsystem, aus der viele Deployments erfolgen

  2. Abhängigkeiten: Abhängigkeiten werden explizit deklariert und isoliert

  3. Konfiguration: Die Konfiguration erfolgt über Umgebungsvariablen

  4. Unterstützende Dienste: Unterstützende Dienste wie eine Datenbank werden behandelt wie Dienste von Dritten (z. B. externer Service)

  5. Build, Release, Run: Die Phasen Build, Release und Run werden strikt getrennt; Codeänderungen zur Laufzeit sind nicht zulässig

  6. Prozesse: Die Anwendung wird als ein oder mehrere Prozesse ausgeführt

  7. Bindung an Ports: Dienste durch das Binden von Ports exportieren, wie den HTTP-Port

  8. Nebenläufigkeit und Skalierung: Skaliert wird über das Prozessmodell, d. h. indem weitere Prozesse gestartet werden

  9. Einweggebrauch: Die Prozesse können weggeworfen werden, sie können also schnell gestartet und gestoppt werden

  10. Dev-Prod-Vergleichbarkeit: Entwicklung, Staging und Produktion werden so ähnlich wie möglich gehalten

  11. Logs: Logs werden als Stream von Ereignissen behandelt; jeder Prozess schreibt seine Logs ungepuffert in die Standardausgabe

  12. Adminprozesse: Administrations- und Managementaufgaben werden als einmalige Vorgänge behandelt und als einmalige, kurzlebige Prozesse ausgeführt

Der Tarifrechner DogOP Cloud-native in Go

Wir bauen den Tarifrechner DogOP für die Hunde-OP-Versicherung in Go als Cloud-Native-Anwendung. Die erste Version v0.1 des DogOP-Rechners hat ein REST API und berechnet einen Tarif für die Hunde-OP-Versicherung. Die DogOP-Anwendung wird als Container bereitgestellt und über Umgebungsvariablen konfiguriert.

Die nächste Version v0.2 von DogOP nutzt eine PostgreSQL-Datenbank, um Angebote zu speichern, mit Cloud-konformer Migration des Datenbankschemas. Darauf setzen wir ein REST API für CRUD-Operationen von Angeboten.

Abstürzen aus der Wolke im laufenden Betrieb wollen wir nicht. Also kommen zum Schluss noch Health Check, Logging und Tracing, damit wir auch während des Flugs auf Kurs bleiben. Vor uns liegt ein spannender Flug, also ab ins Cockpit und anschnallen. Wir starten!

Hello DogOP – Projekt aufsetzen

Als Erstes brauchen wir ein Go-Modul crossnative/dog-op für unsere Anwendung. Das Go-Modul enthält den Quellcode der DogOP-Anwendungen und alle Abhängigkeiten auf andere Go-Module, die wir nutzen. Dazu erzeugen wir ein Verzeichnis dogop. Im Verzeichnis dog-op erstellen wir mit go mod init crossnative/dogop das Go-Modul der DogOP-Anwendung.

Jetzt gehts los mit dem Code unserer Anwendung, zumindest fast. Denn uns fehlt noch eine Abhängigkeit: der HTTP-Router chi [4]. Wir nutzen von Beginn an chi und nicht die Go-Standardbibliothek. chi ist kompatibel zur Standardbibliothek, hat aber noch einige Zusatzfeatures, die wir später brauchen. Also holen wir uns den chi-Router mit go get github.com/go-chi/chi/v5.

Die Datei main.go ist der Einstiegspunkt der DogOP-Anwendung, und die erstellen wir nun. In der main.go erzeugen wir eine HTTP-Routerinstanz mit r := chi.NewRouter(). Am HTTP-Router implementieren wir die erste Route für ein HTTP GET auf den Web-Root / mit r.Get("/", func() {...}) (Listing 1). Der zweite Parameter ist eine anonyme Go-Funktion, die mit w.Write([]byte("Hello DogOp!")) einen String in die HTTP Response schreibt.

Eine Go-Funktion, die einen HTTP Request verarbeitet, wird als Handler Function bezeichnet, sie implementiert nämlich das Interface HandlerFunc der Go-Standardbibliothek [5]. Den Webserver der Standardbibliothek starten wir mit http.ListenAndServe(":8080", r) auf Port 8080, der HTTP-Router wird als Parameter übergeben. Den vollständigen Code der main.go zeigt Listing 2.

Listing 1

r.Get("/", func(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte("Hello DogOp!"))
})

Listing 2

package main
 
import (
  "net/http"
  "github.com/go-chi/chi/v5"
)
 
func main() {
  r := chi.NewRouter()
  r.Get("/", func(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello DogOp!"))
  })
  http.ListenAndServe(":8080", r)
}

Projekt bauen und ausführen

Mit go run . starten wir die DogOP-Anwendung. Ein HTTP Request mit curl localhost:8080 liefert das Ergebnis „Hello DogOp!“. Der Go-Compiler kompiliert die Anwendung und führt sie dann aus. Denn eine Go-Anwendung läuft nativ auf der Maschine, und zwar als self-contained Binary. Das Binary ist self-contained, weil es alle Abhängigkeiten im Bauch hat. Zur Laufzeit wird also nur dieses eine Binary benötigt, mehr nicht. Das ist wichtig für die Cloud, und erfüllt Prinzip 2 der Twelve-Factor-App zu Abhängigkeiten. Denn Prinzip 2 besagt, dass eine Cloud-Native-Anwendung nie von etwas abhängig sein darf, das in der Umgebung vorhanden sein muss, wie zum Beispiel ein systemweites Package.

Das Binary bauen wir mit go build -o build/dogop.. Im Verzeichnis build liegt...