ตั้งเครื่อง GitLab Runner ใช้เอง แบบ Auto Scale ได้ ในราคาเบาๆ ด้วย Preemptible Instance

วันนี้จะขอมาเล่าส่วนหนึ่งของงาน DevOps ในบริษัท เป็นเรื่องของการเอา GitLab Runner เข้าไปรันบนเครื่อง Preemptible Instance ของ Google Cloud Platform ทำให้ลดค่าเครื่องไปได้อย่างมาก แต่ยังทำงานทุกอย่างเป็นปรกติดี เอาล่ะครับ อย่ามัวรอช้า เรามาเริ่มกันเลยดีกว่า

GitLab Runner คืออะไร? ทำไมต้องตั้ง GitLab Runner เอง?

GitLab Runner คือโปรแกรมที่เอาไว้รัน Job ใน GitLab Pipeline ซึ่งมันจะ Spawn Docker แล้วก็ทำตาม Script ที่เราสั่ง ซึ่งถ้าเราใช้ gitlab.com เราสามารถใช้ Shared Runner ได้ฟรีๆ เดือนละ 2000 นาที แชร์กันในกลุ่ม แต่พอมีโปรเจกต์เยอะเข้า มี Commit เยอะเข้า โควต้าที่ให้มาก็ไม่พอละ นอกจากนั้นเราอยากรันบนเครื่องที่เป็นของ บ. เองด้วย จึงเกิดการตั้ง GitLab Runner Server ขึ้นมาใช้งานเอง แล้วให้มันรัน Job ที่ส่งออกมาจาก gitlab.com นั่นแหละ

ซึ่งความสวยงามของ GitLab Pipeline ที่ควรจะทำก็คือ มันสามารถตั้งให้งานต่างๆ สามารถรันพร้อมๆ กันได้ ทำให้เราสามารถใช้ทรัพยากรเครื่องในเวลาสั้นๆ ได้อย่างมีประสิทธิภาพ และลดเวลาในการรัน Pipeline ลงไป และจุดนี้เอง ฟีเจอร์ Auto Scale ของ Cloud จึงน่าสนใจ


Preemptible Instance คืออะไร?

Preemptible Instance คือบริการเครื่อง VM ประเภทหนึ่งของ Google Cloud Platform (GCP) ซึ่งมีความพิเศษตรงที่ มันเป็นเครื่องที่เปิดใช้งานจากทรัพยากรที่เหลือใน farm ของ GCP เครื่องจะถูกเรียกคืนเมื่อไหร่ก็ได้ (ถูกปิด) และเครื่องนี่จะเปิดสูงสุดนานไม่เกิน 24 ชั่วโมง แล้วเครื่องก็จะดับไป มันจะคล้ายๆ กับ Spot Instances ของ AWS แต่ของ GCP นี่จะราคาคงที่ ไม่ต้อง Bid สู้กับใคร ดังนั้นค่อนข้างจะมั่นใจได้มากกว่า (แต่ไม่ 100%) ว่าเราจะได้เครื่องมารัน ที่เราต้องทำก็แค่ให้เครื่องมัน Auto Spawn ขึ้นมาได้เวลาที่มันถูกปิดไปเท่านั้นล่ะ

งานของ GitLab Runner นั้นค่อนข้างจะเหมาะกับเครื่อง Preemptible เนื่องจากเป็นงานที่รันสั้นๆ แล้วก็ไม่ได้ต้องรันอยู่ตลอดเวลาด้วย แต่ก็ต้องระวัง Job ที่ยาวๆ เหมือนกัน ถ้าโชคร้ายเครื่องปิดไปก่อนงานรันเสร็จ จะต้องเขียน gitlab-ci ให้มันไม่สร้างความพินาศให้แก่ระบบด้วยนะครับ ไม่ใช่ว่างานมี deployment dependency กัน แล้วอันนึงรันเสร็จอีกอันรันไม่เสร็จแล้วจะพัง ถ้าเคสนี้อาจไม่เหมาะ ผมขอเตือนไว้ก่อน

มาดูเรื่องราคา Preemptible VM นั้นช่างยั่วยวนให้มาใช้ยิ่งนัก มันจะถูกกว่าเครื่องปรกติมาก ผม Capture ราคามาให้ดูกันเล่นๆ


GCE Instance Group ผู้ทำให้ทุกอย่างเป็นไปได้

GCE Instance Group คืออีกหนึ่งบริการของ GCE ซึ่งมันเป็นบริการสำหรับการทำ Auto Scaling ของ VM นั่นเอง Concept โดยคร่าวคือ เราจะต้องสร้าง Image ของ VM ที่ต้องการเอาไว้ พร้อมให้มันรัน Script ที่ต้องการตอนเปิดเครื่อง (และปิดเครื่อง) จากนั้นก็เซทว่า จะให้มีเครื่องอย่างน้อย อย่างมาก จำนวนกี่เครื่อง และ condition ที่จะบอกว่า เมื่อไหร่ควร Spawn เครื่องเพิ่ม ในสถาการณ์ปรกติ ถ้าไม่มีโหลดอะไรมาก เราก็อาจจะเปิดเครื่อง Standby เอาไว้ตัวเดียว แต่เมื่อไหร่ที่มีงานเข้ามา ก็ให้มัน Spawn เครื่องเพิ่มขึ้นมา เพื่อมารันงานของ GitLab Pipeline ที่ทำงานเป็น Parallel ได้ แล้วในระหว่างที่ไม่มีงานเข้ามา มันก็จะปิดเครื่องส่วนเกินออกไป ทำให้ไม่เสียเงินเกินจำเป็น

นอกจากนั้น ถ้าเราสั่งให้มีเครื่องอย่างน้อย 1 เครื่อง เวลา Preemptible VM ดับไป มันก็จะพยายาม Spawn เครื่องใหม่ขึ้นมาทดแทนทันที ทำให้เรามีเครื่องพร้อมสำหรับการรัน Pipeline อยู่ตลอดเวลานั่นเอง


แล้วจะทำอย่างไรให้มัน Automate ล่ะ?

หลักการคร่าวๆ คือ เราจะต้องเขียน Script เอาไว้ตั้งเครื่องใหม่ ให้มัน Register เข้าไปหา GitLab โดยอัตโนมัติ

ตามหา GitLab Runner Token

สิ่งที่จะต้องใช้คือ GitLab Runner Token สามารถเข้าหาได้ที่หน้า Settings > CI/CD (URL ประมาณ https://gitlab.com/groups/<GROUP>/-/settings/ci_cd) ซึ่งมันจะมี Runner ในระดับ Group, Project อะไรก็เลือกเอาตามความเหมาะสม ซึ่งเคสของเราเป็นเคสของ Group Runners นั่นเอง พอเข้าไปก็จะเห็น Token ที่จะเอาไปใช้นั่นเอง

ในรูปผมแก้ Token เป็น GROUP_RUNNER_TOKEN ตัวแดงๆ เวลาเอาไปใช้ก็นี่แหละ

สร้าง Instance Template

ต่อมาก็ไปสร้าง Instance Template บน GCP ซึ่งผมใช้เป็นเครื่อง 1 CPU, RAM 2 GB, HDD 50 GB ขนาดอันนี้ขึ้นอยู่กับงานแหละ ส่วน Base Image เป็น Debian 9 จากนั้นลงดูในส่วนของ Automation > Startup script อันนี้จะเป็นส่วนของคำสั่งที่เอาไว้รันตอนเปิดเครื่องนั่นเอง ให้เอา Script จากที่นี่ใส่ลงไป

มีส่วนที่ต้องแก้ 2 ส่วน คือ
CHANGE_GITLAB_RUNNER_TOKEN_HERE ให้เอา GitLab Runner Token มาใส่ตรงนี้
LIST,OF,RUNNER,TAGS ให้ใส่ tag ของเครื่องลงไป คั่นด้วย comma อันนี้จะทำให้เราสามารถเลือกรัน Pipeline บนเฉพาะเครื่องเหล่านี้ได้ มันจะส่งผลตอนที่บางที build dind (Docker in Docker) แล้วจะต้องติด tag ของผมก็ตั้งเป็นชื่อประมาณว่า private,spicydog,gce อะไรประมาณนี้
concurrent = 2 อันนี้มันจะไปแก้ไฟล์ Config ให้ 1 Runner สามารถรันได้มากกว่า 1 งานพร้อมกัน จะได้ไม่ต้อง Spawn เครื่องเท่าจำนวนงาน ซึ่งจากที่เห็นก็คือ ผมแก้เป็น 2 เอาไว้ แปลว่าถ้ามี 2 งานเข้ามามันก็รันบนเครื่องนี้เครื่องเดียวได้เลย ไม่ต้องรอ Spawn เครื่องใหม่ (ไปดูรูปของบน จะเห็นว่า Stage แรกผมมี 2 Jobs)

ทีนี้เครื่องเราก็ควรจะ Spawn แล้วถูก Register ลงไปโดยอัตโนมัติละ แต่ยังไม่เสร็จ! ปัญหาคือ เวลาเครื่องปิด เราก็จะต้อง unregister ด้วย ไม่อย่างงั้น GitLab ก็จะไม่รู้ว่าเครื่องนั้นๆ ได้ถูก Shutdown ลงไปแล้ว (ทำ graceful shutdown นั่นเอง) ซึ่งวิธีการนั้นก็แสนง่าย ที่ Metadata ให้ใส่ Item ใหม่ลงไปว่า โดยเซท Key เป็น shutdown-script และ Value เป็น sudo gitlab-runner unregister –all-runners

สุดท้ายคือเซท Preemptibility ให้เป็น On ทีนี้ก็เรียบร้อยละ กดปุ่ม Create ได้เลย


สร้าง Instance Group ขึ้นมาจาก Template

พอสร้าง Template เสร็จ เราก็จะออกกลับมาที่หน้าเดิม ทีนี้ให้เลือก Template ที่ต้องการใช้ แล้วดูข้างบน จะเห็นว่าเราสามารถกด Create Instance Group ได้ ก็กดเลย

ตั้งเชื่อ group ให้เรียบร้อย อาจจะเป็น gitlab-runner ก็ได้ เลือก Zone ที่ต้องการ ของผมเลือกไว้ที่ US เพราะราคาเครื่องถูกกว่า แล้วก็น่าจะอยู่ใกล้กับ Server ของ gitlab.com ด้วย แล้วก็ลงไปดูที่ Instance template ซึ่งจะต้องให้เป็นชื่อเดียวกับ template ที่เราตั้งเอาไว้เมื่อสักครู่ แล้วก็เปิด Auto Scaling ซะ ทีนี้จะ Scale ไปกี่เครื่อง ขึ้นอยู่กับว่า Stage ของเรามี Job สูงสุดกี่อัน แล้วแต่ละอันรันนานเท่าไหร่ด้วย สำหรับผมจะมีอันที่มี Job สูงสุด 5 อัน แต่เป็นงานที่รันเร็ว 1 งาน แล้ว 1 runner ตั้งไว้ให้รันได้ 2 Jobs พร้อมกัน ดังนั้นก็ใช้ 2 เครื่องก็พอ ผมเลยเซทให้ Auto Scale จาก 1-2 ก็พอ ทีนี้ก็ขึ้นอยู่กับว่าแต่ละสถาการณ์จะพาไปเลยครับ นอกจากนั้นผมก็เลือกให้มัน Auto Scale ตั้งแต่ CPU แตะ 20% เลย คือถ้ามี Job มารัน มันก็จะเปิดเครื่องใหม่มารอทันที หลังจาก Stage แรกรันเสร็จ Stage ต่อมาที่มีงานเยอะๆ ก็จะมีเครื่องขึ้นมาพร้อมลุยทันที

เสร็จแล้วก็กด Save


รอมันสักพัก อาจจะประมาณ 3-5 นาที แล้วไปดูที่เว็บ GitLab ส่วนของ CI/CD ตรงที่เราเอา Token เราควรจะเห็นชื่อ Runner ที่เราเพิ่งสร้างขึ้นมาแล้ว ดูง่ายๆ จาก tag ก็ได้ครับ

จริงๆ ตอนต้นจะเห็นเครื่องเดียว แต่หลังจากรันงานไปแล้วมันก็จะโผล่ขึ้นมาเป็น 2 เครื่อง หลังจากรันงานเสร็จ มันก็จะลดลงไปเหลือ 1 เครื่องครับ


Advanced Mode: ตั้งค่าให้ GitLab Runner ใช้งาน Cache ของ GCS ได้

Optional ส่วนตรงนี้ไม่จำเป็นต้องทำก็ได้นะครับ แต่ถ้ามีการใช้ Caching ใน Pipeline เช่น อาจจะใช้ Dependency และต้องการ Runner ที่มีประสิทธิภาพดี เรื่องนี้ก็ควรทำ แต่มันจะตั้งค่ายากๆ หน่อย แล้วผมจะไม่ลงรายละเอียดลึกนะ แค่จะเล่าให้ฟังว่า ตอนผมทำเจอปัญหาอะไรบ้าง

– ที่จะต้องทำเลยคือ สร้าง Google Cloud Storage Bucket ขึ้นมา เอาไว้ Zone เดียวกับ VM ของเรา
– สร้าง Service Account ขึ้นมา กำหนดให้มันเข้าถึง GCS ที่สร้างข้างบนได้
– แล้วก็สร้าง Credential File ที่เป็น json ไฟล์นี้ GitLab Runner จะเอาไว้ต่อกับ GCS ของเรา แล้ว Download มาเก็บไว้ในเครื่อง

เอาล่ะ ทีนี้ที่ต้องทำก็คือ เราจะต้องเอา JSON File นี่ไปเก็บไว้บนเครื่องที่ถูกสร้างจาก Instance Template ให้ได้ ซึ่งความจริงเราก็อาจจะเก็บ Credential บน GCS แล้วค่อยโหลดลงมาก็ได้หรือเปล่าผมไม่แน่ใจ อยากให้ลองกันดู แต่สิ่งที่ผมทำคือเอา JSON เอาเขียนผ่าน Script ที่ลงนั่นล่ะ แต่ทีนี้มันจะเกิดปัญหานิดนึง เดี๋ยวเล่าให้ฟัง แต่เอา Script ไปดูก่อน

ให้ดูในบรรทัดที่ 17 อันนี้คือ content ข้างใน JSON นั่นเอง แต่จะถูก escape ด้วย \ เวลา ” (คือต้องไปเอา JSON ที่ได้มา เปลี่ยน ” เป็น \” ให้หมดครับ) ไม่งั้นมันจะเขียนลงไปไม่ได้ (คิดๆ ไม่รู้ใช้ ‘ ครอบแทนได้ไหม แต่ทางที่ดีกว่าน่าจะเป็นการให้มันโหลดไฟล์ลงมาจาก GCS เลย) จากนั้นเราก็ให้มันเขียนลงไฟล์เอาไว้ที่ /etc/service_account.json ทีนี้ให้ไปเช็คว่า ตอนเครื่องสร้างเสร็จ หน้าตามันเป็น JSON format ที่ ถูกต้อง ผมติดตรงนี้อยู่นานมาก เกิดจากการมึนไว้ใจ Script ที่ตัวเองเขียนมากเกินไป ก็เลยทำให้ข้อมูลมันเขียนลงไปไม่ได้เป็นตามที่ต้องการ

ต่อมาลงมาดูส่วนข้างล่าง ก็จะเห็นการตั้งค่า Cache ไปที่ GCS แล้วก็จะเห็นมันเลือกไฟล์ json ที่เขียนลงไป แล้วก็แก้ GCS_BUCKET_NAME ให้เป็นชื่อ bucket ที่เพิ่งตั้งขึ้นมา จากนั้นก็เอา template นี้ไปเปลี่ยนใน Instance Group ซะ

จากนั้นรัน Job ที่มีการเขียน Cache ดู ถ้าใช้งานได้ เราจะเห็น Directory โผล่ขึ้นมาใน GCS ครับ เท่านั้นก็เป็นอันเรียบร้อย


สำหรับวันนี้ก็ขอจบไว้แต่เพียงเท่านี้ มีช่องให้ Improve กันต่อนิดหน่อย แล้ว Script นี่เอาไปใช้กับ Cloud เจ้าอื่นก็ได้นะครับ สุดท้ายมันเหมือนๆ กันเนี่ยล่ะ (ยกเว้นตรง Cache) ใครมีคำถามอะไร ลองตั้งแล้วติดตรงไหน ก็พูดคุยกันได้ครับ สำหรับวันนี้ สวัสดีครับ


Update (2019-07-24)

เนื่องจาก GitLab Runner มีการอัพเดท ทำให้ image docker:stable ใช้งานไม่ได้ ต้องใส่ตัวแปร env ว่า DOCKER_TLS_CERTDIR: "" เข้าไปด้วย ซึ่งผมได้แก้ script ด้วยบนไปเรียบร้อยแล้ว ดังนั้นจึงไม่ต้องทำอะไร
reference: https://gitlab.com/gitlab-org/gitlab-ce/issues/64959