Automatycznie wdrażaj aplikacje internetowe za pomocą GitHub Webhooks
Opublikowany: 2022-03-11Każdy, kto tworzy aplikacje internetowe i próbuje uruchamiać je na własnych niezarządzanych serwerach, zdaje sobie sprawę z żmudnego procesu związanego z wdrażaniem aplikacji i wypychaniem przyszłych aktualizacji. Dostawcy platformy jako usługi (PaaS) ułatwili wdrażanie aplikacji internetowych bez konieczności przechodzenia przez proces udostępniania i konfigurowania poszczególnych serwerów, w zamian za niewielki wzrost kosztów i spadek elastyczności. PaaS może ułatwił sprawę, ale czasami nadal musimy lub chcemy wdrażać aplikacje na naszych własnych niezarządzanych serwerach. Automatyzacja tego procesu wdrażania aplikacji internetowych na serwerze może początkowo wydawać się przytłaczająca, ale w rzeczywistości wymyślenie prostego narzędzia do automatyzacji może być łatwiejsze niż myślisz. To, jak łatwe będzie wdrożenie tego narzędzia, zależy w dużej mierze od tego, jak proste są Twoje potrzeby, ale z pewnością nie jest to trudne do osiągnięcia i prawdopodobnie może pomóc zaoszczędzić wiele czasu i wysiłku, wykonując żmudne, powtarzalne części aplikacji internetowej wdrożenia.
Wielu programistów wymyśliło własne sposoby automatyzacji procesów wdrażania swoich aplikacji internetowych. Ponieważ sposób wdrażania aplikacji internetowych zależy w dużej mierze od używanego stosu technologicznego, te rozwiązania automatyzacji różnią się między sobą. Na przykład kroki związane z automatycznym wdrażaniem witryny PHP różnią się od wdrażania aplikacji sieci Web Node.js. Istnieją inne rozwiązania, takie jak Dokku, które są dość ogólne i te rzeczy (nazywane pakietami budowania) działają dobrze z szerszym zakresem stosu technologicznego.
W tym samouczku przyjrzymy się podstawowym pomysłom stojącym za prostym narzędziem, które można zbudować, aby zautomatyzować wdrażanie aplikacji internetowych za pomocą elementów webhook, pakietów buildpack i Procfiles GitHub. Kod źródłowy programu prototypowego, który omówimy w tym artykule, jest dostępny w serwisie GitHub.
Pierwsze kroki z aplikacjami internetowymi
Aby zautomatyzować wdrażanie naszej aplikacji internetowej, napiszemy prosty program Go. Jeśli nie znasz Go, nie wahaj się kontynuować, ponieważ konstrukcje kodu użyte w tym artykule są dość proste i powinny być łatwe do zrozumienia. Jeśli masz na to ochotę, prawdopodobnie możesz dość łatwo przenieść cały program na wybrany przez siebie język.
Przed rozpoczęciem upewnij się, że masz zainstalowaną dystrybucję Go w swoim systemie. Aby zainstalować Go, wykonaj kroki opisane w oficjalnej dokumentacji.
Następnie możesz pobrać kod źródłowy tego narzędzia, klonując repozytorium GitHub. Powinno to ułatwić śledzenie, ponieważ fragmenty kodu w tym artykule są oznaczone odpowiednimi nazwami plików. Jeśli chcesz, możesz od razu to wypróbować.
Jedną z głównych zalet korzystania z Go w tym programie jest to, że możemy go zbudować w sposób, w którym mamy minimalne zależności zewnętrzne. W naszym przypadku, aby uruchomić ten program na serwerze wystarczy upewnić się, że mamy zainstalowane Git i Bash. Ponieważ programy Go są kompilowane do statycznie powiązanych plików binarnych, możesz skompilować program na swoim komputerze, przesłać go na serwer i uruchomić bez prawie żadnego wysiłku. W przypadku większości innych popularnych obecnie języków wymagałoby to zainstalowania na serwerze jakiegoś mamuta środowiska wykonawczego lub interpretera tylko po to, aby uruchomić automat wdrażania. Programy Go, gdy są zrobione poprawnie, mogą być bardzo łatwe w użyciu procesora i pamięci RAM – czego oczekujesz od programów takich jak ten.
Webhooki GitHub
Dzięki GitHub Webhooks można skonfigurować repozytorium GitHub tak, aby emitowało zdarzenia za każdym razem, gdy coś się zmieni w repozytorium lub jakiś użytkownik wykona określone działania na hostowanym repozytorium. Dzięki temu użytkownicy mogą subskrybować te zdarzenia i otrzymywać powiadomienia poprzez wywołania adresów URL o różnych zdarzeniach, które mają miejsce w Twoim repozytorium.
Tworzenie webhooka jest bardzo proste:
- Przejdź do strony ustawień swojego repozytorium
- Kliknij „Webhooki i usługi” w lewym menu nawigacyjnym
- Kliknij przycisk „Dodaj webhooka”
- Ustaw adres URL i opcjonalnie klucz tajny (co pozwoli odbiorcy zweryfikować ładunek)
- W razie potrzeby dokonaj innych wyborów w formularzu
- Prześlij formularz, klikając zielony przycisk „Dodaj webhooka”
GitHub udostępnia obszerną dokumentację dotyczącą webhooków i tego, jak dokładnie działają, jakie informacje są dostarczane w ładunku w odpowiedzi na różne zdarzenia itp. Na potrzeby tego artykułu szczególnie interesuje nas zdarzenie „push”, które jest emitowane za każdym razem, gdy ktoś wypycha do dowolnej gałęzi repozytorium.
Pakiety budowania
Buildpacki są w dzisiejszych czasach dość standardowe. Używane przez wielu dostawców PaaS pakiety kompilacji umożliwiają określenie sposobu konfiguracji stosu przed wdrożeniem aplikacji. Pisanie buildpacków dla twojej aplikacji webowej jest naprawdę łatwe, ale często szybkie wyszukiwanie w sieci może ci znaleźć buildpack, którego możesz użyć dla twojej aplikacji webowej bez żadnych modyfikacji.
Jeśli wdrożyłeś aplikację w PaaS, taką jak Heroku, być może wiesz już, czym są buildpacki i gdzie je znaleźć. Heroku posiada obszerną dokumentację na temat struktury buildpacków oraz listę kilku dobrze zbudowanych popularnych buildpacków.
Nasz program do automatyzacji użyje skryptu kompilacji do przygotowania aplikacji przed jej uruchomieniem. Na przykład kompilacja Node.js przez Heroku analizuje plik package.json, pobiera odpowiednią wersję Node.js i pobiera zależności NPM dla aplikacji. Warto zauważyć, że dla uproszczenia nie będziemy mieć obszernego wsparcia dla pakietów budujących w naszym programie prototypowym. Na razie założymy, że skrypty buildpack są napisane do uruchamiania z Bash i będą działać na świeżej instalacji Ubuntu. W razie potrzeby możesz łatwo rozszerzyć to w przyszłości, aby zaspokoić bardziej ezoteryczne potrzeby.
Profile
Procfiles to proste pliki tekstowe, które pozwalają zdefiniować różne typy procesów, które masz w swojej aplikacji. W przypadku większości prostych aplikacji idealnie byłoby mieć pojedynczy proces „sieciowy”, który byłby procesem obsługującym żądania HTTP.
Pisanie profili jest łatwe. Zdefiniuj jeden typ procesu w wierszu, wpisując jego nazwę, dwukropek, a następnie polecenie, które wywoła proces:
<type>: <command>
Na przykład, jeśli pracujesz z aplikacją internetową opartą na Node.js, aby uruchomić serwer WWW, wykonaj polecenie „node index.js”. Możesz po prostu utworzyć Procfile w podstawowym katalogu kodu i nazwać go „Procfile” w następujący sposób:
web: node index.js
Będziemy wymagać od aplikacji, aby definiowały typy procesów w Procfiles, abyśmy mogli uruchamiać je automatycznie po wciągnięciu kodu.
Obsługa wydarzeń
W naszym programie musimy uwzględnić serwer HTTP, który pozwoli nam odbierać przychodzące żądania POST z GitHub. Będziemy musieli wyznaczyć ścieżkę URL, aby obsłużyć te żądania z GitHub. Funkcja, która będzie obsługiwać te przychodzące ładunki, będzie wyglądać mniej więcej tak:
// 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 } }) }
Zaczynamy od sprawdzenia typu zdarzenia, które wygenerowało ten ładunek. Ponieważ interesuje nas tylko wydarzenie „push”, możemy zignorować wszystkie inne wydarzenia. Nawet jeśli skonfigurujesz webhook tak, aby emitował tylko zdarzenia „push”, nadal będzie co najmniej jeden inny rodzaj zdarzenia, którego możesz się spodziewać w punkcie końcowym haka: „ping”. Celem tego zdarzenia jest ustalenie, czy element webhook został pomyślnie skonfigurowany w serwisie GitHub.
Następnie odczytujemy całą treść przychodzącego żądania, obliczamy jego HMAC-SHA1 przy użyciu tego samego klucza tajnego, którego użyjemy do skonfigurowania naszego webhooka, i określimy ważność przychodzącego ładunku, porównując go z podpisem zawartym w nagłówku prośba. W naszym programie ignorujemy ten krok sprawdzania poprawności, jeśli sekret nie jest skonfigurowany. Na marginesie, może nie być mądrym pomysłem czytanie całego ciała bez przynajmniej jakiegoś górnego limitu tego, z jaką ilością danych będziemy chcieli się tutaj zajmować, ale zachowajmy prostotę, aby skupić się na krytycznych aspektach tego narzędzia.
Następnie używamy struktury z biblioteki klienta GitHub dla Go, aby rozpakować przychodzący ładunek. Ponieważ wiemy, że jest to zdarzenie „push”, możemy użyć struktury PushEvent. Następnie używamy standardowej biblioteki kodowania json, aby przenieść ładunek do instancji struktury. Przeprowadzamy kilka testów poprawności i jeśli wszystko jest w porządku, wywołujemy funkcję, która rozpoczyna aktualizację naszej aplikacji.
Aktualizacja aplikacji
Gdy otrzymamy powiadomienie o zdarzeniu w naszym punkcie końcowym webhooka, możemy rozpocząć aktualizację naszej aplikacji. W tym artykule przyjrzymy się dość prostej implementacji tego mechanizmu i na pewno będzie miejsce na ulepszenia. Jednak powinno to być coś, co pozwoli nam zacząć od podstawowego zautomatyzowanego procesu wdrażania.
Inicjowanie lokalnego repozytorium
Ten proces rozpocznie się od prostego sprawdzenia, czy po raz pierwszy próbujemy wdrożyć aplikację. Zrobimy to sprawdzając, czy istnieje lokalny katalog repozytorium. Jeśli nie istnieje, najpierw zainicjujemy nasze lokalne repozytorium:
// 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 }
Ta metoda w strukturze App może być użyta do zainicjowania lokalnego repozytorium, a jej mechanizmy są niezwykle proste:
- Utwórz katalog dla lokalnego repozytorium, jeśli nie istnieje.
- Użyj polecenia „git init”, aby utworzyć czyste repozytorium.
- Dodaj adres URL zdalnego repozytorium do naszego lokalnego repozytorium i nazwij go „origin”.
Gdy mamy już zainicjowane repozytorium, pobieranie zmian powinno być proste.
Pobieranie zmian
Aby pobrać zmiany ze zdalnego repozytorium, wystarczy wywołać jedno polecenie:
// 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() }
Wykonując w ten sposób „git fetch” dla naszego lokalnego repozytorium, możemy uniknąć problemów z Git, który nie może szybko przewijać do przodu w niektórych scenariuszach. Nie chodzi o to, że wymuszone pobieranie jest czymś, na czym powinieneś polegać, ale jeśli musisz wykonać wymuszone wypychanie do zdalnego repozytorium, poradzi sobie z tym z gracją.

Kompilacja aplikacji
Ponieważ do kompilacji wdrażanych aplikacji używamy skryptów z pakietów budujących, nasze zadanie jest stosunkowo proste:
// 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() }
Zaczynamy od usunięcia naszego poprzedniego katalogu aplikacji (jeśli istnieje). Następnie tworzymy nowy i pobieramy do niego zawartość gałęzi master. Następnie używamy skryptu „wykryj” ze skonfigurowanego pakietu, aby określić, czy aplikacja jest czymś, z czym sobie poradzimy. Następnie w razie potrzeby tworzymy katalog „cache” dla procesu kompilacji buildpack. Ponieważ ten katalog jest zachowywany w różnych kompilacjach, może się zdarzyć, że nie będziemy musieli tworzyć nowego katalogu, ponieważ już istnieje z poprzedniego procesu kompilacji. W tym momencie możemy wywołać skrypt „kompilacji” z pakietu buildpack i zlecić mu przygotowanie wszystkiego, co niezbędne dla aplikacji przed uruchomieniem. Gdy pakiety budowania działają poprawnie, mogą samodzielnie obsłużyć buforowanie i ponowne wykorzystanie wcześniej zbuforowanych zasobów.
Ponowne uruchamianie aplikacji
W naszej implementacji tego zautomatyzowanego procesu wdrażania zamierzamy zatrzymać stare procesy przed rozpoczęciem procesu kompilacji, a następnie uruchomić nowe procesy po zakończeniu fazy kompilacji. Chociaż ułatwia to wdrożenie narzędzia, pozostawia kilka potencjalnie niesamowitych sposobów ulepszenia zautomatyzowanego procesu wdrażania. Aby ulepszyć ten prototyp, prawdopodobnie możesz zacząć od zapewnienia zerowego czasu przestoju podczas aktualizacji. Na razie będziemy kontynuować prostsze podejście:
// 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 }
W naszym prototypie zatrzymujemy i uruchamiamy różne procesy, iterując po tablicy węzłów, gdzie każdy węzeł jest procesem odpowiadającym jednej z instancji aplikacji (zgodnie z konfiguracją przed uruchomieniem tego narzędzia na serwerze). W naszym narzędziu śledzimy aktualny stan procesu dla każdego węzła. Prowadzimy dla nich również indywidualne pliki dziennika. Zanim wszystkie węzły zostaną uruchomione, każdemu przypisywany jest unikalny port począwszy od podanego numeru portu:
// 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 }
Na pierwszy rzut oka może się to wydawać nieco bardziej skomplikowane niż to, co zrobiliśmy do tej pory. Aby ułatwić zrozumienie, podzielmy powyższy kod na cztery części. Pierwsze dwa znajdują się w funkcji „NewNode”. Po wywołaniu wypełnia instancję struktury „Node” i tworzy procedurę Go, która pomaga rozpocząć i zatrzymać proces odpowiadający temu węzłowi. Pozostałe dwie to dwie metody na strukturze „Node”: „Start” i „Stop”. Proces jest uruchamiany lub zatrzymywany przez przekazanie „wiadomości” przez określony kanał, na którym czuwa ta procedura Go dla każdego węzła. Możesz przekazać wiadomość, aby rozpocząć proces, lub inną wiadomość, aby go zatrzymać. Ponieważ faktyczne kroki związane z uruchamianiem lub zatrzymywaniem procesu odbywają się w ramach pojedynczej procedury Go, nie ma szans na uzyskanie warunków wyścigu.
Procedura Go rozpoczyna nieskończoną pętlę, w której czeka na „wiadomość” przez kanał „stateCh”. Jeśli wiadomość przekazana do tego kanału żąda od węzła rozpoczęcia procesu (wewnątrz „case StateUp”), do wykonania polecenia używa Bash. Robiąc to, konfiguruje polecenie tak, aby używało zmiennych środowiskowych zdefiniowanych przez użytkownika. Przekierowuje również standardowe wyjścia i strumienie błędów do wstępnie zdefiniowanego pliku dziennika.
Z drugiej strony, aby zatrzymać proces (wewnątrz „case StateDown”), po prostu go zabija. W tym miejscu prawdopodobnie możesz być kreatywny i zamiast zabijać proces, natychmiast wyślij mu SIGTERM i poczekaj kilka sekund, zanim faktycznie go zabijesz, dając procesowi szansę na zatrzymanie się z wdziękiem.
Metody „Start” i „Stop” ułatwiają przekazanie odpowiedniego komunikatu do kanału. W przeciwieństwie do metody „Start”, metoda „Stop” faktycznie czeka na zabicie procesów przed powrotem. „Start” po prostu przekazuje wiadomość do kanału, aby rozpocząć proces i wraca.
Łącząc to wszystko
Na koniec wszystko, co musimy zrobić, to połączyć wszystko w ramach głównej funkcji programu. Tutaj załadujemy i przeanalizujemy plik konfiguracyjny, zaktualizujemy buildpack, spróbujemy raz zaktualizować naszą aplikację i uruchomimy serwer sieciowy, aby nasłuchiwać nadchodzących ładunków zdarzeń „push” z GitHub:
// 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) }
Ponieważ wymagamy, aby pakiety buildpack były prostymi repozytoriami Git, „UpdateBuildpack” (zaimplementowany w buildpack.go) w razie potrzeby wykonuje jedynie „klon git” i „pobieranie git” z adresem URL repozytorium, aby zaktualizować lokalną kopię pakietu.
Wypróbowanie
Jeśli nie sklonowałeś jeszcze repozytorium, możesz to zrobić teraz. Jeśli masz zainstalowaną dystrybucję Go, skompilowanie programu powinno być możliwe od razu.
mkdir hopper cd hopper export GOPATH=`pwd` go get github.com/hjr265/toptal-hopper go install github.com/hjr265/toptal-hopper
Ta sekwencja poleceń utworzy katalog o nazwie hopper, ustawi go jako GOPATH, pobierze kod z GitHub wraz z niezbędnymi bibliotekami Go i skompiluje program do pliku binarnego, który można znaleźć w katalogu „$GOPATH/bin”. Zanim będziemy mogli użyć tego na serwerze, musimy stworzyć prostą aplikację internetową, aby to przetestować. Dla wygody stworzyłem prostą aplikację webową Node.js typu „Hello, world” i przesłałem ją do innego repozytorium GitHub, które możesz wykorzystać do tego testu. Następnie musimy wgrać skompilowany plik binarny na serwer i utworzyć plik konfiguracyjny w tym samym katalogu:
# 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 = ""
Pierwsza opcja w naszym pliku konfiguracyjnym, „core.addr”, pozwala nam skonfigurować port HTTP wewnętrznego serwera WWW naszego programu. W powyższym przykładzie ustawiliśmy go na „:26590”, co spowoduje, że program będzie nasłuchiwał ładunków zdarzeń „push” na „http://{host}:26590/hook”. Podczas konfigurowania webhooka GitHub po prostu zastąp „{host}” nazwą domeny lub adresem IP, który wskazuje na Twój serwer. Upewnij się, że port jest otwarty na wypadek korzystania z zapory.
Następnie wybieramy buildpack, ustawiając jego adres URL Git. Tutaj używamy buildpack Node.js Heroku.
W sekcji „aplikacja” ustawiamy „repo” na pełną nazwę repozytorium GitHub, w którym znajduje się kod aplikacji. Ponieważ hostuję przykładową aplikację pod adresem „https://github.com/hjr265/hopper-hello.js”, pełna nazwa repozytorium to „hjr265/hopper-hello.js”.
Następnie ustawiamy kilka zmiennych środowiskowych dla aplikacji oraz liczbę potrzebnych nam procesów. I na koniec wybieramy sekret, aby móc zweryfikować nadchodzące ładunki zdarzeń „push”.
Teraz możemy uruchomić nasz program automatyzacji na serwerze. Jeśli wszystko jest poprawnie skonfigurowane (w tym wdrożymy klucze SSH, aby repozytorium było dostępne z serwera), program powinien pobrać kod, przygotować środowisko przy użyciu buildpacka i uruchomić aplikację. Teraz wszystko, co musimy zrobić, to skonfigurować webhook w repozytorium GitHub, aby emitował zdarzenia push i wskazać go na „http://{host}:26590/hook”. Upewnij się, że zastąpiłeś „{host}” nazwą domeny lub adresem IP wskazującym na Twój serwer.
Aby w końcu to przetestować, wprowadź kilka zmian w przykładowej aplikacji i wypchnij je do GitHub. Zauważysz, że narzędzie automatyzacji natychmiast przystąpi do działania i zaktualizuje repozytorium na serwerze, skompiluje aplikację i zrestartuje ją.
Wniosek
Z większości naszych doświadczeń możemy powiedzieć, że jest to coś całkiem przydatnego. Prototypowa aplikacja, którą przygotowaliśmy w tym artykule, może nie być czymś, z czego będziesz chciał korzystać w systemie produkcyjnym takim, jaki jest. Jest mnóstwo miejsca na ulepszenia. Narzędzie takie jak to powinno mieć lepszą obsługę błędów, obsługiwać łagodne zamykanie / ponowne uruchamianie, a możesz użyć czegoś takiego jak Docker, aby zawierać procesy zamiast bezpośrednio je uruchamiać. Rozsądniejsze może być ustalenie, czego dokładnie potrzebujesz w swoim konkretnym przypadku, i wymyślenie do tego programu automatyzacji. A może skorzystaj z innego, znacznie stabilniejszego, sprawdzonego rozwiązania dostępnego w całym Internecie. Ale jeśli chcesz wdrożyć coś bardzo dostosowanego, mam nadzieję, że ten artykuł pomoże Ci to zrobić i pokaże, ile czasu i wysiłku możesz zaoszczędzić na dłuższą metę, automatyzując proces wdrażania aplikacji internetowych.