0

Chán cảnh phi xe về nhà chỉ để gõ một dòng kubectl, thế là mình build hẳn Kubernetes client cho điện thoại

Câu chuyện làm Roam — app quản lý K8s không backend, kèm một con AI phân tích sự cố chạy thẳng trên máy


Cái màn hình mình ước có lúc 2h sáng. Không phải laptop. Là cái này.


Để mình kể cho anh em nghe cái lí do đã khiến mình bắt code nô gõ dòng code đầu tiên của app này.

Hôm đó T7 mình đi cà phê với team phết. Rồi điện thoại rung — email alert-manager báo về, filebeat CrashLoopBackOff.

Vấn đề là, chẳng có vẹo gì cần làm nhiều. Đọc log, cert selfsign của Elastic hết hạn - cronjob renew cert ai tắt không bật. Thế chỉ cần bật nó lên và 5p sau mọi thứ xanh.

Cả cái vụ việc chỉ có vậy. Hai mươi giây làm việc thật sự.

Còn cái giá phải trả: phi về nhà, mở laptop, ngồi chờ VPN "làm mình làm mẩy" xem hôm nay nó có thương mình không, kubectl config use-context đúng cluster, rồi gõ một câu lệnh mà đời mình chắc gõ cỡ mười nghìn lần rồi.

Bốn mươi phút của ngày T7. Đổi lấy hai mươi giây việc thật.

Và đó chưa phải lần đầu, chắc cỡ lần thứ năm mươi rồi. Mình từng nghĩ đó là cái giá phải trả khi làm on-call, chấp nhận cho xong. Nhưng hôm đó, mình quyết định rồi.

Thế là Roam ra đời.


Câu hỏi đầu tiên ai cũng hỏi: sao không dùng mấy app SSH/terminal có sẵn cho rồi?

Có thử rồi anh em ơi. Thử cả năm trời chứ ít gì.

Mấy app SSH client trên điện thoại thì đầy, có cái làm khá ngon là đằng khác. Nhưng vấn đề ở UIUX. kubectl get pods trả về một cái bảng rộng ngoằng. Điện thoại thì màn hình hẹp tí. Kết quả là ngồi vuốt ngang màn hình để soi từng cột ASCII bị wrap, cố hiểu cái READY nào thuộc về pod nào, bàn phím ảo che mất nửa màn hình, lúc 2h sáng, gõ bằng một ngón tay cái.

Không phải nó không chạy được. Mà là cảm giác đọc log và gõ lệnh trên SSH client mobile thật sự tệ.

Cái insight khiến mình bắt tay vào làm là: 95% việc mình làm khi on-call là đọc. Xem pod, đọc log, ngó qua event, hiểu cái gì đang hỏng. Chỉ khoảng 5% là ghi — restart, scale, cordon.

Terminal thì tối ưu cho cái 5% đó. Nó làm cho việc "ghi" thật chuẩn xác, còn việc "đọc" thì thành cực hình.

Một cái app mobile nên làm ngược lại. Đọc phải dễ như ăn kẹo. Còn cái tập hợp nhỏ các thao tác ghi thì phải an toàn, cân nhắc kỹ, và không thể lỡ tay bấm nhầm.

Đó là toàn bộ triết lý thiết kế. Mọi thứ bên dưới đều xuất phát từ đây.


App làm được gì

Cluster thật, ngày xui thật. Cert x509 hết hạn làm sập readiness probe — kèm cái push notification đã kéo mình vào đây.

Cái screenshot ở trên là từ một sự cố thật. Readiness probe của filebeat fail, cả cái trace của probe hiện ngay trên card: parse url OK, dns lookup OK, dial up OK, TLS handshake… x509: certificate has expired.

Lấy điện thoại từ túi quần lên là thấy đủ thông tin. App đã đẩy push notification và mình chỉ việc bấm vào.

Còn lại thì có:

Cluster overview. CPU, RAM, disk hiện dạng gauge trực quan. Số pod đang chạy, node ready, deployment health. Một màn hình duy nhất trả lời câu "có gì đang chết không".

Nodes, có mức sử dụngtheo từng node và danh sách pod đang chạy trên đó. Cộng thêm mấy thao tác bảo trì — Cordon, Uncordon, Drain, Delete. Cái Drain có PodDisruptionBudget đàng hoàng, vì drain mà bỏ qua PDB thì không còn là drain nữa.

CPU/RAM/disk theo từng node, và cái gì đang chạy ở đâu.

Workloads — Deployments, Pods, StatefulSets, DaemonSets, ReplicaSets, Jobs, CronJobs — mỗi loại đều search tức thì và lọc theo namespace được. Scale, restart, hoặc mở thẳng raw YAML.

Nhìn phát biết ngay replica health. Desired, available, up-to-date.

Live logs, kiểu terminal, streaming, có search, filter bằng regex và chip theo severity. Đây là màn hình mình dùng nhiều nhất, và cũng là màn hình mình viết lại nhiều lần nhất.

Log stream trực tiếp kèm filter theo severity. Cái chấm LIVE ở góc nghĩa là đang tail.

Raw YAML, có highlight syntax, sửa được luôn.

Đúng vậy, sửa YAML trên điện thoại được. Nhưng mình không khuyến khích bạn mò ma trận yaml ở trên điện thoại tí nào.


Nói không với backend khi làm app

Cái đội Devops, SRE sợ nhất là không biết mấy cái opensource đang dùng data nó đi đâu. Mình làm app nhưng đâu phải chỉ mình dùng, mình cũng muốn giới thiệu cho anh em khác nữa

Cho nên Roam không có backend. Không phải kiểu "backend nhưng cam kết không lấy dữ liệu của bạn" — mà là không có nơi nào để lưu datacả.

  • Kubeconfig được lưu trong keystore phần cứng của máy. Keychain trên iOS, Keystore trên Android. Phải chia nhỏ ra (chunk) vì kubeconfig có kèm cert nên hay vượt giới hạn dung lượng mỗi item.
  • App mở kết nối TLS thẳng từ điện thoại tới API server của bạn.
  • Không có bên thứ ba nào chen vào giữa. Không proxy, không relay, không telemetry đụng vào dữ liệu cluster.

Import kubeconfig, hoặc connect thẳng tới EKS/GKE. Server URL và tên cluster được parse tự động.

Cái này có cái giá thật, và mình muốn thẳng thắn về nó. Không có backend đồng nghĩa không có push notify từ server, không đồng bộ giữa các thiết bị, không có kiểu "cluster lỗi rồi này mở app đi". Chaỵ background task phải chạy on-device, vì đơn giản, OS hạn chế background execution — mà Android 16 trở đi hay iOS còn khó hơn, cái mình phát hiện ra theo kiểu "đau thương" nhất: build release tự dưng crash ngay lúc mở app vì OS cấm background execution.

Thôi oke chấp nhận. Nếu bạn là người trực tiếp on-call, mình nghĩ bạn cũng sẽ chọn vậy thôi.

Thú vị khi code

Vấn đề của việc "connect thẳng tới API server" là bạn cần một Kubernetes client thứ thiệt. TLS với custom CA, exec credential plugin, refresh token, watch stream, phân trang cho list theo chunk — đủ cả bộ.

Bạn có thể tự viết lại hết bằng TypeScript. Thư viện có rồi đấy. Nhưng dùng thư viện TS để call K8s api á, 1 đống lỗi và vấn đề hiệu năng.

Hoặc: compile thẳng client-go — chính cái thư viện mà kubectl được xây trên đó — thành thư viện native cho mobile bằng gomobile, rồi gọi vào từ React Native.

Roam làm theo cách thứ hai. Kiến trúc trông như vầy:

React Native / Expo   (UI, react-query)
        │
   TypeScript bridge   (KubeClient)
        │
Kotlin  ·  Swift       (Expo native module)
        │
      Go + client-go   (gomobile → .aar / .xcframework)
        │
   API server của bạn

Bốn ngôn ngữ trong một call path duy nhất. Nghĩa là mỗi lần mình change một hàm Go, mình phải sửa ở năm chỗ khác nhau và build lại hai native artifact - 1 cho iOS, 1 cho Android

Về mặt chất lượng thì tuyệt vời, nhưng đánh đổi là thêm 1 mớ code và build pipeline. Chỉ thêm một tham số sinceSeconds cho log streaming thôi mà mất tới hai mươi phút round-trip.

Nếu anh em nào đang cân nhắc kiến trúc kiểu này: cái lợi là có thật, nhưng nhớ chuẩn bị tinh thần là build pipeline sẽ trở thành phần bạn ghét nhất trong cả dự án.


Phần AI, và ba nguyên tắc mình tự đặt ra cho chính mình

Phân tích bằng AI, mặc định tắt. Bạn bật và tự chịu trách nhiệm thôi.

Đây là tính năng mình bỏ nhiều thời gian nhất, và cũng là cái mình tự giằng co với bản thân nhiều nhất.

Ý tưởng thì đơn giản. Một warning event của Kubernetes đơn thuần chả có thông tin gì nhiều. Phần lớn là pattern-matching dựa trên kinh nghiệm — mà đó đúng là thứ mà AI làm khá ổn. Vậy nên: khi có warning bắn ra, Roam sẽ tạo một report cấu trúc khá chuẩn.

Issue Level. Tóm tắt một dòng. Root cause. Một hướng fix vĩnh viễn, kèm confidence score và ước lượng thời gian downtime. Và một quick fix — đúng như tên gọi, chỉ tạm thời, kèm ghi chú bạn — con người — vẫn còn việc phải làm.

Sau đó app đưa luôn các câu lệnh kubectl để thực hiện.

Và chính câu cuối đó là chỗ khiến mình lo lắng, vì "AI chạy lệnh trên cluster production" là câu nghe xong ai cũng phải tăng huyết áp. Nên mình viết ra ba nguyên tắc trước khi bắt tay code tính năng này, và mình không cho phép bản thân phá luật.

Nguyên tắc 1: mọi câu lệnh đều phải qua con người review. Không có gì tự chạy hết. Model chỉ đề xuất, bạn tick chọn cái mình đồng ý, bạn bấm run. Không có chế độ tự động, và mình cũng không có dự định thêm.

Nguyên tắc 2: command nguy hiểm không bao giờ được tick sẵn. Có một setting tiện lợi cho phép tick sẵn các lệnh an toàn. Nhưng bất cứ lệnh nào bị gắn cờ nguy hiểm — delete, force, drain — đều bị loại khỏi cái setting đó, vô điều kiện, bất kể bạn cấu hình gì. Một cú tap không bao giờ được phép âm thầm nới rộng thành một lệnh delete cả cluster. Cái setting này còn chỉ được đọc đúng một lần khi mở sheet review, để việc gạt công tắc giữa chừng không thể tick ngược lại vào những ô đang hiện sẵn trên màn hình.

Nguyên tắc 3: không có gì được thực thi qua shell. Các câu lệnh không bị pipe vào shell — vì đơn giản là không có shell nào ở đây cả. Mỗi lệnh đề xuất được diễn giải lại thành một API call có kiểu (typed), đi qua đúng cái client đang chạy cho cả app. Nếu mapper không nhận ra được câu lệnh, nó được đánh dấu "manual" và trả lại cho bạn tự chạy. Blast radius của một câu lệnh do AI "ảo giác" ra bị giới hạn bởi những gì mapper có thể diễn đạt, chứ không phải bởi những gì một shell có thể làm.

Và nó chạy ngay trên điện thoại

Model dùng là llama.cpp — thông qua llama.rn — với weight GGUF quantised, tải về máy. Log và event của bạn được phân tích chỉ trên điện thoại của bạn. Không upload đi đâu hết. Mà nói thật, với cả cái lập luận "không backend" ở trên, đây là lựa chọn duy nhất không tự vả vào mặt mình.

Nếu muốn dùng model to hơn, bạn có thể trỏ tới bất kỳ endpoint nào tương thích OpenAI. Key của bạn, provider của bạn, quyền chọn của bạn — kể cả một model bạn tự host. Không có gì lấy đi data của bạn.

Mình học được gì khi viết AI pipeline cho app

Model nhỏ không cần prompt hay hơn. Nó cần một việc nhỏ hơn.

Mình từng mất một khoảng thời gian đáng xấu hổ để cố prompt-engineer cho một model local quantised xuất ra được một cục JSON báo cáo sự cố lớn, đúng định dạng, trong một lần gọi duy nhất. Prompt dài hơn. Nhiều ví dụ hơn. Chỉ thị nghiêm khắc hơn về format output.

Không vấn đề gì cả. Vấn đề là mình giao cho một model nhỏ một tác vụ có cả chục yêu cầu cùng lúc, và cái fail không nằm ở khả năng suy luận — mà nằm ở cấu trúc. Nó vẫn ra đúng root cause đấy, nhưng sai cấu trúc cần trả về và làm gãy luôn cái parser.

Cách fix nằm ở phía mình: chia nhỏ việc ra, và viết một parser giả định rằng output gần đúng thôi chứ không phải đúng tuyệt đối. Mấy model remote thì không cần. Model local thì bắt buộc phải làm vậy. Mà thành thật thì, làm theo cách khó này xong quay lại dùng model frontier, mình thấy mình cũng viết prompt tốt hơn hẳn.

Phần cứng của thiết bị là một mối quan tâm đầu tiên, chứ không phải cứ cố tống tính năng AI vào. Câu hỏi "điện thoại này có đủ RAM để load model không" là câu mình phải trả lời được trước khi cho phép bật tính năng AI. Có cả một lớp compatibility riêng mà việc duy nhất của nó là quyết định xem có nên cho hiện AI feature trên máy này hay không.

Confidence score không phải để trang trí. Việc hiện một con số confidence thay đổi hẳn cách người ta đọc một đề xuất. Mình thà ship một câu trả lời trông rõ ràng là "chưa chắc lắm" còn hơn là một câu trả lời sai bét mà tự tin quá, khiến người ta bấm approve theo quán tính.


Mấy chỗ khó hơn mình tưởng

Render log. Năm nghìn dòng log stream liên tục trên một chiếc Android tầm trung, kèm filter live, highlight khi search, và auto-scroll phải đủ thông minh để biết lúc nào nên dừng vì người dùng cần cuộn lên xem lại. Cái này khó hơn bất kỳ thứ gì liên quan tới Kubernetes trong cả dự án. Mình viết lại ba lần.

UUID data container của iOS. iOS gán cho data container của app một UUID mới mỗi lần cài lại. Thế nên AI model local lưu lúc trước download rồi sau khi cài bản build mới mất tích luôn — dù bản thân file đó vẫn được migrate đúng. Bài học: lưu gì thì lưu, nhưng luôn re-anchor đường dẫn trước khi dùng. Mất đứt một buổi tối vì cái này.

Debug app iOS thực sự cực hình, và error message trace của nó tệ hơn hẳn bất cứ thứ gì Kubernetes từng show cho mình xem. Mình nói câu này với tư cách một người đã đọc không ít event FailedScheduling trong đời.


Có làm lại lần nữa không?

Có, và lý do chính chả liên quan gì tới code cả.

Xây một cái tool cho đúng công việc mình làm mỗi ngày là một dạng cheat code. Mình chưa bao giờ phải đoán xem user muốn gì, không cần đi khảo sát, không cần tranh luận về một tính năng nào đó một cách trừu tượng. Mỗi lần không chắc một thứ gì đó có nên nằm trong app hay không, mình cứ để đó. Sớm muộn gì mình cũng on-call, gặp đúng tình huống, và biết ngay câu trả lời.

Mình chính là user. Lúc 2h sáng. Bực bội. Đó là research sản phẩm tốt nhất mình từng có, mà miễn phí.

Roam giờ đã lên cả hai store rồi.

Nếu anh em nào đang on-call cho một cluster nào đó, mình thật lòng mong anh em thử và cho mình biết chỗ nào còn dở. Mình là người duy nhất test cái này trên cluster và thói quen của chính mình bấy lâu nay, nên chắc chắn có nhiều workflow mình chưa từng nghĩ tới vì nó không phải của mình.

Bug report là một lời khen. Feature request còn là lời khen to hơn.

Và nếu anh em từng phải phi xe về nhà chỉ để gõ một câu kubectl rollout restart — bài này viết cho anh em đó.


All Rights Reserved

Viblo
Let's register a Viblo Account to get more interesting posts.