Stellen Sie Webanwendungen automatisch mithilfe von GitHub-Webhooks bereit
Veröffentlicht: 2022-03-11Jeder, der Webanwendungen entwickelt und versucht, sie auf seinen eigenen, nicht verwalteten Servern auszuführen, ist sich des langwierigen Prozesses bewusst, der mit der Bereitstellung seiner Anwendung und dem Pushen zukünftiger Updates verbunden ist. Plattform-as-a-Service (PaaS)-Anbieter haben es einfach gemacht, Webanwendungen bereitzustellen, ohne den Prozess der Bereitstellung und Konfiguration einzelner Server durchlaufen zu müssen, im Gegenzug zu einem leichten Anstieg der Kosten und einer Verringerung der Flexibilität. PaaS mag die Dinge einfacher gemacht haben, aber manchmal müssen oder wollen wir Anwendungen immer noch auf unseren eigenen nicht verwalteten Servern bereitstellen. Die Automatisierung dieses Prozesses der Bereitstellung von Webanwendungen auf Ihrem Server mag zunächst überwältigend klingen, aber in Wirklichkeit kann es einfacher sein, ein einfaches Tool zu finden, um dies zu automatisieren, als Sie denken. Wie einfach es sein wird, dieses Tool zu implementieren, hängt stark davon ab, wie einfach Ihre Anforderungen sind, aber es ist sicherlich nicht schwer zu erreichen und kann wahrscheinlich helfen, viel Zeit und Mühe zu sparen, indem Sie die mühsamen, sich wiederholenden Teile der Webanwendung erledigen Einsätze.
Viele Entwickler haben ihre eigenen Wege gefunden, um die Bereitstellungsprozesse ihrer Webanwendungen zu automatisieren. Da die Art und Weise, wie Sie Ihre Webanwendungen bereitstellen, stark vom verwendeten Technologie-Stack abhängt, variieren diese Automatisierungslösungen untereinander. Die Schritte zum automatischen Bereitstellen einer PHP-Website unterscheiden sich beispielsweise von der Bereitstellung einer Node.js-Webanwendung. Es gibt andere Lösungen, wie z. B. Dokku, die ziemlich generisch sind, und diese Dinge (genannt Buildpacks) funktionieren gut mit einer breiteren Palette von Technologiestacks.
In diesem Tutorial werfen wir einen Blick auf die grundlegenden Ideen hinter einem einfachen Tool, das Sie erstellen können, um Ihre Webanwendungsbereitstellungen mit GitHub-Webhooks, Buildpacks und Procfiles zu automatisieren. Der Quellcode des Prototypprogramms, den wir in diesem Artikel untersuchen werden, ist auf GitHub verfügbar.
Erste Schritte mit den Webanwendungen
Um die Bereitstellung unserer Webanwendung zu automatisieren, schreiben wir ein einfaches Go-Programm. Wenn Sie mit Go nicht vertraut sind, zögern Sie nicht, ihm zu folgen, da die in diesem Artikel verwendeten Codekonstrukte ziemlich einfach und leicht verständlich sein sollten. Wenn Sie Lust haben, können Sie das gesamte Programm wahrscheinlich ganz einfach in eine Sprache Ihrer Wahl portieren.
Bevor Sie beginnen, stellen Sie sicher, dass Sie die Go-Distribution auf Ihrem System installiert haben. Um Go zu installieren, können Sie den in der offiziellen Dokumentation beschriebenen Schritten folgen.
Als Nächstes können Sie den Quellcode dieses Tools herunterladen, indem Sie das GitHub-Repository klonen. Dies sollte Ihnen das Nachvollziehen erleichtern, da die Codeausschnitte in diesem Artikel mit den entsprechenden Dateinamen gekennzeichnet sind. Wenn Sie möchten, können Sie es gleich ausprobieren.
Ein großer Vorteil der Verwendung von Go für dieses Programm besteht darin, dass wir es so erstellen können, dass wir nur minimale externe Abhängigkeiten haben. In unserem Fall müssen wir zum Ausführen dieses Programms auf einem Server nur sicherstellen, dass wir Git und Bash installiert haben. Da Go-Programme in statisch gelinkte Binärdateien kompiliert werden, können Sie das Programm auf Ihrem Computer kompilieren, auf den Server hochladen und fast ohne Aufwand ausführen. Für die meisten anderen gängigen Sprachen von heute würde dies eine riesige Laufzeitumgebung oder einen Interpreter erfordern, der auf dem Server installiert ist, nur um Ihren Bereitstellungsautomaten auszuführen. Go-Programme können, wenn sie richtig gemacht werden, auch CPU und RAM sehr schonen - was Sie von Programmen wie diesem erwarten.
GitHub-Webhooks
Mit GitHub-Webhooks ist es möglich, Ihr GitHub-Repository so zu konfigurieren, dass jedes Mal Ereignisse ausgegeben werden, wenn sich etwas im Repository ändert oder ein Benutzer bestimmte Aktionen im gehosteten Repository ausführt. Auf diese Weise können Benutzer diese Ereignisse abonnieren und durch URL-Aufrufe über die verschiedenen Ereignisse benachrichtigt werden, die in Ihrem Repository stattfinden.
Einen Webhook zu erstellen ist sehr einfach:
- Navigieren Sie zur Einstellungsseite Ihres Repositorys
- Klicken Sie im linken Navigationsmenü auf „Webhooks & Dienste“.
- Klicken Sie auf die Schaltfläche „Webhook hinzufügen“.
- Legen Sie eine URL und optional ein Geheimnis fest (mit dem der Empfänger die Nutzlast überprüfen kann).
- Treffen Sie bei Bedarf weitere Auswahlen auf dem Formular
- Senden Sie das Formular ab, indem Sie auf die grüne Schaltfläche „Webhook hinzufügen“ klicken
GitHub bietet eine umfangreiche Dokumentation zu Webhooks und wie sie genau funktionieren, welche Informationen in der Nutzlast als Reaktion auf verschiedene Ereignisse geliefert werden usw. Für die Zwecke dieses Artikels interessieren wir uns besonders für das „Push“-Ereignis, das jedes Mal ausgegeben wird, wenn jemand Pushes zu einem beliebigen Repository-Zweig.
Baupakete
Buildpacks sind heutzutage ziemlich Standard. Mit Buildpacks, die von vielen PaaS-Anbietern verwendet werden, können Sie angeben, wie der Stack konfiguriert wird, bevor eine Anwendung bereitgestellt wird. Das Schreiben von Buildpacks für Ihre Webanwendung ist wirklich einfach, aber meistens findet eine schnelle Suche im Web ein Buildpack, das Sie ohne Änderungen für Ihre Webanwendung verwenden können.
Wenn Sie Anwendungen wie Heroku für PaaS bereitgestellt haben, wissen Sie möglicherweise bereits, was Buildpacks sind und wo Sie sie finden. Heroku hat eine umfassende Dokumentation über die Struktur von Buildpacks und eine Liste einiger gut gebauter beliebter Buildpacks.
Unser Automatisierungsprogramm verwendet ein Kompilierungsskript, um die Anwendung vorzubereiten, bevor es gestartet wird. Beispielsweise analysiert ein von Heroku erstelltes Node.js die Datei package.json, lädt eine geeignete Version von Node.js herunter und lädt NPM-Abhängigkeiten für die Anwendung herunter. Es ist erwähnenswert, dass wir der Einfachheit halber keine umfassende Unterstützung für Buildpacks in unserem Prototypprogramm haben werden. Im Moment gehen wir davon aus, dass Buildpack-Skripte für die Ausführung mit Bash geschrieben wurden und dass sie auf einer neuen Ubuntu-Installation so ausgeführt werden, wie sie ist. Bei Bedarf können Sie dies in Zukunft problemlos erweitern, um weitere esoterische Bedürfnisse zu erfüllen.
Profile
Procfiles sind einfache Textdateien, mit denen Sie die verschiedenen Arten von Prozessen definieren können, die Sie in Ihrer Anwendung haben. Für die meisten einfachen Anwendungen hätten Sie idealerweise einen einzigen „Web“-Prozess, der HTTP-Anforderungen verarbeitet.
Das Schreiben von Procfiles ist einfach. Definieren Sie einen Prozesstyp pro Zeile, indem Sie seinen Namen eingeben, gefolgt von einem Doppelpunkt, gefolgt von dem Befehl, der den Prozess erzeugt:
<type>: <command>
Wenn Sie beispielsweise mit einer Node.js-basierten Webanwendung arbeiten, würden Sie zum Starten des Webservers den Befehl „node index.js“ ausführen. Sie können einfach eine Procfile im Basisverzeichnis des Codes erstellen und sie wie folgt „Procfile“ nennen:
web: node index.js
Wir werden verlangen, dass Anwendungen Prozesstypen in Procfiles definieren, damit wir sie nach dem Einlesen des Codes automatisch starten können.
Umgang mit Ereignissen
In unser Programm müssen wir einen HTTP-Server aufnehmen, der es uns ermöglicht, eingehende POST-Anforderungen von GitHub zu empfangen. Wir müssen einen URL-Pfad zuweisen, um diese Anfragen von GitHub zu verarbeiten. Die Funktion, die diese eingehenden Payloads verarbeitet, sieht etwa so aus:
// hook.go type HookOptions struct { App *App Secret string } func NewHookHandler(o *HookOptions) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { evName := r.Header.Get("X-Github-Event") if evName != "push" { log.Printf("Ignoring '%s' event", evName) return } body, err := ioutil.ReadAll(r.Body) if err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } if o.Secret != "" { ok := false for _, sig := range strings.Fields(r.Header.Get("X-Hub-Signature")) { if !strings.HasPrefix(sig, "sha1=") { continue } sig = strings.TrimPrefix(sig, "sha1=") mac := hmac.New(sha1.New, []byte(o.Secret)) mac.Write(body) if sig == hex.EncodeToString(mac.Sum(nil)) { ok = true break } } if !ok { log.Printf("Ignoring '%s' event with incorrect signature", evName) return } } ev := github.PushEvent{} err = json.Unmarshal(body, &ev) if err != nil { log.Printf("Ignoring '%s' event with invalid payload", evName) http.Error(w, "Bad Request", http.StatusBadRequest) return } if ev.Repo.FullName == nil || *ev.Repo.FullName != o.App.Repo { log.Printf("Ignoring '%s' event with incorrect repository name", evName) http.Error(w, "Bad Request", http.StatusBadRequest) return } log.Printf("Handling '%s' event for %s", evName, o.App.Repo) err = o.App.Update() if err != nil { return } }) }
Wir beginnen mit der Überprüfung des Ereignistyps, der diese Nutzlast generiert hat. Da uns nur das „Push“-Ereignis interessiert, können wir alle anderen Ereignisse ignorieren. Selbst wenn Sie den Webhook so konfigurieren, dass er nur „Push“-Ereignisse ausgibt, gibt es immer noch mindestens eine andere Art von Ereignis, die Sie an Ihrem Hook-Endpunkt erwarten können: „Ping“. Der Zweck dieses Ereignisses besteht darin, festzustellen, ob der Webhook erfolgreich auf GitHub konfiguriert wurde.
Als Nächstes lesen wir den gesamten Text der eingehenden Anfrage, berechnen ihren HMAC-SHA1 unter Verwendung desselben Geheimnisses, das wir zum Konfigurieren unseres Webhook verwenden, und bestimmen die Gültigkeit der eingehenden Nutzdaten, indem wir sie mit der Signatur vergleichen, die im Header der enthalten ist Anfrage. In unserem Programm ignorieren wir diesen Validierungsschritt, wenn das Geheimnis nicht konfiguriert ist. Nebenbei bemerkt, es ist vielleicht keine gute Idee, den gesamten Text zu lesen, ohne zumindest eine Art Obergrenze dafür zu haben, wie viele Daten wir hier behandeln wollen, aber lassen Sie uns die Dinge einfach halten, um uns auf die kritischen Aspekte zu konzentrieren dieses Werkzeugs.
Dann verwenden wir eine Struktur aus der GitHub-Clientbibliothek für Go, um die eingehende Nutzlast zu entpacken. Da wir wissen, dass es sich um ein „Push“-Ereignis handelt, können wir die PushEvent-Struktur verwenden. Anschließend verwenden wir die standardmäßige json-Codierungsbibliothek, um die Nutzlast in eine Instanz der Struktur zu entpacken. Wir führen ein paar Plausibilitätsprüfungen durch, und wenn alles in Ordnung ist, rufen wir die Funktion auf, die mit der Aktualisierung unserer Anwendung beginnt.
Anwendung aktualisieren
Sobald wir eine Ereignisbenachrichtigung an unserem Webhook-Endpunkt erhalten, können wir mit der Aktualisierung unserer Anwendung beginnen. In diesem Artikel werfen wir einen Blick auf eine ziemlich einfache Implementierung dieses Mechanismus, und es wird sicherlich Raum für Verbesserungen geben. Es sollte jedoch etwas sein, mit dem wir mit einem grundlegenden automatisierten Bereitstellungsprozess beginnen können.
Lokales Repository initialisieren
Dieser Prozess beginnt mit einer einfachen Prüfung, um festzustellen, ob dies das erste Mal ist, dass wir versuchen, die Anwendung bereitzustellen. Dazu prüfen wir, ob das lokale Repository-Verzeichnis existiert. Wenn es nicht existiert, initialisieren wir zuerst unser lokales Repository:
// app.go func (a *App) initRepo() error { log.Print("Initializing repository") err := os.MkdirAll(a.repoDir, 0755) // Check err cmd := exec.Command("git", "--git-dir="+a.repoDir, "init") cmd.Stderr = os.Stderr err = cmd.Run() // Check err cmd = exec.Command("git", "--git-dir="+a.repoDir, "remote", "add", "origin", fmt.Sprintf("[email protected]:%s.git", a.Repo)) cmd.Stderr = os.Stderr err = cmd.Run() // Check err return nil }
Diese Methode auf der App-Struktur kann verwendet werden, um das lokale Repository zu initialisieren, und ihre Mechanismen sind extrem einfach:
- Erstellen Sie ein Verzeichnis für das lokale Repository, falls es noch nicht vorhanden ist.
- Verwenden Sie den Befehl „git init“, um ein Bare-Repository zu erstellen.
- Fügen Sie unserem lokalen Repository eine URL für das Remote-Repository hinzu und nennen Sie sie „origin“.
Sobald wir ein initialisiertes Repository haben, sollte das Abrufen von Änderungen einfach sein.
Änderungen abrufen
Um Änderungen aus dem Remote-Repository abzurufen, müssen wir nur einen Befehl aufrufen:
// app.go func (a *App) fetchChanges() error { log.Print("Fetching changes") cmd := exec.Command("git", "--git-dir="+a.repoDir, "fetch", "-f", "origin", "master:master") cmd.Stderr = os.Stderr return cmd.Run() }
Indem wir auf diese Weise einen „Git-Fetch“ für unser lokales Repository durchführen, können wir Probleme vermeiden, bei denen Git in bestimmten Szenarien nicht vorspulen kann. Nicht, dass erzwungene Abrufe etwas sind, auf das Sie sich verlassen sollten, aber wenn Sie einen Force-Push auf Ihr Remote-Repository durchführen müssen, wird dies mit Anmut gehandhabt.

Anwendung kompilieren
Da wir Skripte aus Buildpacks verwenden, um unsere bereitzustellenden Anwendungen zu kompilieren, ist unsere Aufgabe hier relativ einfach:
// app.go func (a *App) compileApp() error { log.Print("Compiling application") _, err := os.Stat(a.appDir) if !os.IsNotExist(err) { err = os.RemoveAll(a.appDir) // Check err } err = os.MkdirAll(a.appDir, 0755) // Check err cmd := exec.Command("git", "--git-dir="+a.repoDir, "--work-tree="+a.appDir, "checkout", "-f", "master") cmd.Dir = a.appDir cmd.Stderr = os.Stderr err = cmd.Run() // Check err buildpackDir, err := filepath.Abs("buildpack") // Check err cmd = exec.Command("bash", filepath.Join(buildpackDir, "bin", "detect"), a.appDir) cmd.Dir = buildpackDir cmd.Stderr = os.Stderr err = cmd.Run() // Check err cacheDir, err := filepath.Abs("cache") // Check err err = os.MkdirAll(cacheDir, 0755) // Check err cmd = exec.Command("bash", filepath.Join(buildpackDir, "bin", "compile"), a.appDir, cacheDir) cmd.Dir = a.appDir cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() }
Wir beginnen mit dem Entfernen unseres vorherigen Anwendungsverzeichnisses (falls vorhanden). Als Nächstes erstellen wir einen neuen und checken den Inhalt des Master-Zweigs dorthin aus. Wir verwenden dann das „detect“-Skript aus dem konfigurierten Buildpack, um festzustellen, ob die Anwendung etwas ist, das wir handhaben können. Dann erstellen wir bei Bedarf ein „Cache“-Verzeichnis für den Buildpack-Kompilierungsprozess. Da dieses Verzeichnis über Builds hinweg bestehen bleibt, kann es vorkommen, dass wir kein neues Verzeichnis erstellen müssen, da bereits eines aus einem früheren Kompilierungsprozess vorhanden ist. An diesem Punkt können wir das „compile“-Skript aus dem Buildpack aufrufen und es alles Notwendige für die Anwendung vor dem Start vorbereiten lassen. Wenn Buildpacks ordnungsgemäß ausgeführt werden, können sie das Caching und die Wiederverwendung von zuvor zwischengespeicherten Ressourcen selbst handhaben.
Anwendung neu starten
Bei unserer Implementierung dieses automatisierten Bereitstellungsprozesses stoppen wir die alten Prozesse, bevor wir mit dem Kompilierungsprozess beginnen, und starten dann die neuen Prozesse, sobald die Kompilierungsphase abgeschlossen ist. Obwohl dies die Implementierung des Tools vereinfacht, bleiben einige potenziell erstaunliche Möglichkeiten zur Verbesserung des automatisierten Bereitstellungsprozesses offen. Um diesen Prototyp zu verbessern, können Sie wahrscheinlich damit beginnen, dass während der Aktualisierungen keine Ausfallzeiten auftreten. Im Moment werden wir mit dem einfacheren Ansatz fortfahren:
// app.go func (a *App) stopProcs() error { log.Print(".. stopping processes") for _, n := range a.nodes { err := n.Stop() if err != nil { return err } } return nil } func (a *App) startProcs() error { log.Print("Starting processes") err := a.readProcfile() if err != nil { return err } for _, n := range a.nodes { err = n.Start() if err != nil { return err } } return nil }
In unserem Prototyp stoppen und starten wir die verschiedenen Prozesse, indem wir über ein Array von Knoten iterieren, wobei jeder Knoten ein Prozess ist, der einer der Instanzen der Anwendung entspricht (wie vor dem Starten dieses Tools auf dem Server konfiguriert). Innerhalb unseres Tools verfolgen wir den aktuellen Status des Prozesses für jeden Knoten. Wir führen auch individuelle Logfiles für sie. Bevor alle Knoten gestartet werden, wird jedem ein eindeutiger Port zugewiesen, beginnend mit einer bestimmten Portnummer:
// node.go func NewNode(app *App, name string, no int, port int) (*Node, error) { logFile, err := os.OpenFile(filepath.Join(app.logsDir, fmt.Sprintf("%s.%d.txt", name, no)), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) if err != nil { return nil, err } n := &Node{ App: app, Name: name, No: no, Port: port, stateCh: make(chan NextState), logFile: logFile, } go func() { for { next := <-n.stateCh if n.State == next.State { if next.doneCh != nil { close(next.doneCh) } continue } switch next.State { case StateUp: log.Printf("Starting process %s.%d", n.Name, n.No) cmd := exec.Command("bash", "-c", "for f in .profile.d/*; do source $f; done; "+n.Cmd) cmd.Env = append(cmd.Env, fmt.Sprintf("HOME=%s", n.App.appDir)) cmd.Env = append(cmd.Env, fmt.Sprintf("PORT=%d", n.Port)) cmd.Env = append(cmd.Env, n.App.Env...) cmd.Dir = n.App.appDir cmd.Stdout = n.logFile cmd.Stderr = n.logFile err := cmd.Start() if err != nil { log.Printf("Process %s.%d exited", n.Name, n.No) n.State = StateUp } else { n.Process = cmd.Process n.State = StateUp } if next.doneCh != nil { close(next.doneCh) } go func() { err := cmd.Wait() if err != nil { log.Printf("Process %s.%d exited", n.Name, n.No) n.stateCh <- NextState{ State: StateDown, } } }() case StateDown: log.Printf("Stopping process %s.%d", n.Name, n.No) if n.Process != nil { n.Process.Kill() n.Process = nil } n.State = StateDown if next.doneCh != nil { close(next.doneCh) } } } }() return n, nil } func (n *Node) Start() error { n.stateCh <- NextState{ State: StateUp, } return nil } func (n *Node) Stop() error { doneCh := make(chan int) n.stateCh <- NextState{ State: StateDown, doneCh: doneCh, } <-doneCh return nil }
Auf den ersten Blick mag dies etwas komplizierter erscheinen als das, was wir bisher getan haben. Um die Dinge leicht verständlich zu machen, lassen Sie uns den obigen Code in vier Teile zerlegen. Die ersten beiden befinden sich innerhalb der Funktion „NewNode“. Wenn es aufgerufen wird, füllt es eine Instanz der „Knoten“-Struktur und erzeugt eine Go-Routine, die dabei hilft, den diesem Knoten entsprechenden Prozess zu starten und zu stoppen. Die anderen beiden sind die beiden Methoden der „Node“-Struktur: „Start“ und „Stop“. Ein Prozess wird gestartet oder gestoppt, indem eine „Nachricht“ durch einen bestimmten Kanal geleitet wird, den diese Pro-Knoten-Go-Routine überwacht. Sie können entweder eine Nachricht übergeben, um den Prozess zu starten, oder eine andere Nachricht, um ihn zu stoppen. Da die eigentlichen Schritte zum Starten oder Stoppen eines Prozesses in einer einzigen Go-Routine stattfinden, gibt es keine Chance, Race Conditions zu erhalten.
Die Go-Routine startet eine Endlosschleife, in der sie auf eine „Nachricht“ durch den „stateCh“-Kanal wartet. Wenn die an diesen Kanal übergebene Nachricht den Knoten auffordert, den Prozess zu starten (in „case StateUp“), verwendet er Bash, um den Befehl auszuführen. Dabei wird der Befehl so konfiguriert, dass er die benutzerdefinierten Umgebungsvariablen verwendet. Es leitet auch Standardausgaben und Fehlerströme in eine vordefinierte Protokolldatei um.
Um andererseits einen Prozess (innerhalb von „case StateDown“) zu stoppen, wird er einfach beendet. An dieser Stelle könnten Sie wahrscheinlich kreativ werden und, anstatt den Prozess sofort zu beenden, ihm ein SIGTERM senden und ein paar Sekunden warten, bevor Sie ihn tatsächlich beenden, um dem Prozess die Möglichkeit zu geben, ordnungsgemäß zu stoppen.
Die Methoden „Start“ und „Stop“ machen es einfach, die entsprechende Nachricht an den Kanal zu übergeben. Im Gegensatz zur „Start“-Methode wartet die „Stop“-Methode tatsächlich darauf, dass die Prozesse beendet werden, bevor sie zurückkehrt. „Start“ übergibt einfach eine Nachricht an den Kanal, um den Prozess zu starten, und kehrt zurück.
Alles kombinieren
Schließlich müssen wir nur noch alles innerhalb der Hauptfunktion des Programms verdrahten. Hier werden wir die Konfigurationsdatei laden und parsen, das Buildpack aktualisieren, versuchen, unsere Anwendung einmal zu aktualisieren, und den Webserver starten, um auf eingehende „Push“-Ereignisnutzlasten von GitHub zu warten:
// main.go func main() { cfg, err := toml.LoadFile("config.tml") catch(err) url, ok := cfg.Get("buildpack.url").(string) if !ok { log.Fatal("buildpack.url not defined") } err = UpdateBuildpack(url) catch(err) // Read configuration options into variables repo (string), env ([]string) and procs (map[string]int) // ... app, err := NewApp(repo, env, procs) catch(err) err = app.Update() catch(err) secret, _ := cfg.Get("hook.secret").(string) http.Handle("/hook", NewHookHandler(&HookOptions{ App: app, Secret: secret, })) addr, ok := cfg.Get("core.addr").(string) if !ok { log.Fatal("core.addr not defined") } err = http.ListenAndServe(addr, nil) catch(err) }
Da Buildpacks einfache Git-Repositories sein müssen, führt „UpdateBuildpack“ (implementiert in buildpack.go) lediglich einen „Git-Klon“ und einen „Git-Pull“ nach Bedarf mit der Repository-URL aus, um die lokale Kopie des Buildpacks zu aktualisieren.
Ausprobieren
Falls Sie das Repository noch nicht geklont haben, können Sie dies jetzt tun. Wenn Sie die Go-Distribution installiert haben, sollte es möglich sein, das Programm sofort zu kompilieren.
mkdir hopper cd hopper export GOPATH=`pwd` go get github.com/hjr265/toptal-hopper go install github.com/hjr265/toptal-hopper
Diese Befehlsfolge erstellt ein Verzeichnis namens hopper, legt es als GOPATH fest, ruft den Code zusammen mit den erforderlichen Go-Bibliotheken von GitHub ab und kompiliert das Programm in eine Binärdatei, die Sie im Verzeichnis „$GOPATH/bin“ finden. Bevor wir dies auf einem Server verwenden können, müssen wir eine einfache Webanwendung erstellen, um dies zu testen. Der Einfachheit halber habe ich eine einfache „Hello, world“-ähnliche Node.js-Webanwendung erstellt und sie in ein anderes GitHub-Repository hochgeladen, das Sie forken und für diesen Test wiederverwenden können. Als nächstes müssen wir die kompilierte Binärdatei auf einen Server hochladen und eine Konfigurationsdatei im selben Verzeichnis erstellen:
# config.tml [core] addr = ":26590" [buildpack] url = "https://github.com/heroku/heroku-buildpack-nodejs.git" [app] repo = "hjr265/hopper-hello.js" [app.env] GREETING = "Hello" [app.procs] web = 1 [hook] secret = ""
Mit der ersten Option in unserer Konfigurationsdatei „core.addr“ können wir den HTTP-Port des internen Webservers unseres Programms konfigurieren. Im obigen Beispiel haben wir es auf „:26590“ gesetzt, wodurch das Programm unter „http://{host}:26590/hook“ auf „push“-Ereignisnutzlasten wartet. Ersetzen Sie beim Einrichten des GitHub-Webhook einfach „{host}“ durch den Domainnamen oder die IP-Adresse, die auf Ihren Server verweist. Stellen Sie sicher, dass der Port geöffnet ist, falls Sie eine Art Firewall verwenden.
Als Nächstes wählen wir ein Buildpack aus, indem wir seine Git-URL festlegen. Hier verwenden wir das Node.js-Buildpack von Heroku.
Unter „app“ setzen wir „repo“ auf den vollständigen Namen Ihres GitHub-Repositorys, das den Anwendungscode hostet. Da ich die Beispielanwendung unter „https://github.com/hjr265/hopper-hello.js“ hoste, lautet der vollständige Name des Repositorys „hjr265/hopper-hello.js“.
Dann legen wir einige Umgebungsvariablen für die Anwendung und die Anzahl der benötigten Prozesstypen fest. Und schließlich wählen wir ein Geheimnis aus, damit wir eingehende „Push“-Ereignisnutzlasten überprüfen können.
Wir können jetzt unser Automatisierungsprogramm auf dem Server starten. Wenn alles richtig konfiguriert ist (einschließlich der Bereitstellung von SSH-Schlüsseln, damit das Repository vom Server aus zugänglich ist), sollte das Programm den Code abrufen, die Umgebung mithilfe des Buildpacks vorbereiten und die Anwendung starten. Jetzt müssen wir nur noch einen Webhook im GitHub-Repository einrichten, um Push-Ereignisse auszugeben und auf „http://{host}:26590/hook“ zu verweisen. Stellen Sie sicher, dass Sie „{host}“ durch den Domainnamen oder die IP-Adresse ersetzen, die auf Ihren Server verweist.
Um es abschließend zu testen, nehmen Sie einige Änderungen an der Beispielanwendung vor und übertragen Sie sie auf GitHub. Sie werden feststellen, dass das Automatisierungstool sofort aktiv wird und das Repository auf dem Server aktualisiert, die Anwendung kompiliert und neu startet.
Fazit
Aus den meisten unserer Erfahrungen können wir sagen, dass dies etwas sehr Nützliches ist. Die Prototyp-Anwendung, die wir in diesem Artikel vorbereitet haben, ist möglicherweise nichts, was Sie so wie sie ist auf einem Produktionssystem verwenden möchten. Es gibt eine Menge Raum für Verbesserungen. Ein Tool wie dieses sollte eine bessere Fehlerbehandlung haben, ordnungsgemäßes Herunterfahren/Neustarten unterstützen, und Sie möchten möglicherweise etwas wie Docker verwenden, um die Prozesse einzudämmen, anstatt sie direkt auszuführen. Es kann klüger sein, herauszufinden, was genau Sie für Ihren speziellen Fall benötigen, und sich dafür ein Automatisierungsprogramm auszudenken. Oder verwenden Sie vielleicht eine andere, viel stabilere, bewährte Lösung, die im gesamten Internet verfügbar ist. Aber falls Sie etwas sehr Individuelles einführen möchten, hoffe ich, dass dieser Artikel Ihnen dabei hilft und zeigt, wie viel Zeit und Mühe Sie möglicherweise langfristig sparen können, indem Sie den Bereitstellungsprozess Ihrer Webanwendung automatisieren.