Tại sao các Bash script đang làm khó bạn
Triển khai một file YAML là phần dễ dàng. Giữ cho một ứng dụng phức tạp hoạt động ổn định trên môi trường production mới là lúc mọi thứ trở nên rắc rối. Tôi thường thấy các đội ngũ dựa dẫm vào các Bash script chắp vá, Jenkins pipeline, hoặc các thủ thuật kubectl thủ công để xử lý logic vận hành. Những script này thường giải quyết các tác vụ như migration schema database, tái cân bằng shard, hoặc xoay vòng certificate.
Cơn đau đầu thực sự là các script mang tính “mệnh lệnh” (imperative). Chúng chạy một chuỗi các bước rồi dừng lại. Nếu một sự cố mạng làm script bị ngắt ở bước 4 trên 10, bạn sẽ bị bỏ lại với một bản triển khai dang dở và một cuộc gọi xử lý sự cố lúc 2 giờ sáng. Script không có bộ nhớ. Nếu môi trường bị sai lệch (drift) năm phút sau khi script kết thúc, sẽ không có gì ở đó để khắc phục. Điều này tạo ra “nợ vận hành” (operational debt), buộc các kỹ sư phải trông chừng quá trình tự động hóa thay vì tập trung phát triển tính năng mới.
Khoảng cách về trí tuệ trong các Controller tiêu chuẩn
Kubernetes dựa trên một Control Loop (Vòng lặp kiểm soát). Các controller có sẵn cho Deployment hoặc StatefulSet hoạt động 24/7 để đảm bảo trạng thái hiện tại khớp với trạng thái mong muốn của bạn. Tuy nhiên, các controller này mang tính tổng quát. Chúng biết một Pod nên ở trạng thái “Running”, nhưng chúng không biết rằng ứng dụng cụ thể của bạn cần 30 giây khởi động (warm-up) hoặc một lệnh đăng ký API cụ thể trước khi có thể tiếp nhận lưu lượng truy cập.
Các tài nguyên tiêu chuẩn thiếu đi “trí tuệ nghiệp vụ” (domain intelligence) cho hệ thống cụ thể của bạn. Khi chúng ta lấp đầy khoảng trống này bằng các script bên ngoài, chúng ta sẽ mất đi sự kỳ diệu của tính khai báo (declarative) trong Kubernetes. Chúng ta cần một cách để dạy cho cluster cách quản lý ứng dụng của mình như thể nó là một tài nguyên bản địa thực thụ.
Giải pháp: Kubernetes Operators
Một Operator về cơ bản là kiến thức vận hành của con người được đóng gói vào phần mềm. Nó kết hợp Custom Resource Definitions (CRDs) với một Custom Controller. Thay vì kích hoạt một script, bạn định nghĩa “Trạng thái mong muốn” (Desired State) trong một file YAML. Sau đó, Operator sẽ làm việc liên tục để biến trạng thái đó thành hiện thực.
Để xây dựng chúng một cách hiệu quả, chúng ta sử dụng thư viện controller-runtime. Đây chính là engine đứng sau Operator SDK và Kubebuilder. Nó xử lý các phần việc nhàm chán — như giao tiếp API, caching tài nguyên và hàng đợi sự kiện — để bạn có thể tập trung hoàn toàn vào logic nghiệp vụ của mình. Theo kinh nghiệm của tôi, việc chuyển sang mô hình này có thể giảm thời gian phục hồi (MTTR) từ 15 phút can thiệp thủ công xuống dưới 500ms thông qua đối soát (reconciliation) tự động.
Thực hành: Xây dựng Operator đầu tiên của bạn
Hãy cùng xây dựng một Operator cơ bản cho một tài nguyên tùy chỉnh có tên là AppService. Operator này sẽ đảm bảo rằng mọi AppService tự động duy trì Deployment và ConfigMap riêng của nó.
1. Khởi tạo dự án
Bạn sẽ cần Go và Operator SDK. Bắt đầu bằng cách tạo một thư mục mới và khởi tạo khung dự án (scaffolding):
mkdir app-operator
cd app-operator
operator-sdk init --domain itfromzero.com --repo github.com/itfromzero/app-operator
2. Tạo API
Bây giờ, chúng ta định nghĩa Custom Resource. Điều này thông báo cho Kubernetes API server về loại đối tượng mới của chúng ta.
operator-sdk create api --group apps --version v1alpha1 --kind AppService --resource --controller
3. Định nghĩa Trạng thái mong muốn
Mở file api/v1alpha1/appservice_types.go. Chúng ta cần cho Kubernetes biết tài nguyên của mình yêu cầu những trường nào, chẳng hạn như số lượng replica và một thông điệp cấu hình.
type AppServiceSpec struct {
Size int32 `json:"size"`
Message string `json:"message"`
}
type AppServiceStatus struct {
Nodes []string `json:"nodes"`
}
Sau khi cập nhật các struct này, hãy chạy make generate và make manifests. Việc này sẽ cập nhật mã nguồn được tạo tự động và các file YAML định nghĩa CRD của bạn.
4. Triển khai Vòng lặp Đối soát (Reconcile Loop)
Hàm Reconcile trong internal/controller/appservice_controller.go là nơi phép màu xảy ra. Nó được kích hoạt bất cứ khi nào AppService thay đổi. Hãy tư duy logic theo hướng: Quan sát -> Phân tích -> Phản hồi.
func (r *AppServiceReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
// 1. Lấy instance AppService
appService := &appsv1alpha1.AppService{}
err := r.Get(ctx, req.NamespacedName, appService)
if err != nil {
return ctrl.Result{}, client.IgnoreNotFound(err)
}
// 2. Kiểm tra xem Deployment đã tồn tại chưa; tạo mới nếu thiếu
found := &appsv1.Deployment{}
err = r.Get(ctx, types.NamespacedName{Name: appService.Name, Namespace: appService.Namespace}, found)
if err != nil && errors.IsNotFound(err) {
dep := r.deploymentForAppService(appService)
log.Info("Đang tạo Deployment mới", "Namespace", dep.Namespace, "Name", dep.Name)
err = r.Create(ctx, dep)
if err != nil {
return ctrl.Result{}, err
}
return ctrl.Result{Requeue: true}, nil
}
// 3. Đồng bộ hóa số lượng replicas với spec
size := appService.Spec.Size
if *found.Spec.Replicas != size {
found.Spec.Replicas = &size
err = r.Update(ctx, found)
if err != nil {
return ctrl.Result{}, err
}
}
return ctrl.Result{}, nil
}
5. Triển khai và Kiểm thử
Sử dụng Kind (Kubernetes in Docker) để kiểm thử cục bộ. Cài đặt các CRD và chạy controller của bạn bằng các lệnh sau:
make install
make run
Trong một terminal riêng biệt, hãy apply một tài nguyên mẫu:
apiVersion: apps.itfromzero.com/v1alpha1
kind: AppService
metadata:
name: example-app
spec:
size: 3
message: "Xin chào từ Operator"
Sau khi bạn chạy kubectl apply, hãy theo dõi logs. Controller sẽ ngay lập tức phát hiện tài nguyên mới và khởi tạo các pod của bạn.
Lợi thế của khả năng Tự phục hồi
Tại sao phải tốn công sức như vậy? Câu trả lời là Khả năng tự phục hồi (Self-Healing). Nếu một kỹ sư mới vào nghề vô tình xóa Deployment của bạn, vòng lặp Reconcile sẽ bắt được sự cố ngay lập tức. Nó quan sát tài nguyên bị thiếu, phân tích sự sai lệch và tạo lại nó trong vòng vài mili giây. Một Bash script chạy bằng cron có thể cuối cùng cũng sửa được lỗi này, nhưng một Operator cung cấp khả năng phục hồi theo thời gian thực.
Go cũng cung cấp cho bạn quyền truy cập vào envtest và các framework unit testing mạnh mẽ. Bạn có thể mô phỏng các lỗi phức tạp, như timeout API hoặc phân tách mạng, đảm bảo logic của bạn sẵn sàng cho môi trường production trước khi nó chạm tới cluster.
Kết luận
Chuyển từ các script thủ công sang các Operator tùy chỉnh là một cột mốc quan trọng đối với bất kỳ đội ngũ DevOps nào. Nó chuyển đổi “tri thức bộ lạc” (tribal knowledge) — những kinh nghiệm nằm trong đầu các kỹ sư kỳ cựu — thành mã nguồn đáng tin cậy và có phiên bản. Mặc dù việc học Go và Kubernetes API đòi hỏi nỗ lực, nhưng sự ổn định và khả năng mở rộng mà bạn nhận được hoàn toàn xứng đáng với từng giờ học tập.

