File YAML 3.000 dòng đã làm sập hệ thống của chúng tôi vào thứ Sáu
Sáu tháng trước, pipeline CI/CD của chúng tôi đã bị nghẽn bởi một file YAML duy nhất dài 3.000 dòng. Vào thời điểm đó, team của tôi đang quản lý khoảng 40 microservices trên ba môi trường khác nhau. Kho lưu trữ Git của chúng tôi đã trở thành một “nghĩa địa” của các manifest dư thừa.
Mỗi khi cần thêm một biến môi trường mới, chúng tôi phải đồng bộ mã thủ công trên hàng chục file. Vào thứ Sáu đó, một lỗi thụt lề hai khoảng trắng đặt sai chỗ trong một ConfigMap đã lọt qua bước kiểm tra (linting) nhưng lại làm hỏng logic của môi trường staging. Chúng tôi đã mất bốn giờ để debug chỉ vì một lỗi khoảng trắng.
Đây chính là giới hạn chịu đựng của “Địa ngục YAML”. Kubernetes là một bộ điều phối (orchestrator) đẳng cấp thế giới, nhưng YAML chỉ là một định dạng tuần tự hóa dữ liệu, không phải là một ngôn ngữ lập trình. Nó thiếu các công cụ cơ bản mà các kỹ sư đã tin dùng trong nhiều thập kỷ qua: vòng lặp, logic, tính trừu tượng thực thụ và kiểm soát kiểu dữ liệu mạnh mẽ (strong typing).
Tại sao YAML gặp bế tắc khi mở rộng quy mô
Vấn đề không nằm ở bản thân Kubernetes; mà là ở cách chúng ta thể hiện ý định của mình. Viết YAML thuần giống như xây dựng một tòa nhà chọc trời bằng búa nhưng không có bản thiết kế. Khi hạ tầng của bạn mở rộng quy mô, ba vấn đề lớn sẽ nảy sinh:
- Không có tính trừu tượng (Zero Abstraction): Nếu 10 dịch vụ của bạn chia sẻ 90% cấu hình giống nhau, bạn vẫn phải duy trì 10 file gần như y hệt. Sự trùng lặp này là “nam châm” thu hút sự sai lệch cấu hình (configuration drift).
- Thiếu hụt tính an toàn kiểu (Type Safety Gap): IDE của bạn sẽ không cảnh báo rằng
cpu: "500"là một chuỗi không hợp lệ cho đến khi cluster từ chối nó. Trong một manifest lớn, những lỗi nhỏ này sẽ bị ẩn giấu cho đến khi triển khai (deployment). - Mệt mỏi vì mã lặp (Boilerplate Fatigue): Một Deployment và Service tiêu chuẩn không nên yêu cầu 100 dòng văn bản lặp đi lặp lại chỉ để chạy một container.
Helm vs. Kustomize vs. CDK8s: Công cụ nào chiến thắng?
Chúng tôi không nhảy ngay sang CDK8s. Chúng tôi đã thử nghiệm các tiêu chuẩn ngành trước. Đây là kết quả thực tế trong quy trình sản xuất của chúng tôi:
1. Helm
Helm rất tuyệt vời để phân phối các package công khai, nhưng nó phụ thuộc nặng nề vào Go templates. Việc trộn lẫn YAML với {{ if .Values.enabled }} tạo ra “mã spaghetti” cực kỳ khó debug. Khi logic của bạn trở nên phức tạp, các template sẽ trở nên không thể đọc nổi.
2. Kustomize
Kustomize sử dụng các bản vá (patch) để ghi đè giá trị, điều này rất tốt vì nó là tính năng gốc của kubectl. Tuy nhiên, nó vẫn mang tính khai báo (declarative) thuần túy. Nếu bạn cần tạo một Service cho mỗi mục trong danh sách 20 microservices, bạn lại phải quay lại việc viết 20 mục thủ công.
3. CDK8s (Cloud Development Kit for Kubernetes)
CDK8s thay đổi cuộc chơi bằng cách cho phép bạn định nghĩa tài nguyên bằng TypeScript, Python hoặc Java. Nó biên dịch mã của bạn thành YAML Kubernetes tiêu chuẩn và hợp lệ. Bạn có quyền truy cập vào các class, vòng lặp và điều kiện trong khi vẫn xuất ra các manifest mà Kubernetes mong đợi. Nó thu hẹp khoảng cách giữa kỹ thuật phần mềm và vận hành (operations).
Quá trình chuyển đổi: Chuyển sang CDK8s
Sau một tháng thử nghiệm, chúng tôi đã di chuyển các dịch vụ cốt lõi sang CDK8s bằng TypeScript. Sự thay đổi này đã biến chúng tôi từ những “người chỉnh sửa YAML” thành các Kỹ sư Nền tảng (Platform Engineers). Thay vì quản lý dữ liệu tĩnh, chúng tôi bắt đầu xây dựng các thư viện hạ tầng có thể tái sử dụng.
Thiết lập môi trường của bạn
Để bắt đầu, bạn sẽ cần CLI của CDK8s. Tôi khuyên dùng Node.js cho việc thiết lập ban đầu vì tính năng tự động hoàn thành (autocompletion) của TypeScript trong VS Code cực kỳ hữu ích khi điều hướng các schema phức tạp của Kubernetes.
# Cài đặt CLI trên toàn hệ thống
npm install -g cdk8s-cli
# Khởi tạo một thư mục dự án mới
mkdir k8s-infrastructure && cd k8s-infrastructure
cdk8s init typescript-app
Thay thế Copy-Paste bằng Logic
Thay vì sao chép các khối mã, bạn có thể đóng gói logic của mình vào một class có thể tái sử dụng. Dưới đây là một ví dụ thực tế về cách tôi thu gọn định nghĩa dịch vụ web tiêu chuẩn của chúng tôi thành một construct TypeScript duy nhất:
import { Construct } from 'constructs';
import { App, Chart, ChartProps } from 'cdk8s';
import { KubeDeployment, KubeService, IntOrString } from './imports/k8s';
export class WebService extends Construct {
constructor(scope: Construct, id: string, port: number, image: string) {
super(scope, id);
const label = { app: id };
new KubeService(this, 'service', {
spec: {
type: 'ClusterIP',
ports: [ { port, targetPort: IntOrString.fromNumber(port) } ],
selector: label
}
});
new KubeDeployment(this, 'deployment', {
spec: {
replicas: 2,
selector: { matchLabels: label },
template: {
metadata: { labels: label },
spec: {
containers: [
{ name: 'web', image: image, ports: [ { containerPort: port } ] }
]
}
}
}
});
}
}
Khởi tạo các dịch vụ
Giờ đây, việc thêm một dịch vụ mới chỉ tốn một dòng mã. Chúng ta có thể lặp qua một đối tượng cấu hình để tạo ra nhiều deployment ngay lập tức:
class MyChart extends Chart {
constructor(scope: Construct, id: string, props: ChartProps = { }) {
super(scope, id, props);
const apps = [
{ name: 'auth-api', port: 8080, image: 'myorg/auth:v1.2.4' },
{ name: 'cart-api', port: 8081, image: 'myorg/cart:v2.1.0' },
{ name: 'catalog-api', port: 8082, image: 'myorg/catalog:v1.0.5' }
];
apps.forEach(app => {
new WebService(this, app.name, app.port, app.image);
});
}
}
const app = new App();
new MyChart(app, 'production');
app.synth();
Khi bạn chạy cdk8s synth, script này sẽ tạo ra một file YAML duy nhất được định dạng hoàn hảo. Nếu cần cập nhật số lượng bản sao (replica count) từ 2 lên 5 cho mọi dịch vụ, chúng tôi chỉ cần thay đổi một dòng trong class thay vì chỉnh sửa 40 file riêng biệt.
Kết quả: Giảm 70% mã, Tăng 100% sự tự tin
Sau khi vận hành hệ thống này trong môi trường production suốt sáu tháng, kết quả có thể đo lường được. Chúng tôi đã giảm tổng số dòng mã hạ tầng xuống hơn 70%. Các lỗi cú pháp về cơ bản đã biến mất vì trình biên dịch TypeScript sẽ bắt lỗi ngay trong quá trình phát triển. Nếu tôi vô tình truyền một chuỗi vào trường giới hạn CPU, IDE của tôi sẽ gạch chân đỏ lỗi đó ngay lập tức.
Về mặt kỹ thuật, chiến thắng lớn nhất là kiểm thử đơn vị (unit testing). Hiện tại chúng tôi sử dụng Jest để xác thực hạ tầng của mình. Chúng tôi có thể khẳng định bằng lập trình rằng mọi Deployment phải bao gồm các bối cảnh bảo mật (security contexts) hoặc giới hạn tài nguyên cụ thể trước khi YAML được tạo ra. Mức độ xác thực này gần như không thể thực hiện được với YAML thuần hoặc Helm.
Lời kết
Áp dụng CDK8s không có nghĩa là bạn từ bỏ Kubernetes; nó có nghĩa là cuối cùng bạn đã đối xử với hạ tầng của mình như một phần mềm. Nếu các Helm chart của bạn đang trở nên khó đọc hoặc các file YAML của bạn đang kéo dài hàng ngàn dòng, đã đến lúc phải thay đổi. Việc thiết lập ban đầu có thể tốn thêm chút công sức, nhưng việc giảm thiểu bảo trì về lâu dài và tăng cường sự tự tin khi triển khai là hoàn toàn xứng đáng với sự đầu tư đó.

