ปรับใช้เว็บแอปพลิเคชันโดยอัตโนมัติโดยใช้ GitHub Webhooks

เผยแพร่แล้ว: 2022-03-11

ใครก็ตามที่พัฒนาเว็บแอปพลิเคชันและพยายามเรียกใช้บนเซิร์ฟเวอร์ที่ไม่มีการจัดการของตนเอง จะทราบถึงกระบวนการที่น่าเบื่อที่เกี่ยวข้องกับการปรับใช้แอปพลิเคชันของตนและผลักดันการอัปเดตในอนาคต ผู้ให้บริการ Platform as a service (PaaS) ทำให้ง่ายต่อการปรับใช้เว็บแอปพลิเคชันโดยไม่ต้องผ่านกระบวนการจัดเตรียมและกำหนดค่าเซิร์ฟเวอร์แต่ละเครื่อง เพื่อแลกกับค่าใช้จ่ายที่เพิ่มขึ้นเล็กน้อยและความยืดหยุ่นลดลง PaaS อาจทำให้สิ่งต่าง ๆ ง่ายขึ้น แต่บางครั้งเรายังต้องการหรือต้องการปรับใช้แอปพลิเคชันบนเซิร์ฟเวอร์ที่ไม่มีการจัดการของเราเอง การทำให้กระบวนการปรับใช้เว็บแอปพลิเคชันนี้กับเซิร์ฟเวอร์ของคุณเป็นไปโดยอัตโนมัติอาจฟังดูยากเกินไปในตอนแรก แต่ในความเป็นจริง การมีเครื่องมือง่ายๆ ในการทำให้สิ่งนี้เป็นอัตโนมัติอาจทำได้ง่ายกว่าที่คุณคิด การใช้เครื่องมือนี้ง่ายเพียงใดนั้นขึ้นอยู่กับความต้องการของคุณเป็นอย่างมาก แต่ทำได้ไม่ยากอย่างแน่นอน และอาจช่วยประหยัดเวลาและความพยายามได้มากด้วยการทำเว็บแอปพลิเคชันซ้ำๆ ที่น่าเบื่อ การปรับใช้

นักพัฒนาหลายคนได้คิดค้นวิธีการทำให้กระบวนการปรับใช้เว็บแอปพลิเคชันเป็นไปโดยอัตโนมัติ เนื่องจากวิธีการปรับใช้เว็บแอปพลิเคชันของคุณขึ้นอยู่กับกลุ่มเทคโนโลยีที่ใช้อยู่มาก โซลูชันระบบอัตโนมัติเหล่านี้จึงแตกต่างกันไป ตัวอย่างเช่น ขั้นตอนที่เกี่ยวข้องในการปรับใช้เว็บไซต์ PHP โดยอัตโนมัตินั้นแตกต่างจากการปรับใช้เว็บแอปพลิเคชัน Node.js มีโซลูชันอื่นๆ เช่น Dokku ที่ค่อนข้างทั่วไป และสิ่งเหล่านี้ (เรียกว่า buildpacks) ทำงานได้ดีกับกลุ่มเทคโนโลยีที่หลากหลาย

เว็บแอปพลิเคชันและเว็บฮุค

ในบทช่วยสอนนี้ เราจะมาดูแนวคิดพื้นฐานเบื้องหลังเครื่องมือง่ายๆ ที่คุณสามารถสร้างเพื่อทำให้เว็บแอปพลิเคชันของคุณใช้งานได้โดยอัตโนมัติโดยใช้ GitHub webhooks, buildpacks และ Procfiles ซอร์สโค้ดของโปรแกรมต้นแบบที่เราจะสำรวจในบทความนี้มีอยู่ใน GitHub

เริ่มต้นใช้งานเว็บแอปพลิเคชัน

เพื่อทำให้เว็บแอปพลิเคชันของเราใช้งานได้โดยอัตโนมัติ เราจะเขียนโปรแกรม Go อย่างง่าย หากคุณไม่คุ้นเคยกับ Go อย่าลังเลที่จะปฏิบัติตาม เนื่องจากโครงสร้างโค้ดที่ใช้ตลอดทั้งบทความนี้ค่อนข้างเรียบง่ายและควรเข้าใจได้ง่าย หากคุณรู้สึกเช่นนั้น คุณสามารถพอร์ตโปรแกรมทั้งหมดเป็นภาษาที่คุณเลือกได้อย่างง่ายดาย

ก่อนเริ่มต้น ตรวจสอบให้แน่ใจว่าคุณได้ติดตั้ง Go distribution บนระบบของคุณ ในการติดตั้ง Go คุณสามารถทำตามขั้นตอนที่ระบุไว้ในเอกสารอย่างเป็นทางการ

ถัดไป คุณสามารถดาวน์โหลดซอร์สโค้ดของเครื่องมือนี้ได้โดยการโคลนที่เก็บ GitHub ซึ่งจะทำให้ง่ายต่อการปฏิบัติตาม เนื่องจากข้อมูลโค้ดในบทความนี้มีป้ายกำกับชื่อไฟล์ที่เกี่ยวข้อง หากคุณต้องการ คุณสามารถลองใช้งานได้ทันที

ข้อดีอย่างหนึ่งที่สำคัญของการใช้ Go สำหรับโปรแกรมนี้คือ เราสามารถสร้างมันได้ในวิธีที่เรามีการพึ่งพาภายนอกน้อยที่สุด ในกรณีของเรา ในการรันโปรแกรมนี้บนเซิร์ฟเวอร์ เราเพียงแค่ต้องแน่ใจว่าเราได้ติดตั้ง Git และ Bash แล้ว เนื่องจากโปรแกรม Go ถูกคอมไพล์เป็นไบนารีที่ลิงก์แบบสแตติก คุณจึงสามารถคอมไพล์โปรแกรมบนคอมพิวเตอร์ของคุณ อัปโหลดไปยังเซิร์ฟเวอร์ และรันโปรแกรมได้โดยไม่ต้องใช้ความพยายามใดๆ สำหรับภาษายอดนิยมอื่นๆ ในปัจจุบัน จำเป็นต้องมีสภาพแวดล้อมรันไทม์แมมมอธหรือล่ามที่ติดตั้งบนเซิร์ฟเวอร์เพียงเพื่อเรียกใช้การปรับใช้อัตโนมัติของคุณ โปรแกรม Go เมื่อทำถูกต้องแล้ว ก็สามารถทำงานบน CPU และ RAM ได้อย่างง่ายดายเช่นกัน ซึ่งเป็นสิ่งที่คุณต้องการจากโปรแกรมลักษณะนี้

GitHub Webhooks

ด้วย GitHub Webhooks คุณสามารถกำหนดค่าที่เก็บ GitHub ของคุณเพื่อปล่อยเหตุการณ์ทุกครั้งที่มีการเปลี่ยนแปลงภายในที่เก็บหรือผู้ใช้บางคนดำเนินการบางอย่างบนที่เก็บที่โฮสต์ ซึ่งช่วยให้ผู้ใช้สามารถสมัครรับข้อมูลกิจกรรมเหล่านี้และรับการแจ้งเตือนผ่านการเรียกใช้ URL ของกิจกรรมต่างๆ ที่เกิดขึ้นรอบพื้นที่เก็บข้อมูลของคุณ

การสร้างเว็บฮุคนั้นง่ายมาก:

  1. ไปที่หน้าการตั้งค่าของที่เก็บของคุณ
  2. คลิกที่ “Webhooks & Services” บนเมนูการนำทางด้านซ้าย
  3. คลิกที่ปุ่ม “เพิ่มเว็บฮุค”
  4. ตั้งค่า URL และทางเลือกที่เป็นความลับ (ซึ่งจะทำให้ผู้รับสามารถตรวจสอบส่วนของข้อมูลได้)
  5. เลือกตัวเลือกอื่นในแบบฟอร์มตามความจำเป็น
  6. ส่งแบบฟอร์มโดยคลิกที่ปุ่มสีเขียว “เพิ่มเว็บฮุค”

Github Webhooks

GitHub จัดเตรียมเอกสารอย่างละเอียดเกี่ยวกับ Webhooks และวิธีการทำงาน ข้อมูลใดที่ถูกส่งในส่วนของข้อมูลเพื่อตอบสนองต่อเหตุการณ์ต่างๆ เป็นต้น สำหรับวัตถุประสงค์ของบทความนี้ เราสนใจเป็นพิเศษในเหตุการณ์ "พุช" ที่ปล่อยออกมาทุกครั้งที่มีคน พุชไปยังสาขาของที่เก็บ

Buildpacks

Buildpacks ค่อนข้างเป็นมาตรฐานในทุกวันนี้ ใช้โดยผู้ให้บริการ PaaS หลายราย buildpacks ช่วยให้คุณสามารถระบุวิธีกำหนดค่าสแต็กก่อนที่จะปรับใช้แอปพลิเคชัน การเขียน buildpacks สำหรับเว็บแอปพลิเคชันของคุณนั้นง่ายมาก แต่บ่อยครั้งที่การค้นหาอย่างรวดเร็วบนเว็บสามารถค้นหา buildpacks ที่คุณสามารถใช้ได้สำหรับเว็บแอปพลิเคชันของคุณโดยไม่ต้องดัดแปลงใดๆ

หากคุณได้ปรับใช้แอปพลิเคชันกับ PaaS เช่น Heroku คุณอาจรู้อยู่แล้วว่า buildpacks คืออะไรและจะหาได้จากที่ใด Heroku มีเอกสารที่ครอบคลุมเกี่ยวกับโครงสร้างของ buildpacks และรายการ buildpacks ยอดนิยมบางตัวที่ได้รับความนิยม

โปรแกรมอัตโนมัติของเราจะใช้คอมไพล์สคริปต์เพื่อเตรียมแอปพลิเคชันก่อนเปิดตัว ตัวอย่างเช่น การสร้าง Node.js โดย Heroku จะแยกวิเคราะห์ไฟล์ package.json ดาวน์โหลด Node.js เวอร์ชันที่เหมาะสม และดาวน์โหลดการพึ่งพา NPM สำหรับแอปพลิเคชัน เป็นที่น่าสังเกตว่า เพื่อให้ทุกอย่างเรียบง่าย เราจะไม่ได้รับการสนับสนุนอย่างกว้างขวางสำหรับ buildpacks ในโปรแกรมต้นแบบของเรา สำหรับตอนนี้ เราจะถือว่าสคริปต์ buildpack นั้นเขียนขึ้นเพื่อรันด้วย Bash และรันบนการติดตั้ง Ubuntu ใหม่ตามที่เป็นอยู่ หากจำเป็น คุณสามารถขยายสิ่งนี้ได้อย่างง่ายดายในอนาคตเพื่อจัดการกับความต้องการที่ลึกลับมากขึ้น

โปรไฟล์

Procfiles เป็นไฟล์ข้อความธรรมดาที่ให้คุณกำหนดกระบวนการประเภทต่างๆ ที่คุณมีในแอปพลิเคชันของคุณ สำหรับแอปพลิเคชันทั่วไปส่วนใหญ่ คุณควรมีกระบวนการ "เว็บ" เดียว ซึ่งจะเป็นกระบวนการที่จัดการคำขอ HTTP

การเขียน Procfiles เป็นเรื่องง่าย กำหนดประเภทกระบวนการหนึ่งประเภทต่อบรรทัดโดยพิมพ์ชื่อ ตามด้วยโคลอน ตามด้วยคำสั่งที่จะวางไข่กระบวนการ:

 <type>: <command>

ตัวอย่างเช่น หากคุณกำลังทำงานกับเว็บแอปพลิเคชันที่ใช้ Node.js ในการเริ่มเว็บเซิร์ฟเวอร์ คุณจะต้องดำเนินการคำสั่ง “node index.js” คุณสามารถสร้าง Procfile ที่ไดเร็กทอรีฐานของรหัสและตั้งชื่อว่า "Procfile" ด้วยสิ่งต่อไปนี้:

 web: node index.js

เราจะกำหนดให้แอปพลิเคชันกำหนดประเภทกระบวนการใน Procfiles เพื่อให้เราสามารถเริ่มต้นได้โดยอัตโนมัติหลังจากดึงโค้ด

การจัดการเหตุการณ์

ภายในโปรแกรมของเรา เราต้องรวมเซิร์ฟเวอร์ HTTP ที่จะอนุญาตให้เรารับคำขอ POST ขาเข้าจาก GitHub เราจะต้องกำหนดเส้นทาง URL บางส่วนเพื่อจัดการกับคำขอเหล่านี้จาก GitHub ฟังก์ชันที่จะจัดการกับ payloads ที่เข้ามาเหล่านี้จะมีลักษณะดังนี้:

 // 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 } }) }

เราเริ่มต้นด้วยการตรวจสอบประเภทของเหตุการณ์ที่สร้างเพย์โหลดนี้ เนื่องจากเราสนใจเฉพาะเหตุการณ์ "พุช" เราจึงละเว้นกิจกรรมอื่นๆ ทั้งหมดได้ แม้ว่าคุณจะกำหนดค่าเว็บฮุคให้ปล่อยเหตุการณ์ "พุช" เท่านั้น แต่ก็ยังมีเหตุการณ์ประเภทอื่นอีกอย่างน้อยหนึ่งประเภทที่คุณคาดว่าจะได้รับที่จุดปลายเบ็ดของคุณ: "ping" วัตถุประสงค์ของกิจกรรมนี้คือการพิจารณาว่าเว็บฮุคได้รับการกำหนดค่าสำเร็จบน GitHub หรือไม่

ต่อไป เราอ่านเนื้อหาทั้งหมดของคำขอที่เข้ามา คำนวณ HMAC-SHA1 โดยใช้ความลับเดียวกันกับที่เราจะใช้ในการกำหนดค่าเว็บฮุคของเรา และกำหนดความถูกต้องของเพย์โหลดที่เข้ามาโดยเปรียบเทียบกับลายเซ็นที่รวมอยู่ในส่วนหัวของ ขอ. ในโปรแกรมของเรา เราจะละเว้นขั้นตอนการตรวจสอบความถูกต้องนี้ หากไม่มีการกำหนดค่าข้อมูลลับ ในบันทึกด้านข้าง อาจไม่ใช่ความคิดที่ฉลาดที่จะอ่านเนื้อหาทั้งหมดโดยที่อย่างน้อยต้องมีขีดจำกัดบนว่าเราต้องการจัดการกับข้อมูลมากน้อยเพียงใดในที่นี้ แต่ขอให้เราทำให้สิ่งต่างๆ ง่ายขึ้นเพื่อมุ่งเน้นไปที่ประเด็นที่สำคัญ ของเครื่องมือนี้

จากนั้นเราใช้โครงสร้างจากไลบรารีไคลเอนต์ GitHub สำหรับ Go เพื่อแยกส่วนของข้อมูลขาเข้า เนื่องจากเรารู้ว่าเป็นเหตุการณ์ "พุช" เราจึงสามารถใช้โครงสร้าง PushEvent ได้ จากนั้นเราใช้ไลบรารีการเข้ารหัส json มาตรฐานเพื่อแยกส่วนของข้อมูลออกเป็นอินสแตนซ์ของโครงสร้าง เราทำการตรวจสอบสุขภาพจิตสองสามครั้ง และถ้าทุกอย่างเรียบร้อย เราจะเรียกใช้ฟังก์ชันที่เริ่มอัปเดตแอปพลิเคชันของเรา

กำลังอัปเดตแอปพลิเคชัน

เมื่อเราได้รับการแจ้งเตือนเหตุการณ์ที่จุดสิ้นสุดของเว็บฮุค เราสามารถเริ่มอัปเดตแอปพลิเคชันของเราได้ ในบทความนี้ เราจะมาดูการใช้งานกลไกนี้ที่ค่อนข้างง่าย และจะมีที่ว่างสำหรับการปรับปรุงอย่างแน่นอน อย่างไรก็ตาม ควรเป็นสิ่งที่จะช่วยให้เราเริ่มต้นกระบวนการปรับใช้อัตโนมัติขั้นพื้นฐานบางอย่างได้

แผนภูมิการไหลของแอปพลิเคชันเว็บฮุค

กำลังเริ่มต้น Local 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 }

วิธีการนี้บนโครงสร้างแอปสามารถใช้เพื่อเริ่มต้นที่เก็บในเครื่องได้ และกลไกของวิธีนี้ก็ง่ายมาก:

  1. สร้างไดเร็กทอรีสำหรับที่เก็บโลคัลหากไม่มีอยู่
  2. ใช้คำสั่ง "git init" เพื่อสร้างพื้นที่เก็บข้อมูลเปล่า
  3. เพิ่ม URL สำหรับที่เก็บระยะไกลไปยังที่เก็บในเครื่องของเรา และตั้งชื่อว่า "ต้นทาง"

เมื่อเรามีพื้นที่เก็บข้อมูลเริ่มต้นแล้ว การดึงข้อมูลการเปลี่ยนแปลงควรเป็นเรื่องง่าย

กำลังเรียกการเปลี่ยนแปลง

ในการดึงการเปลี่ยนแปลงจากที่เก็บระยะไกล เราเพียงแค่เรียกใช้คำสั่งเดียว:

 // 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 ที่กำหนดค่าไว้เพื่อตรวจสอบว่าแอปพลิเคชันเป็นสิ่งที่เราสามารถจัดการได้หรือไม่ จากนั้น เราสร้างไดเร็กทอรี "แคช" สำหรับกระบวนการคอมไพล์ buildpack หากจำเป็น เนื่องจากไดเร็กทอรีนี้ยังคงอยู่ในบิลด์ต่างๆ จึงอาจเป็นไปได้ว่าเราไม่ต้องสร้างไดเร็กทอรีใหม่เพราะไดเร็กทอรีจะมีอยู่แล้วจากกระบวนการคอมไพล์ก่อนหน้านี้ ณ จุดนี้ เราสามารถเรียกใช้สคริปต์ "คอมไพล์" จาก 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" เมื่อถูกเรียก มันจะเติมอินสแตนซ์ของโครงสร้าง "โหนด" และสร้างรูทีน Go ที่ช่วยเริ่มต้นและหยุดกระบวนการที่สอดคล้องกับโหนดนี้ อีกสองวิธีคือสองวิธีในโครงสร้าง "โหนด": "เริ่ม" และ "หยุด" กระบวนการเริ่มต้นหรือหยุดโดยการส่ง "ข้อความ" ผ่านช่องทางเฉพาะที่รูทีน 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 distribution ก็ควรจะคอมไพล์โปรแกรมได้ทันที

 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” ซึ่งจะทำให้โปรแกรมฟังเพย์โหลดเหตุการณ์ “พุช” ที่ “http://{host}:26590/hook” เมื่อตั้งค่าเว็บฮุค GitHub เพียงแทนที่ “{host}” ด้วยชื่อโดเมนหรือที่อยู่ IP ที่ชี้ไปที่เซิร์ฟเวอร์ของคุณ ตรวจสอบให้แน่ใจว่าพอร์ตเปิดอยู่ในกรณีที่คุณใช้ไฟร์วอลล์บางประเภท

ต่อไป เราเลือก buildpack โดยการตั้งค่า Git URL ที่นี่เราใช้ Node.js buildpack ของ Heroku

ภายใต้ "แอพ" เราตั้งค่า "repo" เป็นชื่อเต็มของที่เก็บ GitHub ของคุณที่โฮสต์รหัสแอปพลิเคชัน เนื่องจากฉันโฮสต์แอปพลิเคชันตัวอย่างที่ “https://github.com/hjr265/hopper-hello.js” ชื่อเต็มของที่เก็บคือ “hjr265/hopper-hello.js”

จากนั้นเราตั้งค่าตัวแปรสภาพแวดล้อมบางอย่างสำหรับแอปพลิเคชัน และจำนวนกระบวนการแต่ละประเภทที่เราต้องการ และสุดท้าย เราเลือกความลับ เพื่อให้เราสามารถตรวจสอบเพย์โหลดของเหตุการณ์ "พุช" ที่เข้ามาได้

ตอนนี้เราสามารถเริ่มโปรแกรมอัตโนมัติของเราบนเซิร์ฟเวอร์ได้แล้ว หากทุกอย่างได้รับการกำหนดค่าอย่างถูกต้อง (รวมถึงการปรับใช้คีย์ SSH เพื่อให้สามารถเข้าถึงที่เก็บได้จากเซิร์ฟเวอร์) โปรแกรมควรดึงรหัส เตรียมสภาพแวดล้อมโดยใช้ buildpack และเปิดแอปพลิเคชัน ตอนนี้ สิ่งที่เราต้องทำคือตั้งค่าเว็บฮุคในที่เก็บ GitHub เพื่อส่งเหตุการณ์พุชและชี้ไปที่ “http://{host}:26590/hook” ตรวจสอบให้แน่ใจว่าคุณได้แทนที่ “{host}” ด้วยชื่อโดเมนหรือที่อยู่ IP ที่ชี้ไปยังเซิร์ฟเวอร์ของคุณ

ในการทดสอบขั้นสุดท้าย ให้ทำการเปลี่ยนแปลงบางอย่างในแอปพลิเคชันตัวอย่างและพุชไปยัง GitHub คุณจะสังเกตเห็นว่าเครื่องมืออัตโนมัติจะทำงานทันทีและอัปเดตที่เก็บบนเซิร์ฟเวอร์ คอมไพล์แอปพลิเคชัน และรีสตาร์ท

บทสรุป

จากประสบการณ์ส่วนใหญ่ของเรา เราสามารถบอกได้ว่านี่คือสิ่งที่มีประโยชน์มาก แอปพลิเคชันต้นแบบที่เราเตรียมไว้ในบทความนี้อาจไม่ใช่สิ่งที่คุณต้องการใช้ในระบบการผลิตเหมือนที่มันเป็น มีพื้นที่มากมายสำหรับการปรับปรุง เครื่องมือเช่นนี้ควรมีการจัดการข้อผิดพลาดที่ดีกว่า รองรับการปิดระบบ/รีสตาร์ทอย่างสง่างาม และคุณอาจต้องการใช้บางอย่างเช่น Docker เพื่อให้มีกระบวนการต่างๆ แทนที่จะเรียกใช้โดยตรง อาจเป็นการฉลาดกว่าที่จะคิดให้ออกว่าคุณต้องการอะไรสำหรับกรณีของคุณโดยเฉพาะ และใช้โปรแกรมอัตโนมัติสำหรับสิ่งนั้น หรืออาจใช้โซลูชันอื่นๆ ที่เสถียรกว่าและผ่านการทดสอบตามเวลาซึ่งมีให้บริการบนอินเทอร์เน็ต แต่ในกรณีที่คุณต้องการเปิดตัวบางสิ่งที่ปรับแต่งได้เองมาก ฉันหวังว่าบทความนี้จะช่วยคุณได้ และแสดงว่าคุณประหยัดเวลาและความพยายามได้มากเพียงใดในระยะยาวด้วยการทำให้กระบวนการปรับใช้เว็บแอปพลิเคชันของคุณเป็นไปโดยอัตโนมัติ

ที่เกี่ยวข้อง: อธิบาย Git Flow ที่ปรับปรุงแล้ว