CI/CD Đa Nền Tảng Với Dagger: Lập Trình Hóa Quy Trình Build và Test Vượt Xa Giới Hạn YAML

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

Nỗi Thất Vọng Với Các Quy Trình YAML Dễ Gãy

Hầu hết kỹ sư DevOps đều từng đốt cả buổi chiều chỉ để "git push và cầu may". Bạn commit, chờ năm phút để remote runner nhận job, nhìn nó thất bại vì một lỗi chính tả ngớ ngẩn, rồi lại lặp lại quy trình đó. Lịch sử commit nhanh chóng tràn ngập các thông điệp như fix: trigger pipeline, fix: missing quote in step 4, hay debug ci again (final).

Các hệ thống CI truyền thống ép bạn phải dùng các file YAML khai báo (declarative) vốn rất dễ phình to ngoài tầm kiểm soát. Những gì ban đầu chỉ là file cấu hình gọn gàng 20 dòng sẽ nhanh chóng biến thành con quái vật 600 dòng nhồi nhét đầy các lệnh bash nối chuỗi, cú pháp riêng biệt của từng nhà cung cấp và lớp mã chắp vá lỏng lẻo.

Cách tiếp cận tập trung vào YAML này tạo ra hai nút thắt cổ chai dai dẳng:

  • Khả năng tái lập ở local bằng không: Bạn không thể chạy các bước của GitHub Actions, GitLab CI hay CircleCI ngay trên máy trạm của mình với độ chuẩn xác tuyệt đối. Các công cụ như act có thể hỗ trợ phần nào, nhưng chúng thường hoạt động khác so với runner thực tế trên production. Mọi tinh chỉnh nhỏ nhất đều đòi hỏi một vòng lặp đẩy mã lên server từ xa.
  • Phụ thuộc vào nhà cung cấp (Vendor lock-in): Chuyển đổi pipeline từ GitLab sang GitHub Actions hoặc AWS CodeBuild đồng nghĩa với việc phải viết lại toàn bộ workflow từ đầu. Quá trình di chuyển này ngốn hàng tuần làm việc của kỹ sư và rất dễ phát sinh sai lệch cấu hình.

Giải pháp ở đây rất đơn giản: hãy đối xử với pipeline như một phần mềm thực thụ. Dagger hiện thực hóa điều này bằng cách cho phép bạn định nghĩa các quy trình tự động hóa bằng những ngôn ngữ lập trình tiêu chuẩn như Python, Go hoặc TypeScript.

Dagger Tái Cấu Trúc Logic Phân Phối Phần Mềm Như Thế Nào

Dagger là một engine CI/CD có thể lập trình được, xây dựng trên nền tảng Moby BuildKit—cùng một engine đã được kiểm chứng qua thực tế dùng để build Docker hiện đại. Thay vì phân tích các file YAML tĩnh, Dagger cung cấp một engine nhúng được điều khiển thông qua typed GraphQL API. Bạn tương tác với engine này qua các client SDK bằng ngôn ngữ lập trình ưa thích của mình.

Container Dưới Dạng Đối Tượng Phần Mềm Hạng Nhất

Trong Dagger, container không chỉ đơn thuần là môi trường thực thi. Chúng là các đối tượng bất biến (immutable objects) trong mã nguồn. Bạn khởi tạo một base image, mount thư mục, thiết lập biến môi trường và xâu chuỗi các thao tác. Mỗi thao tác trả về một tham chiếu container mới, biến việc xây dựng pipeline thành phong cách lập trình hàm (functional programming) mạch lạc.

Đồ Thị DAG Khai Báo và Tự Động Caching

BuildKit tự động biên dịch các lệnh gọi hàm của bạn thành Đồ thị có hướng không chu trình (DAG). Nếu một thao tác trung gian không thay đổi—chẳng hạn như thư mục node_modules không bị chỉnh sửa hay một binary dependency tĩnh—Dagger sẽ bỏ qua việc thực thi và tái sử dụng các layer đã cache. Một lượt build từng mất 8 phút trên runner nguội (cold runner) có thể hoàn thành trong chưa đầy 35 giây khi đã có cache, bất kể nó chạy trên laptop của bạn hay bên trong remote runner.

Hệ Sinh Thái Công Cụ Nhất Quán

Tại sao phải bắt đội ngũ của bạn học các ngôn ngữ đặc thù (DSL) độc quyền của từng nền tảng CI khi họ đã quá quen thuộc với Python hoặc TypeScript? Với Dagger, các nhà phát triển được sử dụng toàn bộ công cụ IDE hiện có. Bạn có sẵn tính năng auto-completion, linting, type safety, modular import và các framework unit test tiêu chuẩn ngay khi bắt đầu.

Thực Hành: Build và Test Một Dịch Vụ Bằng Python và Dagger

Hãy cùng xây dựng một pipeline có khả năng tái lập cho một dự án Node.js TypeScript bằng Dagger Python SDK. Pipeline này sẽ cô lập các dependency, chạy bộ kiểm thử và trích xuất artifact build cho production.

1. Thiết Lập Môi Trường Local

Đảm bảo Docker hoặc một container runtime tương thích (như OrbStack hoặc Podman) đang chạy trên máy của bạn. Tạo một thư mục làm việc mới và cài đặt SDK:

mkdir dagger-pipeline && cd dagger-pipeline
python3 -m venv .venv
source .venv/bin/activate
pip install dagger-io anyio

2. Viết Script Pipeline

Tạo một file có tên ci.py. Chúng a sẽ định nghĩa một workflow bất đồng bộ để kéo mã nguồn, cài đặt package, chạy test và xuất thư mục phân phối đã biên dịch.

import sys
import anyio
import dagger

async def main():
    # Khởi tạo kết nối tới Dagger engine
    async with dagger.connection(dagger.Config(log_output=sys.stderr)) as client:
        # Loại trừ các file rác ở local để giữ build context luôn nhẹ
        src = client.host().directory(".", exclude=[".venv", "node_modules", ".git", "build-artifacts"])

        # 1. Chuẩn bị môi trường Node.js base
        node = (
            client.container()
            .from_("node:20-alpine")
            .with_directory("/app", src)
            .with_workdir("/app")
        )

        # 2. Cài đặt các dependency
        installed = node.with_exec(["npm", "ci"])

        # 3. Chạy unit test
        print("==> Đang chạy bộ test...")
        test_runner = installed.with_exec(["npm", "test"])
        test_output = await test_runner.stdout()
        print(test_output)

        # 4. Tạo bản build production và xuất ra máy host
        print("==> Đang tạo production bundle...")
        built = installed.with_exec(["npm", "run", "build"])
        dist_dir = built.directory("/app/dist")

        await dist_dir.export("./build-artifacts")
        print("Đã ghi build artifact thành công vào ./build-artifacts")

if __name__ == "__main__":
    anyio.run(main)

3. Chạy và Debug ở Local

Thực thi pipeline từ terminal của bạn:

python ci.py

Dagger kết nối tới BuildKit, thực thi từng bước bên trong các container cô lập và ghi các asset đã biên dịch trực tiếp vào ./build-artifacts. Khi một bài test thất bại, bạn sẽ nhận được output terminal ngay lập tức kèm toàn bộ stack trace. Không còn cảnh phải xếp hàng chờ đợi các cloud runner dùng chung.

4. Tích Hợp Vào GitHub Actions

Vì toàn bộ logic nghiệp vụ của bạn giờ đây nằm trong code tiêu chuẩn, nhà cung cấp CI chỉ việc gọi Python. Dưới đây là toàn bộ nội dung file .github/workflows/ci.yml:

name: Continuous Integration

on:
  push:
    branches: [main]
  pull_request:

jobs:
  pipeline:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout mã nguồn
        uses: actions/checkout@v4

      - name: Thiết lập Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: Cài đặt Dagger SDK
        run: pip install dagger-io anyio

      - name: Thực thi Pipeline
        run: python ci.py

Bạn cần chạy đúng workflow này trong GitLab CI hoặc một agent Jenkins nội bộ? Chỉ cần chạy python ci.py bên trong bất kỳ runner nào hỗ trợ Docker. Logic thực thi không hề thay đổi dù chỉ một dòng.

Thực Tiễn Tốt Nhất Cho Môi Trường Production

Chuyển từ thử nghiệm sang các hệ thống production đòi hỏi một vài nguyên tắc kiến trúc sau:

  • Tối ưu build context: Luôn sử dụng danh sách loại trừ tường minh trong host().directory(). Việc tải cả thư mục node_modules nặng 2 GB hoặc lịch sử .git thô vào engine sẽ làm chậm quá trình thực thi và lãng phí I/O ổ đĩa.
  • Kiểm thử logic pipeline với Pytest: Bạn không cần một Docker daemon đang chạy để xác thực các tham số cấu hình cơ bản. Hãy viết các unit test nhẹ cho bộ phân tích đối số (argument parser), quy tắc build matrix và kiểm tra hợp lệ của môi trường.
  • Lưu trữ BuildKit cache trên các runner tạm thời: Các CI runner trên cloud thường xóa sạch ổ đĩa local sau mỗi lần chạy. Hãy xuất layer cache sang một OCI registry bên ngoài bằng các cờ cache của Dagger để các pull request không phải build lại base image từ đầu.
  • Bảo vệ thông tin đăng nhập nhạy cảm: Không bao giờ truyền secret dưới dạng chuỗi dòng lệnh thuần. Hãy dùng client.set_secret() hoặc đọc chúng qua client.host().env_variable().secret(). Dagger sẽ tự động ẩn các giá trị này khỏi log và loại chúng khỏi các snapshot container trung gian.

Tổng Kết

YAML hoạt động tốt cho các tham số key-value đơn giản, nhưng sẽ bộc lộ điểm yếu khi phải điều phối các pipeline phân phối phức tạp của doanh nghiệp. Bằng cách chuyển sang Dagger, bạn mang các thực tiễn kỹ thuật chuẩn mực—tính module hóa, khả năng kiểm thử và cơ chế caching tất định—vào hệ thống build của mình, đồng thời xóa bỏ hoàn toàn lý do bào chữa quen thuộc: "chạy bình thường trên máy tôi mà".

Share: