มา build และ test Go project ด้วย Bazel กัน !!!!

ก่อนอื่นเลยคงต้องบอกก่อนนะครับว่า บทความนี้ไม่ได้เขียนมาให้คนที่พึ่งเริ่มเขียนโปรแกรมอ่านสักเท่าไหร่ เพราะฉะนั้นถือว่าเตือนแล้วนะครับเพื่อใครที่หลงเข้ามาอ่านบทความนี้ ส่วนใครอยากทำตามหรือ ดูตัวอย่าง clone repo นี้ได้เลย
Why Bazel ?
งั้นอย่างแรกเลยคงต้องตั้งคำถามว่าทำไมต้องใช้ Bazel มันคืออะไร แล้วมาช่วยอะไร Bazel คือ build tools ตัวนึงที่เข้ามาช่วยจัดการ ตัว project สั่ง run สั่ง test ซึ่งข้อดีของตัว Bazel เนี่ยคือมันรองรับการทำงานหลายภาษา นั่นหมายความว่ามันเป็นเครื่องมือที่เหมาะมาก สำหรับการทำ mono repo นั้นเอง ซึ่งปัจจุบันตัว mono repo เนี่ยก็เริ่มแพร่หลายมากขึ้นเพราะว่า microservices นั้นเองซึ่ง โดยทั่วไปแล้ว ภาษา และ stack ที่ใช้ในแต่ละ service เนี่ยก็จะไม่ค่อยเหมือนกันหรอก นั้นทำให้การสั่ง build หรือสั่ง test ก็ต้องใช้คำสั่งแตกต่างกันไปเยอะแยะวุ่นวาย ยิ่ง โปรเจคใหญ่ยิ่งดูแลยาก อีกจุดสำคัญคือ ตัว Bazel มีความสามารถในการเก็บ cache ตอน build หรือ ตอน test ทำให้การสั่ง build จาก Bazel มีความเร็วสูงกว่า default build tool ของแต่ละภาษามาก
Install Bazel
สำหรับการติดตั้ง … อันนี้ให้อ่านจาก docs เลยละกันนะครับ มีค่อนข้างหลาย OS หลายวิธีเลือกเอาที่สะดวกเลยครับ 😅
Let’s start !!!
ผมทำการ go mod init
โปรเจคขึ้นมาอันนึงนะครับใน main ก็จะเป็นโปรแกรม api จาก Fiber ธรรมดาอันนึง

main.go
greet.go
greet_test.go
อันนี้ก็คงจะเป็น use case หลักคือประกอบไปด้วย
- main
- module
- testing
ซึ่งตรงนี้ถ้าปกติสั่งbuild ก็แค่สั่งgo build
ก็ใช้งานได้แล้ว
Bazel
มาถึงตรงที่เราจะมา Setup Bazel ใน project เรากันแล้วว เดียวตรงนี้ผมจะอธิบายการทำงานไปทีละขึ้นตอนนะครับ
.bazelversion
อย่างแรกเราจะสร้างไฟล์ .bazelversion
ขึ้นมาแล้วเขียน version ของ Bazel ที่เราจะใช้ลงไป ซึ่งขั้นตอนนี้ไม่บังคับแต่ควรทำ ถ้าไม่สร้างไฟล์ มันจะทำการเรียกใช้เป็น version ล่าสุด
5.1.1
.bazelrc
สร้างไฟล์ .bazelrc
ซึ่งเราจะทำการ config ให้ Bazel เก็บไฟล์แคชไว้ที่ไหน อันนี้ปกติก็ใช้มีประโยชน์ตอนทำ CI บท VM สำหรับไฟล์นี้ก็ไม่บังคับเช่นกัน
build --repository_cache=~/.cache/bazel-repo
fetch --repository_cache=~/.cache/bazel-repo
query --repository_cache=~/.cache/bazel-repo
build --disk_cache=~/.cache/bazel-disk
WORKSPACE
ต่อมาเป็นไฟล์สำคัญ WORKSPACE
เป็นไฟล์ที่เราจะทำการใส่เครื่องมือที่จะใช้ใน project นี้ซึ่ง Bazel เลือกที่จะใช้ ภาษา Starlark ในการใช้เขียนไฟล์นี้
WORKSPACE
สำหรับไฟล์นี้มีความสำคัญ แล้วก็สำหรับผมเป็นอะไรที่ซับซ้อนมากใน Bazel ฉะนั้นมาอธิบายกันหน่อยว่าอะไรเป็นอะไร
อย่างคำสั่ง load
มันก็คือ import state นี่แหละ เหมือนเวลาเรา import ในภาษาอื่นๆ
คือการ import http_archive เข้ามาซึ่งตอนเริ่มต้นสิ่งที่ Bazel มีให้เราก็จะมีแต่ bazel_tools ของที่เหลือเราต้องโหลดมาเองจากในเน็ต เลยต้องใช้ http_archive
ต่อมา
คือการที่เราจะโหลดเครื่องมือในการ build go มาโดยทั่วไปใน Bazel จะเรียก rules อันนี้คือ rules_go
โค้ดส่วนนี้ส่วนใหญ่จะมีให้ ใน release ของ github แล้วเรา copy มาได้เลย ซึ่งเรามาดูดีกว่าแต่ละอันหมายความว่าอะไร
- name จะเป็นชื่อที่เราใช้ metion ในตัว workspace นี้เราสามารถเปลี่ยนได้แต่แน่นำให้ใช้ตาม official จะสะดวกและคนอื่นอ่านง่ายด้วย
- sha256 จะเป็นเหมือน token ที่เอาไว้โหลด rules ซึ่งตรงนี้ให้ copy เอาจาก repo rules นั้นๆเอาเลยถ้าใส่มั่วมันจะโหลด ไม่ผ่าน
- urls คือ source ของ rules นั้นๆ
ย้ำอีกรอบนะครับว่าตรงนี้ปกติจะ copy จาก repo เอาเลยไม่ต้องเขียนเอง
ส่วนต่อมา gazelle
อันนี้จะเป็นเครื่องมือสำหรับ generate BUILD file (จะพูดถัดไป) แล้วก็ generate dependencies ของ Go ด้วย
สำหรับอันนี้เราก็จะทำการ import ฟังชันก์ที่เราจะใช้จาก rules ที่เราโหลดมาซึ่งชื่อด้านหน้าก็จะ reference ไปที่ name ที่เราตั้งไว้
เราก็ทำการ regist พวก dependencies ให้เรียบร้อย สำหรับขั้นตอนพวกนี้ก็แนะนำให้ทำตาม docs ได้เลยเพราะ rules แต่ละตัวก็จะมีวิธีการใช้งานแตกต่างกันไป
BUILD
สำหรับ BUILD file จะเป็นไฟล์ที่เราทำการใส่คำสั่งในการสั่ง build project ไว้ซึ่งเราจะใส่ไว้ใน directory ต่างๆ
ก่อนอื่นสร้างไฟล์ชื่อ BUILD.bazel ขึ้นมาใน root directory เพราะเราจะใช้ gazelle ในการ generate BUILD file ให้เราใน directory ย่อยๆ
BUILD.bazel
โดย prefix จะเป็นชื่อ module ที่เรา init project
จากนั้นมาที่ terminal รันคำสั่ง
bazel run //:gazelle
รันครั้งแรกอาจต้องใช้เวลานานหน่อยแต่ครั้งถัดไปจะเร็วมากเพราะ Bazel จะทำการเก็บ cache ให้ไว้เรียบร้อย

รันเรียบร้อยก็จะได้ไฟล์มาเต็มเลย
สำหรับพวก folder bazel เราสามารถ ignore ใน .gitignore ได้
/bazel-*
หลังจาก run gazelle แล้วให้กลับมาที่ BUILD.bazel ตอนแรกแล้วใส่ gazelle-update-repos เพิ่มเข้าไป
BUILD.bazel
สำหรับ gazelle-update-repos จะทำการอ่านไฟล์ go.mod แล้ว generate code ทำการโหลด package ที่เราใช้ในโปรเจคลงมาที่ตัว Bazel
จากนั้น run
bazel run //:gazelle-update-repos
เราจะได้ไฟล์ชื่อ deps.bzl มาซึ่งถ้าเปิดดูจะมีพวกชื่อ package ที่เราใช้ใน project อยู่ แล้วถ้าเราลองไปเปิดดู WORKSPACE จะเจอโค้ดบรรทัดนึงเพิ่มขึ้นมา
สำหรับไฟล์ bzl จะเป็นไฟล์ที่เราสามารถ define module หรือ rule ของเราเองแล้วไป load ใน WORKSPACE หรือ BUILD ได้แต่ในบทความนี้ขอไม่พูดถึงละกันครับ
WORKSPACE
ลอง build & run
bazel run //src

เป็นอันเรียบร้อย (ไฟล์ bin จะอยู่ใน bazel-bin)
ลองรันเทสหน่อย
bazel test --test_output=errors //...
สำหรับคำสั่งนี้ก็จะคล้ายกับ go test ./...

มาอธิบายเพิ่มเติมเรื่องการทำงานของ Bazel ในการ build go หน่อยนะครับ คือตัว Bazel จะทำการ build เป็นส่วน ๆ ไปจะเห็นได้จาก ถ้าไปเปิดไฟล์ที่ตัว gazelle generate มาให้
เช่น src/BUILD.bazel
ตัว go_library จะทำหน้าที่ดูแล้วแค่ไฟล์ main.go อย่างเดียว แล้วค่อยมารวมกับไฟล์ greet (สังเกตใน deps) ส่วน package ที่ต้องให้ gazelle โหลด package มาให้ก็เพราะเหตุผลเดียวกัน เพราะแบบนี้ทำให้เวลาเรามีการแก้ไขโค้ดหรือเพิ่ม module ใหม่ขึ้นมา ตัว Bazel ก็จะ build ได้เร็วมากเพราะมันเก็บ cache ของ module อื่นๆไว้นั้นเอง
Path in Bazel
มาพูดถึงเรื่อง path เราจะใช้ // ในการ reference ถึง workspace หรือในที่นี้ก็คือ root directory ของโปรเจคนั้นเอง ส่วน :command_name เราจะใช้เรียกคำสั่งใน path นั้นๆ แต่ถ้าละไว้มันจะใส่ชื่อ command ด้วยชื่อ directory นั้นๆ เช่น
- //:gazelle หมายถึง เรียกคำสั่ง gazelle ใน BUILD file ใน //
- //src มีค่าเท่ากับ //src:src หมายถึง เรียกใช้ src ใน BUILD file ใน //src
Performance
เรามาลองเทสเรื่องความเร็วดีกว่า ว่า Setup มาขนาดนี้จะเป็นอย่างไรโดยผมจะลองสองแบบคือ มีแคช กับไม่มี (ไม่นับโหลดพวก rules นะ) แล้วก็ go build ธรรมดา
ไม่มีแคช 2.30s (bazel clean
)

แคช 0.12s

go build ไม่มีแคช 1.52s

go build แคช 0.13s

อันนี้ทดลองคร่าวๆนะครับผลอาจจะไม่ได้แม่นยำอะไรขนาดนั้นแต่จะยิงสังเกตุได้ถ้าตัว project มีขนาดใหญ่มีหลาย module หรือเราลองแก้ไข module แล้ว build ใหม่
Conclusion
มาสรุปกันดีกว่าครับ สำหรับ Bazel build tool ก็เป็นนึงใน tools ที่ผมว่าน่าสนใจเลยที่เดียวยิ่งใช้ในระบบ CI ในโปรเจคใหญ่ๆ น่าจะลด build time ได้อย่างมากแต่ก็มีข้อเสียว่า การ config ยุ่งยากมาก เห็นได้ชัดจากขนาดของโปรเจคนี้เล็กมากแต่ไฟล์ config เยอะจัดๆแล้วก็จากที่ผมลองเล่นมา community ยังไม่เยอะเท่าที่ควรกว่าจะแก้ error อะไรได้นี่เหนื่อยมากๆครับ ยังไงก็ลองเล่นกันดูครับเดี๋ยวผมแนบ repo ของโปรเจคนี้ไว้ด้านล่างอีกรอบยังไงก็ของคุณ คนที่อ่านมาถึงตรงนี้นะครับไว้พบกันใหม่บทความหน้าครับ 🎉🥳🎉🥳