Continuous Integration
รวมและทดสอบทั้งระบบอัตโนมัติทุกครั้งที่ code เปลี่ยน
คืออะไร
หัวข้อที่มีชื่อว่า “คืออะไร”Continuous Integration (CI) เป็นแนวปฏิบัติจาก Extreme Programming (XP) ที่รับประกันว่าปัญหาของทั้งระบบจะถูกตรวจพบให้เร็วที่สุดเท่าที่จะเป็นไปได้หลังจากมันถูกใส่เข้ามา
มันหมายถึงการ build และทดสอบทั้งระบบโดยอัตโนมัติ (คือการรวมระบบเข้ากับทุกสิ่งที่มันพึ่งพา) หลังการเปลี่ยนแปลงทุกครั้ง โดยทั่วไปคือทุกครั้งที่มีการ commit เข้าสู่ mainline ของระบบควบคุม version Continuous Integration คือการผลักแนวปฏิบัติ “daily build” — ซึ่งครั้งหนึ่งเคยถูกยกให้เป็น best practice ที่ควรมุ่งไปให้ถึง — ให้สุดขั้ว จากที่เคย build วันละครั้ง กลายเป็น build ทุกครั้งที่มีการเปลี่ยนแปลงเข้ามา
ผลลัพธ์คือ feedback loop ที่สั้นที่สุดเท่าที่จะเป็นไปได้ ระหว่างเวลาที่ปัญหาถูกใส่เข้ามาในระบบ กับเวลาที่มันถูกตรวจพบโดยอัตโนมัติ Martin Fowler อธิบายว่าเหตุผลที่ CI ทรงพลังมาจากธรรมชาติของงาน integration เอง — ความยากในการ merge code สองก้อนเข้าด้วยกันไม่ได้เพิ่มแบบเชิงเส้นตามขนาดของการเปลี่ยนแปลง แต่เพิ่มแบบไม่เป็นสัดส่วน (non-linear) ยิ่งปล่อยให้ code แยกกันนานเท่าไร การรวมกลับเข้ามาก็ยิ่งเจ็บปวดมากขึ้นเท่านั้น การ integrate บ่อย ๆ ในปริมาณเล็ก ๆ จึงถูกกว่าการ integrate นาน ๆ ครั้งในปริมาณใหญ่เสมอ
ทำงานร่วมกับ Fail Fast: CI คือการนำหลัก Fail Fast มาใช้ในระดับทั้งระบบ ยิ่งพบข้อผิดพลาดใกล้เวลาที่มันเกิดมากเท่าไร ก็ยิ่งหาสาเหตุและแก้ได้ถูกและง่ายขึ้นเท่านั้น เพราะบริบท (context) ของการเปลี่ยนแปลงยังสดใหม่อยู่ในหัวของผู้เขียน code
สิ่งสำคัญที่ต้องแยกให้ออกคือ CI เป็นเรื่องของ กระบวนการรวม code และตรวจสอบคุณภาพ ไม่ใช่ Continuous Delivery (CD) ที่พูดถึงการทำให้ระบบพร้อม release ได้ตลอดเวลา หรือ Continuous Deployment ที่ปล่อยขึ้น production อัตโนมัติ — CI คือรากฐานที่ทำให้ CD และ Continuous Deployment เป็นไปได้ แต่ทั้งสามเป็นคนละขั้นตอนกัน
ทำอย่างไร
หัวข้อที่มีชื่อว่า “ทำอย่างไร”การนำ CI มาใช้จริงต้องอาศัยหลายองค์ประกอบทำงานร่วมกัน (สรุปจากแนวทางของ Fowler และแนวปฏิบัติที่ยอมรับกันทั่วไป):
- เก็บทุกอย่างไว้ใน version control เดียว — source code script build file config schema ของฐานข้อมูล ต้องอยู่ใน repository เดียวกัน เพื่อให้ใครก็ตามสามารถ build ระบบทั้งหมดจากเครื่องเปล่า ๆ ได้
- ทำ build ให้เป็นอัตโนมัติด้วยคำสั่งเดียว — ไม่ว่าจะเป็น
dotnet build, MSBuild, หรือ build tool อื่น ต้องไม่มีขั้นตอนที่ต้องทำมือแทรกอยู่ - ทำให้ build ทดสอบตัวเองได้ (self-testing) — build ต้องรวมการรัน automated test เข้าไปด้วย ไม่ใช่แค่ compile ผ่านแล้วจบ
- ทุกคน commit เข้า mainline อย่างน้อยวันละครั้ง — Kent Beck เคยกล่าวไว้ว่าไม่ควรมี code ที่ยังไม่ถูก integrate ค้างอยู่นานเกินสองสามชั่วโมง
- ทุก commit ต้อง build บนเครื่อง integration ที่แยกจากเครื่อง dev — เพื่อตัดปัญหา “มันรันได้บนเครื่องผม” ออกไป โดยทั่วไปใช้ CI server เช่น GitHub Actions, Azure Pipelines, หรือ Jenkins คอยเฝ้าดู mainline แล้ว trigger build อัตโนมัติ
- แก้ build ที่พังให้เร็วที่สุด — เมื่อ build แดง ไม่มีงานไหนสำคัญกว่าการทำให้มันเขียวอีกครั้ง บ่อยครั้งวิธีที่เร็วที่สุดคือ revert commit ที่ทำให้พัง แล้วค่อยแก้ทีหลัง
- รักษาความเร็วของ build ให้สั้น — เป้าหมายทั่วไปคือ build ที่ commit ต้องเสร็จในเวลาประมาณสิบนาที หากมี test ชุดใหญ่ที่ช้า ให้แยกไปรันใน pipeline ขั้นถัดไป (deployment pipeline แบบมีหลายขั้น)
- ทดสอบในสภาพแวดล้อมที่ใกล้เคียง production — เพื่อลดโอกาสที่ปัญหาจะโผล่มาเฉพาะตอน deploy จริง
- ทำให้ทุกคนเห็นสถานะของ build ได้ง่าย — dashboard การแจ้งเตือนใน chat หรือ badge ใน README ช่วยให้ทีมรู้ทันทีเมื่อ mainline พัง
CI ไม่ได้จำกัดอยู่แค่การ compile — มันควรครอบคลุมการรัน static analysis, linter, security scan และ automated test ในระดับต่าง ๆ (unit, integration) เพื่อให้ “เขียว” หมายถึงคุณภาพในหลายมิติ ไม่ใช่แค่ compile ผ่าน
flowchart LR
Dev[Developer commits code] --> Repo[Push to mainline]
Repo --> Trigger[CI server detects change]
Trigger --> Build[Automated build]
Build --> Test[Automated tests run]
Test -->|pass| Green[Build green feedback to team]
Test -->|fail| Red[Build red fix immediately]
Red --> Dev
ตัวอย่าง
หัวข้อที่มีชื่อว่า “ตัวอย่าง”ตัวอย่าง GitHub Actions workflow ง่าย ๆ ที่ทำ CI สำหรับ project .NET — ทุกครั้งที่มี push หรือเปิด pull request เข้า main จะ build และรัน test อัตโนมัติ:
name: CI
on: push: branches: [main] pull_request: branches: [main]
jobs: build-and-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: ตั้งค่า .NET SDK uses: actions/setup-dotnet@v4 with: dotnet-version: "8.0.x"
- name: คืนค่า dependency run: dotnet restore
- name: Build project run: dotnet build --no-restore --configuration Release
- name: รัน automated test ทั้งหมด run: dotnet test --no-build --configuration Release --logger trxฝั่ง code C# เอง ก็ควรเขียนให้ “self-testing” ได้ตั้งแต่ต้น เช่น unit test ที่ CI จะรันทุกครั้ง:
public class OrderTotalCalculatorTests{ [Fact] public void CalculateTotal_WithTwoLineItems_ReturnsSumOfLinePrices() { // Arrange: จำลองคำสั่งซื้อสองรายการ var calculator = new OrderTotalCalculator(); var order = new Order(new List<OrderLine> { new OrderLine(unitPrice: 100m, quantity: 2), // 200 new OrderLine(unitPrice: 50m, quantity: 1) // 50 });
// Act decimal total = calculator.CalculateTotal(order);
// Assert: ผลรวมต้องถูกต้อง — ถ้าใครแก้ logic แล้วพลาด CI จะจับได้ทันที Assert.Equal(250m, total); }}เมื่อ CI server รันทดสอบทุก commit ทีมจะรู้ทันทีถ้าใครทำ regression เข้ามา แทนที่จะไปเจอตอน integrate ใหญ่ก่อน release ซึ่งสายเกินไปและแก้ยากกว่ามาก
ประโยชน์และข้อควรระวัง
หัวข้อที่มีชื่อว่า “ประโยชน์และข้อควรระวัง”ประโยชน์
- ลดความเสี่ยงตอน release — เพราะ mainline ถูกทดสอบและ integrate อยู่ตลอดเวลา ไม่มี “ช่วง integration” ก้อนใหญ่ที่น่ากลัวก่อนส่งมอบ
- ลดต้นทุนของการ integrate — เนื่องจากความยากของการ merge เพิ่มแบบไม่เป็นเชิงเส้นตามขนาดการเปลี่ยนแปลง การรวม code ทีละน้อยบ่อย ๆ จึงถูกกว่าการรวมก้อนใหญ่นาน ๆ ครั้งเสมอ
- พบ defect เร็วขึ้นและแก้ถูกกว่า — ตรงกับหลัก Fail Fast ยิ่ง bug ถูกจับใกล้จุดที่มันเกิด ยิ่งแก้ง่ายและถูก
- สนับสนุนการ refactor อย่างต่อเนื่อง — เมื่อไม่มี branch ที่แยกออกไปนาน ๆ ทีมกล้าปรับโครงสร้าง code โดยไม่กลัว merge conflict มหาศาล
- ทำให้การตัดสินใจ release เป็นเรื่องธุรกิจ ไม่ใช่ข้อจำกัดทางเทคนิค — เมื่อ mainline พร้อม deploy อยู่เสมอ ทีมเลือกได้เองว่าจะปล่อยเมื่อไร
ข้อควรระวัง
- CI ไม่ทำงานถ้าไม่มีชุดทดสอบอัตโนมัติที่น่าเชื่อถือ — build ที่ “เขียว” ทั้งที่ไม่มี test ครอบคลุมเพียงพอ ให้ความมั่นใจปลอม (false confidence)
- ต้องมีทีมที่ทำงานร่วมกันสม่ำเสมอ — Fowler ชี้ว่า CI เหมาะกับทีมที่ commit เข้า mainline เป็นประจำ ไม่เหมาะกับโครงการที่ contributor เข้ามาแบบห่าง ๆ
- build ที่ช้าทำลายวินัยของทีม — ถ้า build ใช้เวลานานเกินไป คนจะเริ่มเลี่ยงการ commit บ่อย ๆ ซึ่งย้อนกลับไปสู่ปัญหาเดิมที่ CI พยายามแก้
- build พังแล้วปล่อยทิ้งไว้ = CI ตายทั้งระบบ — ถ้าไม่มีวินัยว่า “ไม่มีงานไหนสำคัญกว่าการแก้ build แดง” สถานะ build จะกลายเป็นสิ่งที่ทีมเพิกเฉย และสูญเสียคุณค่าของ CI ไปทั้งหมด
- ไม่ใช่ Continuous Delivery หรือ Continuous Deployment — CI ทำให้ code build และทดสอบผ่านอัตโนมัติเท่านั้น การส่งขึ้น production อัตโนมัติเป็นอีกขั้นที่ต้องออกแบบ pipeline เพิ่มเติม (feature flag, deployment pipeline แบบหลายขั้น) ต่อยอดจากรากฐาน CI