Всеки, който разработва уеб приложения и се опитва да ги стартира на собствените си неуправлявани сървъри, е наясно с досадния процес, свързан с разгръщането на тяхното приложение и прокарването на бъдещи актуализации. Доставчиците на платформа като услуга (PaaS) улесниха разполагането на уеб приложения, без да се налага да преминават през процеса на предоставяне и конфигуриране на отделни сървъри, в замяна на леко увеличение на разходите и намаляване на гъвкавостта. PaaS може да е улеснил нещата, но понякога все пак трябва или искаме да разположим приложения на нашите собствени неуправлявани сървъри. Автоматизирането на този процес на разполагане на уеб приложения на вашия сървър може да звучи поразително в началото, но в действителност измислянето на прост инструмент за автоматизиране на това може да е по-лесно, отколкото си мислите. Колко лесно ще бъде внедряването на този инструмент зависи много от това колко лесни са вашите нужди, но със сигурност не е трудно да се постигне и вероятно може да помогне за спестяване на много време и усилия, като правите досадни повтарящи се битове на уеб приложение разполагания.
Много разработчици са измислили свои собствени начини за автоматизиране на процесите на внедряване на своите уеб приложения. Тъй като начина на разполагане на вашите уеб приложения зависи много от точния технологичен стек, който се използва, тези решения за автоматизация се различават помежду си. Например стъпките, включени в автоматично разполагане на PHP уебсайт е различно от внедряване на уеб приложение Node.js . Съществуват и други решения, като например Доку , които са доста общи и тези неща (наречени buildpacks) работят добре с по-широк набор от технологични стекове.
трябва ли да научите C или C++
В този урок ще разгледаме основните идеи зад един прост инструмент, който можете да изградите за автоматизиране на разгръщането на вашите уеб приложения, използвайки GitHub уеб куки, buildpacks и Procfiles. Изходният код на прототипа на програмата, който ще разгледаме в тази статия, е на разположение на GitHub .
За да автоматизираме внедряването на нашето уеб приложение, ще напишем проста програма Go. Ако не сте запознати с Go, не се колебайте да продължите, тъй като кодовите конструкции, използвани в тази статия, са доста прости и трябва да бъдат лесни за разбиране. Ако ви харесва, вероятно можете доста лесно да пренесете цялата програма на език по ваш избор.
Преди да започнете, уверете се, че на вашата система е инсталирана дистрибуцията Go. За да инсталирате Go, можете да следвате стъпки, описани в официалната документация .
След това можете да изтеглите изходния код на този инструмент, като клонирате Хранилище на GitHub . Това трябва да ви улесни да продължите, тъй като кодовите фрагменти в тази статия са обозначени със съответните имена на файлове. Ако искате, можете опитай го веднага.
Едно от основните предимства на използването на Go за тази програма е, че можем да я изградим по начин, при който имаме минимални външни зависимости. В нашия случай, за да стартираме тази програма на сървър, просто трябва да се уверим, че имаме инсталирани Git и Bash. Тъй като програмите Go са компилирани в статично свързани двоични файлове, можете да компилирате програмата на вашия компютър, да я качите на сървъра и да я стартирате с почти нулеви усилия. За повечето други популярни езици днес това ще изисква някаква мамутска среда за изпълнение или интерпретатор, инсталиран на сървъра, само за да стартирате вашия автоматизатор за внедряване. Програмите Go, когато се направят правилно, също могат да бъдат много лесни за процесори и RAM - което е нещо, което искате от програми като тази.
С GitHub Webhooks е възможно да конфигурирате вашето хранилище GitHub да излъчва събития всеки път, когато нещо се промени в хранилището или някой потребител изпълни определени действия върху хостваното хранилище. Това позволява на потребителите да се абонират за тези събития и да бъдат уведомявани чрез извикване на URL адреси за различните събития, които се случват около вашето хранилище.
Създаването на уеб кука е много просто:
GitHub предоставя обширна документация за Webhooks и как точно работят, каква информация се доставя в полезния товар в отговор на различни събития и т.н. За целите на тази статия ние сме особено заинтересовани от “Push” събитие което се излъчва всеки път, когато някой натисне към който и да е клон на хранилището.
В наши дни компилациите са почти стандартни. Използвани от много доставчици на PaaS, buildpacks ви позволяват да посочите как ще бъде конфигуриран стекът преди приложението да бъде внедрено. Писането на компилационни пакети за вашето уеб приложение е наистина лесно, но по-често бързото търсене в мрежата може да ви намери компилационен пакет, който можете да използвате за уеб приложението си без никакви модификации.
Ако сте разположили приложение в PaaS като Heroku, може би вече знаете какво представляват buildpacks и къде да ги намерите. Heroku има някои изчерпателни документация за структурата на buildpacks и а списък с някои добре изградени популярни buildpacks .
Нашата програма за автоматизация ще използва скрипт за компилиране, за да подготви приложението, преди да го стартира. Например компилация на Node.js от Heroku анализира файла package.json, изтегля подходяща версия на Node.js и изтегля зависимости на NPM за приложението. Струва си да се отбележи, че за да улесним нещата, няма да имаме обширна поддръжка за buildpacks в нашата прототипна програма. Засега ще приемем, че buildpack скриптовете са написани, за да се изпълняват с Bash, и че те ще се изпълняват на нова инсталация на Ubuntu, както е. Ако е необходимо, можете лесно да удължите това в бъдеще, за да отговорите на по-езотерични нужди.
Профилите са прости текстови файлове, които ви позволяват да дефинирате различните видове процеси, които имате във вашето приложение. За повечето прости приложения в идеалния случай ще имате един „уеб“ процес, който би бил процесът, който обработва HTTP заявките.
Писането на профили е лесно. Определете един тип процес на ред, като въведете името му, последвано от двоеточие, последвано от командата, която ще породи процеса:
:
Например, ако работите с базирано на Node.js уеб приложение, за да стартирате уеб сървъра, ще изпълните командата “node index.js”. Можете просто да създадете Procfile в основната директория на кода и да го наименувате „Procfile“ със следното:
web: node index.js
Ще изискваме от приложенията да дефинират типовете процеси в Procfiles, за да можем да ги стартираме автоматично след изтегляне на кода.
В нашата програма трябва да включим HTTP сървър, който ще ни позволи да получаваме входящи POST заявки от GitHub. Ще трябва да отделим някакъв URL път за обработка на тези заявки от GitHub. Функцията, която ще обработва тези входящи полезни товари, ще изглежда по следния начин:
// 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 } }) }
Започваме с проверка на типа събитие, генерирало този полезен товар. Тъй като се интересуваме само от събитието „push“, можем да игнорираме всички останали събития. Дори ако конфигурирате webhook да излъчва само „push“ събития, пак ще има поне един друг вид събитие, което можете да очаквате да получи в крайната точка на вашата кука: „ping“. Целта на това събитие е да определи дали webhook е конфигуриран успешно на GitHub.
След това четем цялото тяло на входящата заявка, изчисляваме нейния HMAC-SHA1, използвайки същата тайна, която ще използваме за конфигуриране на нашата уеб кука, и определяме валидността на входящия полезен товар, като го сравняваме с подписа, включен в заглавката на заявка. В нашата програма игнорираме тази стъпка за проверка, ако тайната не е конфигурирана. Като странична бележка може да не е разумно да прочетете цялото тяло, без поне да имате някакъв горен лимит за това с колко данни ще искаме да се занимаваме тук, но нека оставим нещата прости, за да се съсредоточим върху критичните аспекти на този инструмент.
След това използваме структура от Клиентска библиотека на GitHub за Go за демаркация на входящия полезен товар. Тъй като знаем, че това е „push“ събитие, можем да използваме Структура на PushEvent . След това използваме стандартната библиотека за кодиране json, за да демаршалираме полезния товар в екземпляр на структурата. Извършваме няколко проверки за здравословно състояние и ако всичко е наред, извикваме функцията, която започва да актуализира нашето приложение.
След като получим известие за събитие в нашата крайна точка на webhook, можем да започнем да актуализираме приложението си. В тази статия ще разгледаме доста простото прилагане на този механизъм и със сигурност ще има място за подобрения. Това обаче трябва да ни даде началото на някои основни автоматизирани процеси на внедряване.
кое от тях се използва като мярка за общата сума на наличния паричен поток от даден проект?
Този процес ще започне с проста проверка, за да се определи дали за първи път се опитваме да разгърнем приложението. Ще направим това, като проверим дали съществува локалната директория на хранилището. Ако не съществува, първо ще инициализираме нашето локално хранилище:
// 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 }
Този метод на структурата на приложението може да се използва за инициализиране на локалното хранилище и неговите механизми са изключително прости:
След като имаме инициализирано хранилище, извличането на промените трябва да бъде лесно.
За да извлечем промени от отдалеченото хранилище, трябва само да извикаме една команда:
// 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() }
Правейки „git fetch“ за нашето локално хранилище по този начин, ние можем да избегнем проблеми с Git, който не може да превърта напред в някои сценарии. Не че принудителните извличания са нещо, на което трябва да разчитате, но ако трябва да направите принудително натискане на отдалеченото хранилище, това ще се справи с благодат.
Тъй като използваме скриптове от buildpacks, за да компилираме нашите приложения, които се разполагат, нашата задача тук е относително лесна:
// 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() }
Започваме с премахването на предишната ни директория с приложения (ако има такава). След това създаваме нов и проверяваме съдържанието на главния клон към него. След това използваме скрипта “засичане” от конфигурирания пакет за изграждане, за да определим дали приложението е нещо, с което можем да се справим. След това създаваме директория „кеш“ за процеса на компилация на buildpack, ако е необходимо. Тъй като тази директория продължава в компилациите, може да се случи да не се налага да създаваме нова директория, тъй като тя вече ще съществува от някакъв предишен процес на компилация. На този етап можем да извикаме скрипта “compile” от buildpack и да го накараме да подготви всичко необходимо за приложението преди стартирането. Когато buildpacks се изпълняват правилно, те могат да се справят сами с кеширането и повторното използване на предварително кеширани ресурси.
При нашето внедряване на този автоматизиран процес на внедряване ще спрем старите процеси, преди да започнем процеса на компилация, и след това ще стартираме новите процеси, след като фазата на компилация завърши. Въпреки че това улеснява прилагането на инструмента, той оставя някои потенциално невероятни начини за подобряване на автоматизирания процес на внедряване. За да подобрите този прототип, вероятно можете да започнете, като осигурите нулев престой по време на актуализации. Засега ще продължим с по-простия подход:
// 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 }
В нашия прототип спираме и стартираме различните процеси, като итерираме масив от възли, където всеки възел е процес, съответстващ на един от екземплярите на приложението (както е конфигуриран преди стартирането на този инструмент на сървъра). В рамките на нашия инструмент ние следим текущото състояние на процеса за всеки възел. Също така поддържаме отделни регистрационни файлове за тях. Преди да се стартират всички възли, на всеки се присвоява уникален порт, започващ от даден номер на порт:
// 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 }
На пръв поглед това може да изглежда малко по-сложно от това, което сме правили досега. За да направим нещата лесни за разбиране, нека разделим горния код на четири части. Първите две са в рамките на функцията “NewNode”. Когато бъде извикан, той попълва екземпляр на структурата “Node” и създава рутина Go, която помага за стартиране и спиране на процеса, съответстващ на този Node. Другите два са двата метода в структурата на “Node”: “Start” и “Stop”. Процесът се стартира или спира чрез предаване на „съобщение“ през определен канал, че тази рутинна Go-програма за всеки възел следи. Можете да предадете съобщение, за да стартирате процеса, или друго съобщение, за да го спрете. Тъй като действителните стъпки, свързани с стартиране или спиране на процес, се случват в една рутина Go, няма шанс да се получат състезателни условия.
Рутината Go стартира безкраен цикъл, където изчаква „съобщение“ през канала „stateCh“. Ако съобщението, предадено на този канал, изисква възелът да стартира процеса (вътре в “case StateUp”), той използва Bash за изпълнение на командата. Докато прави това, той конфигурира командата да използва дефинирани от потребителя променливи на средата. Той също така пренасочва стандартните потоци на изход и грешки към предварително дефиниран регистрационен файл.
От друга страна, за да спре процес (вътре в “case StateDown”), той просто го убива. Тук вероятно бихте могли да проявите креативност и вместо да убиете процеса, незабавно му изпратете SIGTERM и изчакайте няколко секунди, преди действително да го убиете, давайки шанс на процеса да прекрати изящно.
Методите „Старт“ и „Стоп“ улесняват предаването на съответното съобщение на канала. За разлика от метода „Старт“, методът „Стоп“ всъщност изчаква процесите да бъдат унищожени, преди да се върне. „Старт“ просто предава съобщение на канала, за да стартира процеса и се връща.
И накрая, всичко, което трябва да направим, е да свържем всичко в рамките на основната функция на програмата. Това е мястото, където ще заредим и анализираме конфигурационния файл, ще актуализираме buildpack, ще се опитаме да актуализираме приложението си веднъж и ще стартираме уеб сървъра, за да прослушваме входящите полезни натоварвания от 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) }
Тъй като изискваме buildpacks да бъдат прости хранилища на Git, “UpdateBuildpack” (внедрен в buildpack.go ) просто изпълнява „git clone” и „git pull”, както е необходимо, с URL адреса на хранилището, за да актуализира локалното копие на buildpack.
В случай, че не сте клонирали хранилището все пак можете да го направите сега. Ако имате инсталирана дистрибуция Go, трябва да е възможно да компилирате програмата веднага.
mkdir hopper cd hopper export GOPATH=`pwd` go get github.com/hjr265/toptal-hopper go install github.com/hjr265/toptal-hopper
Тази последователност от команди ще създаде директория с име hopper, ще я зададе като GOPATH, ще вземе кода от GitHub заедно с необходимите Go библиотеки и ще компилира програмата в двоичен файл, който можете да намерите в директорията “$ GOPATH / bin”. Преди да можем да използваме това на сървър, трябва да създадем просто уеб приложение, с което да тестваме това. За удобство създадох просто уеб приложение Node.js, подобно на „Здравей, свят“ и го качих в него друго хранилище на GitHub които можете да разклоните и да използвате повторно за този тест. След това трябва да качим компилирания двоичен файл на сървър и да създадем конфигурационен файл в същата директория:
# 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 = ''
Първата опция в нашия конфигурационен файл, „core.addr“, ни позволява да конфигурираме HTTP порта на вътрешния уеб сървър на нашата програма. В горния пример го зададохме на „: 26590“, което ще накара програмата да прослушва полезни натоварвания на събитията „push“ при „http: // {host}: 26590 / hook“. Когато настройвате уеб куката на GitHub, просто заменете „{host}“ с името на домейна или IP адреса, който сочи към вашия сървър. Уверете се, че портът е отворен, в случай че използвате някаква защитна стена.
c корпорациите предлагат по-голяма правна защита на собствениците от корпорациите.
След това избираме компилационен пакет, като задаваме неговия Git URL. Тук използваме Heroku’s Node.js buildpack .
Под „app“ задаваме „repo“ на пълното име на вашето хранилище в GitHub, в което се хоства кодът на приложението. Тъй като хоствам примерното приложение на „https://github.com/hjr265/hopper-hello.js“, пълното име на хранилището е „hjr265 / hopper-hello.js“.
След това задаваме някои променливи на средата за приложението и броя на всяка от тях тип процеси Имаме нужда от. И накрая, ние избираме тайна, за да можем да проверим входящите полезни натоварвания на събитията.
Вече можем да стартираме нашата програма за автоматизация на сървъра. Ако всичко е конфигурирано правилно (включително разполагане на SSH ключове, така че хранилището да е достъпно от сървъра), програмата трябва да извлече кода, да подготви средата с помощта на buildpack и да стартира приложението. Сега всичко, което трябва да направим, е да настроим уеб хук в хранилището на GitHub, който да излъчва push събития и да го насочва към „http: // {host}: 26590 / hook“. Уверете се, че сте заменили „{host}“ с името на домейна или IP адреса, който сочи към вашия сървър.
Накрая тест it, направете някои промени в примерното приложение и ги изпратете до GitHub. Ще забележите, че инструментът за автоматизация веднага ще влезе в действие и ще актуализира хранилището на сървъра, ще компилира приложението и ще го рестартира.
От повечето ни преживявания можем да кажем, че това е нещо доста полезно. Прототипното приложение, което сме подготвили в тази статия, може да не е нещо, което ще искате да използвате в производствена система, каквато е. Има много място за подобрение. Инструмент като този трябва да има по-добра обработка на грешки, да поддържа елегантни изключвания / рестартирания и може да искате да използвате нещо като Docker, за да съдържа процесите, вместо да ги стартирате директно. Може да е по-разумно да разберете какво точно ви трябва за вашия конкретен случай и да измислите програма за автоматизация за това. Или може би използвайте друго, много по-стабилно, изпитано във времето решение, достъпно в целия Интернет. Но в случай, че искате да пуснете нещо много персонализирано, надявам се тази статия да ви помогне да направите това и да покажете колко време и усилия бихте могли да спестите в дългосрочен план, като автоматизирате процеса на внедряване на вашето уеб приложение.
Свързани: Обяснен подобрен Git Flow