Bài 40.2: Container orchestration với Kubernetes

Bài 40.2: Container orchestration với Kubernetes
Chào mừng trở lại với series blog Lập trình Web của chúng ta! Đến giờ, có thể bạn đã khá quen thuộc với việc xây dựng các ứng dụng Front-end đẹp mắt và hiệu quả bằng React hay Next.js. Nhưng đã bao giờ bạn dừng lại suy nghĩ về cách những ứng dụng tuyệt vời đó được đưa đến tay người dùng một cách ổn định, đáng tin cậy và có thể "gánh" được hàng triệu lượt truy cập cùng lúc chưa?
Đây chính là lúc chúng ta bước chân vào thế giới của DevOps và Container Orchestration, cụ thể là với "ngôi sao" sáng nhất hiện nay: Kubernetes (thường gọi tắt là K8s).
Tại sao một Front-end Developer lại cần biết về Kubernetes? Đơn giản là vì việc hiểu cách ứng dụng của bạn được triển khai (deploy) và quản lý trong môi trường sản phẩm sẽ giúp bạn viết code tốt hơn, hiểu rõ hơn về hiệu năng, khả năng mở rộng, và phối hợp hiệu quả hơn với các thành viên trong đội Backend hay DevOps. Kubernetes không chỉ là một khái niệm xa vời dành cho "mấy anh quản trị hệ thống", nó là nền tảng của các ứng dụng web hiện đại!
Vấn đề: Quản lý Container bằng tay
Trước khi Kubernetes xuất hiện, hoặc trong các dự án nhỏ không dùng orchestration, việc quản lý các ứng dụng đóng gói dưới dạng containers (như với Docker) gặp phải vô số thách thức:
- Mở rộng (Scaling): Làm sao để nhanh chóng tăng số lượng bản sao của ứng dụng khi lượng truy cập tăng đột biến? Rồi lại giảm xuống khi lưu lượng truy cập giảm đi để tiết kiệm tài nguyên?
- Tự phục hồi (Self-healing): Nếu một container gặp sự cố và dừng hoạt động, ai sẽ phát hiện ra và khởi động lại nó?
- Triển khai cập nhật (Updates & Rollouts): Làm sao để cập nhật phiên bản mới của ứng dụng mà không làm gián đoạn người dùng? Làm sao để "quay lui" về phiên bản cũ nếu bản cập nhật mới gây lỗi?
- Quản lý cấu hình (Configuration Management): Thông tin cấu hình (như kết nối database, API key) cần được quản lý an toàn và dễ dàng cập nhật cho nhiều container.
- Cân bằng tải (Load Balancing): Làm sao để phân phối đều lượng truy cập từ người dùng đến các bản sao khác nhau của ứng dụng?
- Quản lý tài nguyên: Làm sao để đảm bảo các container không sử dụng quá nhiều CPU/RAM, hoặc được phân bổ đủ tài nguyên cần thiết?
Với chỉ vài container, bạn có thể quản lý bằng tay. Nhưng khi số lượng lên đến hàng chục, hàng trăm, thậm chí hàng nghìn container chạy trên nhiều máy chủ khác nhau, việc này trở nên bất khả thi. Đó chính là lúc chúng ta cần đến Container Orchestration.
Kubernetes là gì?
Kubernetes là một nền tảng nguồn mở mạnh mẽ, được phát triển ban đầu bởi Google và hiện đang được duy trì bởi Cloud Native Computing Foundation (CNCF). Nhiệm vụ chính của nó là tự động hóa việc triển khai, mở rộng, quản lý và di chuyển (scaling, managing, and migrating) các ứng dụng đóng gói trong container.
Hãy hình dung Kubernetes như một "nhạc trưởng" tài ba, điều phối toàn bộ "dàn nhạc" gồm hàng nghìn container, đảm bảo tất cả các nhạc cụ (container) chơi đúng nốt (hoạt động ổn định), khi cần thì thêm nhạc công (tăng scale), khi có nhạc công mệt (container lỗi) thì có người thay thế ngay lập tức.
Các khái niệm cốt lõi trong Kubernetes
Để hiểu cách K8s hoạt động, chúng ta cần nắm vững một vài khái niệm cơ bản. Đừng lo lắng nếu bạn thấy hơi nhiều thuật ngữ mới, chúng ta sẽ đi qua từng phần và xem cách chúng kết nối với nhau.
1. Cluster (Cụm)
Một Kubernetes Cluster là tập hợp các máy chủ (có thể là máy vật lý hoặc máy ảo) hoạt động cùng nhau. Cluster bao gồm:
- Control Plane (Master Node(s)): Bộ não của Cluster. Đây là nơi chạy các thành phần điều khiển cốt lõi của K8s, chịu trách nhiệm quản lý trạng thái của cluster, lên lịch cho các container chạy ở đâu, xử lý các sự kiện (ví dụ: một container chết), v.v.
- Worker Nodes: Các máy chủ "làm việc", nơi các container ứng dụng của bạn thực sự chạy. Mỗi Worker Node có một Kubelet (agent giao tiếp với Control Plane) và một Container Runtime (ví dụ: Docker) để chạy container.
2. Nodes (Các nút/máy chủ)
Mỗi máy chủ trong Cluster (cả Control Plane và Worker) được gọi là một Node.
3. Pods (Các Pod)
Đây là đơn vị nhỏ nhất mà bạn triển khai trong Kubernetes. Một Pod là một nhóm gồm một hoặc nhiều container cùng chia sẻ tài nguyên mạng (địa chỉ IP nội bộ, cổng) và bộ nhớ (volume).
- Tại sao không chạy container trực tiếp? Pod cung cấp một lớp trừu tượng cao hơn. Khi bạn có nhiều container cần làm việc rất chặt chẽ với nhau (ví dụ: container ứng dụng chính và một container "sidecar" ghi log hoặc proxy), bạn gom chúng vào cùng một Pod. Hầu hết các ứng dụng web đơn giản (như một ứng dụng Front-end Next.js) thường chỉ chạy trong một container duy nhất bên trong một Pod.
- Pod là tạm thời (ephemeral). K8s sẽ tạo mới hoặc xóa bỏ các Pod dựa trên trạng thái mong muốn của bạn. Địa chỉ IP của Pod sẽ thay đổi khi Pod được tạo lại.
Ví dụ YAML cho một Pod đơn giản chạy Nginx:
apiVersion: v1
kind: Pod
metadata:
name: my-nginx-pod
spec:
containers:
- name: nginx-container
image: nginx:latest
ports:
- containerPort: 80
apiVersion
: Phiên bản API của Kubernetes mà bạn đang sử dụng.kind
: Loại tài nguyên bạn đang tạo (ở đây làPod
).metadata
: Thông tin về tài nguyên, bao gồmname
(tên Pod của bạn).spec
: Đặc tả cấu hình của Pod.containers
: Danh sách các container chạy trong Pod.name
: Tên của container.image
: Tên image container (ví dụ:nginx:latest
lấy từ Docker Hub).ports
: Danh sách các port mà container lắng nghe.
Giải thích: Đoạn code này định nghĩa một Pod tên là my-nginx-pod
sẽ chạy một container duy nhất sử dụng image nginx
phiên bản latest
. Container này sẽ expose port 80.
4. Deployments (Các Triển khai)
Đây là cách phổ biến nhất để triển khai các ứng dụng không trạng thái (stateless), như ứng dụng Front-end của bạn. Một Deployment cho phép bạn khai báo trạng thái mong muốn (desired state) cho các Pod của mình.
- Bạn nói cho Deployment biết bạn muốn chạy bao nhiêu bản sao (replicas) của ứng dụng.
- Deployment sẽ tự động tạo ra các Pod cần thiết.
- Nếu một Pod chết, Deployment sẽ phát hiện và tạo một Pod mới để thay thế, đảm bảo số lượng bản sao luôn đúng như bạn khai báo (Self-healing).
- Deployment quản lý việc triển khai cập nhật theo kiểu cuốn chiếu (rolling update) để không làm gián đoạn dịch vụ. Bạn có thể dễ dàng "quay lui" (rollback) về phiên bản trước.
Một Deployment quản lý các Pod thông qua một tài nguyên khác gọi là ReplicaSet
, nhưng đối với hầu hết mục đích, bạn chỉ cần tương tác với Deployment.
Ví dụ YAML cho một Deployment chạy 3 bản sao của ứng dụng web:
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-web-app-deployment
spec:
replicas: 3 # Tôi muốn 3 bản sao của ứng dụng này
selector:
matchLabels:
app: my-web-app # Chọn các Pod có label này
template: # Định nghĩa mẫu Pod mà Deployment sẽ tạo
metadata:
labels:
app: my-web-app # Gán label này cho Pod
spec:
containers:
- name: web-app-container
image: your-dockerhub-user/your-web-app:latest # Thay bằng image ứng dụng của bạn
ports:
- containerPort: 3000 # Ví dụ ứng dụng Next.js chạy ở port 3000
kind
: Loại tài nguyên làDeployment
.spec.replicas
: Số lượng bản sao Pod bạn muốn duy trì.spec.selector.matchLabels
: Cách Deployment tìm và quản lý các Pod thuộc về nó. Nó sẽ tìm các Pod có labelapp: my-web-app
.spec.template
: Đây chính là định nghĩa của Pod mà Deployment sẽ tạo ra.metadata.labels
: Các label được gán cho Pod. Label này phải khớp vớiselector
của Deployment.spec
: Định nghĩa container bên trong Pod, tương tự như ví dụ Pod đơn giản ở trên.
Giải thích: Đoạn code này định nghĩa một Deployment tên là my-web-app-deployment
. Nó sẽ đảm bảo luôn có 3 Pod đang chạy. Mỗi Pod sẽ chứa một container sử dụng image your-dockerhub-user/your-web-app:latest
(image ứng dụng web của bạn, ví dụ: một ứng dụng Next.js đóng gói trong Docker) và expose port 3000. Deployment sử dụng label app: my-web-app
để nhận diện và quản lý các Pod này.
5. Services (Các Dịch vụ)
Như đã nói, Pod có địa chỉ IP tạm thời. Nếu một Pod chết và được Deployment tạo lại, nó sẽ có IP mới. Vậy làm sao để các Pod khác (ví dụ: một Backend API) hoặc người dùng từ bên ngoài biết được địa chỉ ổn định để kết nối đến ứng dụng Front-end của bạn?
Service giải quyết vấn đề này. Một Service cung cấp một địa chỉ IP và tên DNS ổn định để truy cập vào một nhóm các Pod (thường là các Pod được quản lý bởi một Deployment). Service sẽ tự động phân phối (cân bằng tải nội bộ) các yêu cầu đến các Pod đang hoạt động thuộc nhóm của nó.
Có nhiều loại Service:
- ClusterIP (mặc định): Cung cấp IP nội bộ trong Cluster. Chỉ có thể truy cập từ bên trong Cluster. Thường dùng cho giao tiếp giữa các Service với nhau (ví dụ: Front-end gọi Backend API).
- NodePort: Mở một port cố định trên mỗi Node trong Cluster. Truy cập bằng
<Địa chỉ IP của Node>:<NodePort>
. - LoadBalancer: (Thường dùng trong các dịch vụ Cloud) Cung cấp một Load Balancer bên ngoài (với IP công cộng) để phân phối traffic đến các Pod. Đây là loại bạn thường dùng để expose ứng dụng web ra Internet.
Ví dụ YAML cho một Service (LoadBalancer) để expose Deployment ra ngoài Internet:
apiVersion: v1
kind: Service
metadata:
name: my-web-app-service
spec:
selector:
app: my-web-app # Chọn các Pod có label này (phải khớp với Deployment)
ports:
- protocol: TCP
port: 80 # Port của Service (người dùng truy cập)
targetPort: 3000 # Port của Container trong Pod
type: LoadBalancer # Loại Service để expose ra ngoài (Cloud Provider)
kind
: Loại tài nguyên làService
.metadata.name
: Tên của Service.spec.selector
: Service sử dụng label này để tìm các Pod mà nó sẽ chuyển tiếp traffic tới. Phải khớp với label của Pod trong Deployment.spec.ports
: Định nghĩa mapping port.port
: Port mà Service lắng nghe (ví dụ: 80 cho HTTP chuẩn).targetPort
: Port mà container bên trong Pod đang lắng nghe (ví dụ: 3000 nếu ứng dụng Next.js chạy ở port 3000).
spec.type
: Loại Service.LoadBalancer
sẽ yêu cầu Cloud Provider tạo một cân bằng tải công cộng.
Giải thích: Đoạn code này định nghĩa một Service tên là my-web-app-service
. Nó sẽ tìm các Pod có label app: my-web-app
(chính là các Pod được tạo bởi Deployment của chúng ta). Nó sẽ lắng nghe ở port 80 và chuyển tiếp traffic đến port 3000 của các container trong các Pod đó. Loại LoadBalancer
sẽ giúp ứng dụng của bạn có một IP công cộng để người dùng có thể truy cập từ Internet.
6. ConfigMaps và Secrets
Để quản lý cấu hình (như URL của Backend API, biến môi trường) và dữ liệu nhạy cảm (như mật khẩu, API key) mà không nhúng trực tiếp vào image container, Kubernetes cung cấp:
- ConfigMaps: Lưu trữ dữ liệu cấu hình không nhạy cảm dưới dạng key-value.
- Secrets: Lưu trữ dữ liệu nhạy cảm, được mã hóa (encoder base64 theo mặc định, nhưng nên dùng các giải pháp quản lý Secret chuyên nghiệp hơn trong production).
Bạn có thể "mount" ConfigMap và Secret vào Pod dưới dạng file hoặc biến môi trường. Điều này giúp tách biệt cấu hình khỏi code, làm cho ứng dụng dễ dàng triển khai lại và quản lý hơn.
Ví dụ YAML cho ConfigMap:
apiVersion: v1
kind: ConfigMap
metadata:
name: my-app-config
data:
API_URL: "http://my-backend-service"
FEATURE_FLAG_A: "true"
Ví dụ cách sử dụng ConfigMap trong Deployment:
# ... (phần trên của Deployment YAML)
spec:
# ...
template:
# ...
spec:
containers:
- name: web-app-container
image: your-dockerhub-user/your-web-app:latest
ports:
- containerPort: 3000
envFrom: # Lấy biến môi trường từ ConfigMap
- configMapRef:
name: my-app-config # Tên của ConfigMap
# ... (phần dưới của Deployment YAML)
Giải thích: ConfigMap my-app-config
lưu trữ hai giá trị cấu hình. Trong định nghĩa Deployment, chúng ta sử dụng envFrom
để tự động biến các key trong ConfigMap thành các biến môi trường trong container (API_URL
và FEATURE_FLAG_A
). Ứng dụng Front-end chạy bên trong container có thể đọc các biến môi trường này.
Tại sao điều này lại quan trọng với Front-end Developer?
- Hiểu môi trường triển khai: Bạn sẽ có cái nhìn rõ ràng hơn về nơi và cách ứng dụng React/Next.js của bạn hoạt động sau khi được deploy. Điều này giúp bạn đưa ra các quyết định tốt hơn về cấu hình, tối ưu hiệu suất, và xử lý lỗi.
- Khả năng mở rộng và hiệu năng: Hiểu về Deployment và Scaling giúp bạn nắm được cách ứng dụng của bạn đáp ứng khi có nhiều người dùng. Nếu ứng dụng bị chậm khi scale up, có thể vấn đề nằm ở code Front-end hoặc cách nó tương tác với các thành phần khác, không chỉ là vấn đề của hạ tầng.
- Quản lý cấu hình: Bạn sẽ hiểu cách các biến môi trường hoặc file cấu hình được truyền vào container của bạn thông qua ConfigMaps/Secrets, giúp bạn làm việc hiệu quả hơn với các team DevOps/Backend.
- Phối hợp nhóm: Khi làm việc trong môi trường sử dụng K8s, việc hiểu các khái niệm cơ bản giúp giao tiếp và phối hợp giữa các team Front-end, Backend, và DevOps trở nên liền mạch hơn. Bạn có thể dễ dàng thảo luận về các vấn đề liên quan đến môi trường, triển khai, hoặc log lỗi.
- Kiến thức quý giá: Kubernetes là một kỹ năng rất được săn đón trong ngành công nghệ hiện đại. Việc có kiến thức về nó, dù không phải là chuyên gia, sẽ làm tăng giá trị của bạn trên thị trường lao động và mở ra nhiều cơ hội mới.
Comments