↓Chuyển đến nội dung chính
  1. Bài viết/

Bài 01: cẩm nang chuyển bến từ Docker sang orchestration

Docker chạy tốt trên một máy, nhưng app trải ra nhiều máy thì cần một lớp phần mềm điều hành cả cụm. Bài này giải thích lớp đó qua K3s: các mảnh ghép, đường đi của một request, và việc cần chuẩn bị trước khi cài.

I. Giới hạn của một máy lẻ #

Docker chạy tốt trên một máy, nhưng app trải ra nhiều máy thì cần một lớp phần mềm điều hành cả cụm. Bài này giải thích lớp đó qua K3s: các mảnh ghép, đường đi của một request, và việc cần chuẩn bị trước khi cài.

Sau vụ server bị nhà mạng chặn kết nối ở phần mở đầu, tôi có trong tay hai cái server cho một app chỉ cần một. Để không một máy thì phí, nên tôi muốn cả hai chạy chung như một hệ thống. Có điều Docker, công cụ tôi vẫn dùng, không sinh ra cho việc đó.

Phần lớn app ngày nay chạy dưới dạng container. Container là một gói chứa app cùng mọi thứ app cần, nên đem sang máy nào cũng chạy giống nhau. Docker, công cụ phổ biến nhất để tạo và chạy container, lo rất tốt cho một VPS đơn lẻ. VPS là một server ảo thuê theo tháng trên cloud.

Nhưng Docker chỉ lo từng máy riêng lẻ. Một máy đột ngột tắt thì ai dời app sang máy kia? Với Docker thuần, người đó là tôi, làm bằng tay, vào bất kỳ giờ nào sự cố xảy ra.

Hình dung một quán cơm trưa mở thêm vài bếp ở mấy nơi trong thành phố. Chủ quán không thể chạy qua chạy lại mỗi lần có người nghỉ ốm; quán cần một người quản lý giữ bảng phân công và điều người tới bếp nào đang thiếu.

Trong phần mềm, việc của người quản lý đó là orchestration: quyết định app nào chạy ở đâu, tự dựng lại bản chết, và cập nhật phiên bản mới không gây gián đoạn. Kubernetes là chuẩn phổ biến nhất cho việc này.

Kubernetes gom nhiều máy thành một cụm với hai vai. Control plane là người quản lý: nó ghi nhớ trạng thái mong muốn và liên tục chỉnh cụm cho khớp. Worker là các bếp, nơi app thực sự chạy.

Tôi thử Kubernetes bản đầy đủ trước, và như đã kể ở phần mở đầu, riêng control plane đã ngốn gần hết RAM lẫn CPU của VPS. Tôi thuê server để chạy app, rốt cuộc server chủ yếu chạy bộ máy quản lý app. K3s là bản Kubernetes rút gọn cho đúng tình huống này. Trong series, control plane nằm trên một VPS tôi gọi là máy master, và có ít nhất một worker là máy tính ở nhà tôi.

Vậy K3s cắt bớt gì để nhẹ như vậy, và Docker còn ở đâu?

II. Vì sao chọn K3s, và Docker đã đi đâu #

K3s là Kubernetes gói gọn trong một file chương trình chưa tới 100MB. Nó bỏ các phần tích hợp riêng cho những nhà cung cấp cloud lớn, và đổi database mặc định từ etcd sang SQLite.

Đổi database là lý do chính khiến K3s nhẹ. Kubernetes cần một nơi lưu trạng thái của cả cụm: đang có gì, chạy ở đâu, cấu hình ra sao. Đó chính là cuốn sổ phân công của người quản lý. Bản gốc dùng etcd, database thiết kế để nhiều máy cùng giữ cuốn sổ đó đồng bộ: chắc chắn, nhưng tốn RAM và nhiều tiến trình chạy nền. SQLite chỉ là một file trên ổ đĩa, và với vài máy thì đổi như vậy tiết kiệm nhiều mà gần như không mất gì.

Nhờ vậy K3s hợp cho cụm máy tự dựng ở nhà, thiết bị đặt ngoài hiện trường, hay hệ thống nào cần gọn mà vẫn đủ phần lõi Kubernetes.

Rồi đến chi tiết làm tôi khựng lại: tôi rời Docker để lên K3s, và K3s cũng không dùng Docker, Kubernetes hiện nay cũng vậy.

  • Docker chỉ còn lo tạo image trên máy cá nhân. Image là cái khuôn; mỗi container là một bản đang chạy đúc từ khuôn đó.
  • Trên máy chạy K3s, container do containerd chạy, và đó cũng chính là phần lõi Docker vẫn dùng bên dưới. K3s gọi thẳng containerd, bỏ qua Docker.
  • Tôi không gõ lệnh chạy từng container nữa, mà khai báo trạng thái mong muốn trong file YAML. Từ đó, K3s chỉ việc bảo containerd tải image về và khởi chạy. YAML là định dạng file cấu hình dạng chữ, mỗi dòng một thiết lập kiểu “tên: giá trị”.

Đây là thay đổi tư duy lớn nhất: mô tả cái đích, không mô tả các bước. Ở quán cơm, thay vì gọi dặn từng đầu bếp “hôm nay tới bếp số 2”, bạn ghi lên bảng “luôn có 3 người nấu”. Người quản lý sẽ tự điều người hoặc tìm người thay khi có ai vắng. Việc của tôi chỉ còn là viết đúng cái bảng đó, bằng mấy mục có tên khá lạ.

III. Các mảnh ghép của K3s: cái nào tự viết, cái nào hệ thống lo #

Kubernetes đặt tên cho từng mảnh nhỏ, và nhiều cái tên nghe đáng sợ hơn việc nó làm: tờ phiếu xin chỗ lưu dữ liệu mà cũng tên là PersistentVolumeClaim. Mỗi mảnh bạn khai báo gọi là một resource. Tôi chia chúng làm hai nhóm: nhóm phải tự viết YAML hằng ngày, và nhóm hệ thống tự lo.

Nhóm 1: động tay hằng ngày

  • Pod: đơn vị nhỏ nhất mà Kubernetes chạy. Pod không đồng nghĩa với container: một Pod có thể chứa vài container dùng chung đường mạng, nên chúng gọi nhau qua localhost như hai chương trình trên cùng một máy. Localhost là địa chỉ luôn trỏ về chính máy đang chạy.
  • Deployment: bạn khai “3 bản sao của app này”, Deployment giữ cho luôn đủ 3 Pod. Nó cũng lo cập nhật cuốn chiếu: thay từng Pod một, Pod mới chạy ổn rồi mới tắt Pod cũ, nên người dùng không thấy gián đoạn.
  • Service: Pod sống ngắn, mỗi lần bị dựng lại là đổi địa chỉ IP. Service cấp một IP và một cái tên cố định, rồi chuyển request tới các Pod đang chạy. Request là một lần trình duyệt hay app khác gửi yêu cầu tới app.
  • Ingress: cổng vào cho request từ bên ngoài, làm việc ở tầng ứng dụng: đọc request hỏi tên miền nào, đường dẫn nào, rồi chuyển tới đúng Service. K3s kèm sẵn Traefik, một phần mềm định tuyến, cho việc này.
  • Gateway API: chuẩn định tuyến mới hơn. Nó sửa điểm yếu của Ingress bằng cách tách cấu hình của người quản trị hạ tầng khỏi cấu hình của từng đội làm app. Series này dùng Envoy Gateway, một phần mềm theo chuẩn này, thay cho Traefik.
  • ConfigMap và Secret: giữ cấu hình và mật khẩu nằm ngoài image, để đổi mật khẩu không phải tạo lại image. Pod đọc chúng lúc khởi động.
  • PersistentVolumeClaim: tờ phiếu nói trên. Không có nó, thứ gì Pod ghi ra cũng mất khi Pod chết.

Nhóm 2: hạ tầng, hiểu là đủ

  • ReplicaSet: nằm giữa Deployment và Pod, giữ đúng số bản sao. Deployment tự tạo nó, nên hiếm khi phải viết.
  • LoadBalancer: loại Service cho traffic từ internet vào cụm, làm việc ở tầng mạng: chỉ nhìn địa chỉ IP và port, không đọc nội dung. Port là số hiệu cửa trên một máy, mỗi dịch vụ nghe ở một port riêng.
  • ServiceLB: nhà cung cấp cloud có sẵn dịch vụ cân bằng tải, lo cấp địa chỉ cho LoadBalancer và chia traffic cho các máy. Server vật lý và VPS thường không có thứ đó, nên K3s kèm ServiceLB (tên cũ là Klipper) để chữa cháy.
  • MetalLB: bộ cân bằng tải đúng nghĩa cho những môi trường đó, cấp được IP thật trong mạng nội bộ như các cloud lớn. Để kiểm soát IP tốt hơn, tôi sẽ tắt ServiceLB mặc định và dùng MetalLB.

Quy ra quán cơm:

Mảnh ghép trong K3sTrong quán cơm
Control planeNgười quản lý
Worker nodeCác bếp
PodMột đầu bếp
DeploymentBảng ghi “luôn có 3 người nấu”
ServiceSố điện thoại cố định của quầy
Ingress hoặc Gateway APILễ tân đọc phiếu, chỉ quầy
LoadBalancerĐịa chỉ và cửa chính của quán

Phép so sánh hụt ở chỗ này: máy chết thì Pod trên đó không được điều sang bếp khác như người, mà bị bỏ, và Kubernetes dựng Pod mới y hệt ở máy khác.

Còn một chuyện: các bếp của tôi không nằm chung một chỗ.

IV. Nối máy ở cloud và máy ở nhà thành một mạng riêng #

Đây là một cụm hybrid cloud. Hybrid cloud là cách trộn máy thuê trên cloud với phần cứng của mình, ở đây là VPS và máy tính ở nhà tôi. Việc nối các máy ở xa này cho an toàn, không phải mở port bừa bãi, tôi giao cho Tailscale.

Tailscale là một dịch vụ VPN. VPN là một mạng riêng được mã hóa chạy trên internet. Nhờ nó, các máy dù ở Sài Gòn hay Singapore vẫn nói chuyện với nhau như đang chung một mạng văn phòng.

Tôi chọn một dịch vụ có sẵn cũng có lý do. Tính tới giờ, thành tích mạng của tôi trong series là một server bị chặn UDP và một VPN rớt liên tục. UDP là kiểu gửi dữ liệu nhanh mà hầu hết VPN dựa vào, nên UDP bị chặn thì VPN rớt theo.

Việc khó nhất Tailscale lo là xuyên qua NAT. NAT cho mọi thiết bị sau router nhà bạn dùng chung một địa chỉ IP công khai. Nhờ đó, máy bên trong gọi ra được, còn bên ngoài không gọi thẳng vào một máy cụ thể được.

Giống một chung cư chỉ có một địa chỉ ngoài đường. Thư trả lời về đúng căn hộ vì bảo vệ nhớ ai vừa gửi đi; thư lạ chỉ ghi địa chỉ tòa nhà thì bị bỏ. VPS gọi vào máy ở nhà tôi chính là lá thư lạ đó. Tailscale cho hai máy gửi về phía nhau gần như cùng lúc, để bảo vệ mỗi bên tưởng thư đến là thư trả lời và cho qua.

Khi NAT quá chặt, cách đó cũng không thành, và traffic phải đi vòng qua DERP server, máy chủ tiếp sức của Tailscale. Hai máy đều gọi ra DERP được, nên DERP chuyển dữ liệu giữa hai bên, như hai cư dân cùng nhờ một bưu cục chuyển thư. DERP mặc định là máy của Tailscale ở đâu đó trên thế giới; tự dựng DERP riêng thì đường vòng nằm trên hạ tầng của tôi, gần và nhanh hơn.

Kết quả: đối với K3s, máy ở nhà chỉ là thêm một máy trong cùng mạng với VPS. Đủ mảnh rồi, giờ ghép lại.

V. Theo chân một request đi qua cụm #

Hãy theo dấu một request từ trình duyệt. Trên sơ đồ, mũi tên liền là đoạn qua internet và trên máy master; mũi tên nét đứt là đoạn qua Tailscale về nhà tôi.

graph TD User([Khách truy cập]) -->|Qua Cloudflare| CF[Cloudflare] CF -->|Public IP / Port 443| LB[LoadBalancer: MetalLB trên Master] subgraph K3s Cluster LB -->|Giao gói tin| TLS[Gateway: Envoy] TLS -->|Đọc domain: vinhmdev.com| SVC[Service: Frontend] SVC -->|Quyết định chia tải| VPN((Mạng Tailscale + DERP)) VPN -.->|Đẩy qua hầm| Pod1(Pod 1) VPN -.->|Đẩy qua hầm| Pod2(Pod 2) subgraph Home Worker Node Pod1 Pod2 end end
  1. Request đi qua Cloudflare, dịch vụ đứng trước website để tăng tốc và lọc tấn công, rồi tới IP công khai của máy master ở port 443, port mặc định của web HTTPS.
  2. Địa chỉ đó do MetalLB gắn cho Envoy Gateway, nên request tới địa chỉ đó được chuyển thẳng vào Envoy. MetalLB chỉ lo cho địa chỉ “có người nhận”; việc chuyển gói tin là của chính Kubernetes.
  3. Envoy đọc header của request. Header là phần thông tin đi kèm, trong đó có tên miền khách vừa gõ. Thấy vinhmdev.com, Envoy chuyển request tới đúng Service.
  4. Service thấy các Pod của nó nằm trên worker ở nhà tôi. Request được chia thành những mẩu dữ liệu nhỏ gọi là gói tin và được mã hóa. Chúng đi qua đường hầm Tailscale từ VPS về máy ở nhà; nếu mạng chặn quá ngặt thì DERP server sẽ đỡ lưng. Đường hầm là một kết nối mã hóa giữa hai máy: dữ liệu vẫn đi qua internet nhưng người ngoài không đọc được.

Người dùng chỉ thấy trang web mở ra, không hề biết request vừa đi từ VPS về tận nhà tôi rồi quay lại. Giống khách gọi món ở quầy mặt tiền, không biết suất cơm được nấu ở một bếp bên kia thành phố.

Nếu máy master bốc cháy thì sao?

Về phía Kubernetes, mất master là mất bộ não: không tạo thêm Pod mới được nữa. Nhưng các Pod đang chạy trên worker thì không chết, như các bếp vẫn nấu món đã được phân khi người quản lý vắng mặt.

Vấn đề thật nằm ở mạng. Không có bộ cân bằng tải độc lập bên ngoài, nên tôi trỏ Cloudflare thẳng tới IP của master, và master kiêm luôn cửa chính. Master sập thì app ở worker vẫn chạy, nhưng người dùng từ internet không còn đường vào. Bếp vẫn nấu đều, chỉ có cửa duy nhất là đang khóa.

Đó là một điểm chết duy nhất: một bộ phận mà chỉ cần nó hỏng là cả hệ thống ngừng phục vụ. Muốn gỡ, về sau phải thêm bộ cân bằng tải bên ngoài, hoặc cơ chế tự chuyển IP sang máy khác khi master chết.

VI. Chuẩn bị gì trước khi cài #

Bài tới sẽ bắt tay cài đặt. Nếu bạn định làm theo, cần chuẩn bị:

  • Server Ubuntu LTS sạch và SSH key. Ubuntu LTS là bản Linux cho server được cập nhật trong nhiều năm. SSH là cách đăng nhập từ xa vào server để gõ lệnh; SSH key là khóa dùng thay mật khẩu.

  • Tắt sẵn thành phần mặc định. Khi cài K3s, tôi sẽ thêm tham số tắt Traefik và ServiceLB, nhường chỗ cho Envoy Gateway và MetalLB.

  • Tắt swap. Swap là phần ổ cứng mà hệ điều hành mượn làm RAM khi RAM đầy: rẻ, nhưng chậm hơn nhiều. Lệnh sau tắt mọi swap đang bật:

    1
    
    sudo swapoff -a
    

    Lý do: Kubernetes bản chuẩn mặc định từ chối chạy trên máy còn bật swap. K3s nới lỏng chỗ này và vẫn chạy, nhưng tôi vẫn tắt. Kubernetes tính bộ nhớ với giả định RAM là RAM thật: hết RAM thì Pod bị dừng rõ ràng rồi được dựng lại. Có swap, cả máy có thể chậm lê lết vì liên tục chuyển dữ liệu qua lại với ổ cứng, mà không có lỗi nào báo ra. Lệnh trên chỉ có hiệu lực tới lần khởi động lại; muốn tắt hẳn, bỏ dòng khai báo swap trong file /etc/fstab.

  • Làm quen với vi hoặc nano, hai trình soạn thảo văn bản chạy trong cửa sổ dòng lệnh. Sửa YAML bằng vi qua SSH là kỹ năng sống còn ở nơi không có giao diện đồ họa. Kỹ năng đầu tiên nên học là cách thoát ra: gõ :q rồi Enter. Câu hỏi “làm sao thoát khỏi Vim” trên Stack Overflow từng vượt một triệu lượt xem, nên nếu bạn từng kẹt trong đó thì không hề cô đơn.

Lưu ý xương máu: KHÔNG cài chung Docker và K3s trên máy master.

Hai thứ này xung đột ở tầng mạng: cùng tranh port, cùng ghi vào bảng định tuyến và iptables của hệ thống. Đây là hai bộ luật Linux dùng để quyết định gói tin đi đường nào và có được qua hay không. Mỗi bên thêm luật cho mạng container của mình mà không biết bên kia tồn tại, nên gói tin đi lạc theo những cách rất khó lần. Gỡ mớ bòng bong này vừa mệt vừa vô ích.

K3s đã có containerd rất nhẹ, nên Docker không thêm được gì. Hãy bắt đầu từ một server thật sạch, gỡ Docker nếu có.

Kể cả khi không bao giờ tự cài, bạn vẫn có thể mang một câu hỏi tới đội hạ tầng: “Máy nào mà chết thì người dùng không vào được?” Nếu câu trả lời chỉ là một máy, đó là một điểm chết duy nhất, giống máy master của tôi lúc này.

Hẹn gặp lại ở Bài 02: thiết lập VPN với Tailscale.