+6

Tạo Kubernetes cluster cho môi trường phát triển với kind

👋👋 Hello hello, xin chào tất cả anh em. Anh em nào đã vào đây thì comment mình chào nhau một cái cho đông vui nhé!

Trong bài viết này, mình sẽ chia sẻ cách mình dựng Local Kubernetes cluster cho môi trường phát triển với kind. Đây là công cụ cho phép tạo local Kubernetes cluster trên Docker để test và phát triển ứng dụng K8s một cách nhanh chóng. Ưu điểm của Kind đó là nhỏ gọn, dễ dùng và hỗ trợ phiên bản Kubernetes mới nhất.

Do chạy trên Docker nên bản chất mỗi node trong cluster sẽ chạy dưới dạng một Docker container. Ví dụ dưới đây là một cluster gồm 1 control-plane và 2 worker nodes được tạo bằng kind:

~> docker ps --format 'table {{.ID}}\t{{.Names}}\t{{.Status}}'
CONTAINER ID   NAMES                STATUS
fdb8308627ef   dev-worker2          Up About an hour
51699c312a8f   dev-control-plane    Up About an hour
15b518c254f0   dev-worker           Up About an hour

Tất nhiên, chạy trên Docker nên chúng ta có thể tạo nhiều cluster trên cùng một máy với tên cluster khác nhau như: dev1, dev2, etc.

Cài đặt

Cài đặt Docker

Để sử dụng kind, đầu tiên sẽ cần cài đặt Docker Engine trên máy tính trước. Nếu máy đã cài Docker thì bỏ qua bước này.

Các bạn làm theo một trong hai cách dưới đây để cài Docker:

Ví dụ, cài phiên bản Docker v25.0 thì câu lệnh cài đặt sẽ là:

curl https://releases.rancher.com/install-docker/25.0.sh | sh

Nhớ kiểm tra thêm đã cấu hình sysctl dưới đây chưa, nếu chưa thì bạn ghi thêm vào trong file /etc/sysctl.conf:

# kiểm tra:
sudo sysctl -a | grep 'net.bridge.bridge-nf-call-iptables'
# ghi thêm câu hình:
echo net.bridge.bridge-nf-call-iptables=1 | sudo tee -a /etc/sysctl.conf
# áp dụng cấu hình mới:
sudo sysctl -p

Cài đặt kind

Nếu dùng macOS hoặc Linux, bạn có thể cài kind thông qua Homebrew bằng câu lệnh sau:

brew install kind

Hoặc bạn có thể cài dưới dạng Go package, hoặc tải file binary theo hướng dẫn tại https://kind.sigs.k8s.io/docs/user/quick-start/#installing-from-release-binaries.

Tạo cluster

Để tạo cluster, bạn dùng câu lệnh bên dưới. Thêm tham số --name để đặt tên cho cluster. Mặc định tên cluster sẽ là kind:

kind create cluster --name local
Creating cluster "local" ...
 ✓ Ensuring node image (kindest/node:v1.29.2) 🖼
 ✓ Preparing nodes 📦  
 ✓ Writing configuration 📜 
 ✓ Starting control-plane 🕹️ 
 ✓ Installing CNI 🔌 
 ✓ Installing StorageClass 💾 
Set kubectl context to "kind-local"
You can now use your cluster with:

kubectl cluster-info --context kind-local

Not sure what to do next? 😅  Check out https://kind.sigs.k8s.io/docs/user/quick-start/

Mặc định sau khi tạo cluster, context sẽ được chuyển sang cluster vừa tạo. Kiểm tra các nodes bạn sẽ thấy cluster local được tạo với 1 node:

NAME                  STATUS   ROLES           AGE   VERSION
local-control-plane   Ready    control-plane   23s   v1.29.2

Để xóa cluster vừa tạo, chúng ta sẽ chạy lệnh:

kind delete cluster --name local

Tạo cluster nhiều node

Bây giờ hãy tạo một cluster có nhiều node hơn, bao gồm: 1 control plane và 2 worker nodes. Chúng ta sẽ cần tạo file cấu hình sau và lưu lại tại file ~/kind.multinodes.yaml để sử dụng khi cần sau này:

kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker

Tạo cluster mới với tên dev, dùng tham số --config để chọn file cấu hình cluster multi nodes:

kind create cluster --config ~/kind.multinodes.yaml --name dev
Creating cluster "dev" ...
 ✓ Ensuring node image (kindest/node:v1.29.2) 🖼
 ✓ Preparing nodes 📦 📦 📦
 ✓ Writing configuration 📜
 ✓ Starting control-plane 🕹️
 ✓ Installing CNI 🔌
 ✓ Installing StorageClass 💾
 ✓ Joining worker nodes 🚜
Set kubectl context to "kind-dev"
You can now use your cluster with:

kubectl cluster-info --context kind-dev

Have a nice day! 👋
~> kubectl get nodes
NAME                STATUS   ROLES           AGE   VERSION
dev-control-plane   Ready    control-plane   64s   v1.29.2
dev-worker          Ready    <none>          42s   v1.29.2
dev-worker2         Ready    <none>          40s   v1.29.2

Kiểm tra control plane node một chút, chúng ta thấy nó có 1 taint được thiết lập để chỉ chạy system components cho control-plane.

~> kubectl get node dev-control-plane -ojsonpath={..taints}
[{"effect":"NoSchedule","key":"node-role.kubernetes.io/control-plane"}]

Ngó qua tiếp StorageClass, chúng ta thấy kind có cài sẵn một storage provisioner là rancher-local-path để lưu trữ persistent data vào local storage.

~> kubectl get storageclass
NAME                 PROVISIONER             RECLAIMPOLICY   VOLUMEBINDINGMODE      ALLOWVOLUMEEXPANSION   AGE
standard (default)   rancher.io/local-path   Delete          WaitForFirstConsumer   false                  5m25s

Tạo thử ứng dụng

k create deploy whoami --image traefik/whoami:latest --replicas 2
k expose deploy whoami --port 80 --type LoadBalancer
~> kubectl get pod,svc
NAME                          READY   STATUS    RESTARTS   AGE
pod/whoami-8666965f4d-b9sjj   1/1     Running   0          35s
pod/whoami-8666965f4d-s2786   1/1     Running   0          35s

NAME                 TYPE           CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
service/kubernetes   ClusterIP      10.96.0.1     <none>        443/TCP        13m
service/whoami       LoadBalancer   10.96.33.98   <pending>     80:30546/TCP   31s

Bên trên mình chạy một ứng dụng Whoami và expose nó thông qua một LoadBalancer Service. Truy cập thử thông qua IP của container dev-control-plane với node port được mở ở trên bạn sẽ thấy ứng dụng đã chạy.

~> curl 172.20.0.5:30546
Hostname: whoami-8666965f4d-s2786
IP: 127.0.0.1
IP: ::1
IP: 10.244.2.2
IP: fe80::6025:faff:fe11:d1bf
RemoteAddr: 172.20.0.5:21271
GET / HTTP/1.1
Host: 172.20.0.5:30546
User-Agent: curl/7.68.0
Accept: */*

Cloud LoadBalancer Provider

Trong thực tế, việc dùng node port khá bất tiện. Mình dùng LoadBalancer service nên rất mong muốn có thể mô phỏng quá trình cấp External-IP như trên Cloud và truy cập thông qua External-IP đó.

Đối với kind, mình sẽ sử dụng thêm công cụ cloud-provider-kind để dựng Cloud LoadBalancer Provider. Cài đặt nó dưới dạng pre-built binaries hoặc dùng Go packages với câu lệnh dưới đây:

go install sigs.k8s.io/cloud-provider-kind@latest

Đây là một công cụ mới có nên tính năng chưa có nhiều. Chúng ta chỉ cần chạy binary và giữ cho process đó luôn chạy, nó sẽ thực hiện cấp External-IP cho các LoadBalancer Service.

cloud-provider-kind
I0502 01:36:19.775598 3355312 controller.go:167] probe HTTP address https://127.0.0.1:38137
I0502 01:36:19.780542 3355312 controller.go:88] Creating new cloud provider for cluster dev
I0502 01:36:19.789712 3355312 controller.go:95] Starting cloud controller for cluster dev
I0502 01:36:19.789758 3355312 node_controller.go:165] Sending events to api server.
I0502 01:36:19.790535 3355312 controller.go:231] Starting service controller
I0502 01:36:19.790575 3355312 shared_informer.go:311] Waiting for caches to sync for service
I0502 01:36:19.790599 3355312 node_controller.go:174] Waiting for informer caches to sync
I0502 01:36:19.793707 3355312 reflector.go:351] Caches populated for *v1.Service from pkg/mod/k8s.io/client-go@v0.29.3/tools/cache/reflector.go:229
I0502 01:36:19.793940 3355312 reflector.go:351] Caches populated for *v1.Node from pkg/mod/k8s.io/client-go@v0.29.3/tools/cache/reflector.go:229
I0502 01:36:19.891657 3355312 shared_informer.go:318] Caches are synced for service
I0502 01:36:19.891704 3355312 controller.go:733] Syncing backends for all LB services.
I0502 01:36:19.891712 3355312 controller.go:737] Successfully updated 0 out of 0 load balancers to direct traffic to the updated set of nodes
I0502 01:36:19.891722 3355312 controller.go:733] Syncing backends for all LB services.
I0502 01:36:19.891728 3355312 controller.go:737] Successfully updated 0 out of 0 load balancers to direct traffic to the updated set of nodes
I0502 01:36:19.891733 3355312 controller.go:733] Syncing backends for all LB services.
I0502 01:36:19.891738 3355312 controller.go:737] Successfully updated 0 out of 0 load balancers to direct traffic to the updated set of nodes
I0502 01:36:19.891761 3355312 instances.go:47] Check instance metadata for dev-control-plane
I0502 01:36:19.891814 3355312 instances.go:47] Check instance metadata for dev-worker
I0502 01:36:19.891767 3355312 controller.go:398] Ensuring load balancer for service default/whoami
I0502 01:36:19.894048 3355312 instances.go:47] Check instance metadata for dev-worker2
I0502 01:36:19.894143 3355312 controller.go:954] Adding finalizer to service default/whoami
I0502 01:36:19.894172 3355312 event.go:376] "Event occurred" object="default/whoami" fieldPath="" kind="Service" apiVersion="v1" type="Normal" reason="EnsuringLoadBalancer" message="Ensuring load balancer"
I0502 01:36:19.905593 3355312 loadbalancer.go:28] Ensure LoadBalancer cluster: dev service: whoami
I0502 01:36:19.944316 3355312 instances.go:75] instance metadata for dev-control-plane: &cloudprovider.InstanceMetadata{ProviderID:"kind://dev/kind/dev-control-plane", InstanceType:"kind-node", NodeAddresses:[]v1.NodeAddress{v1.NodeAddress{Type:"Hostname", Address:"dev-control-plane"}, v1.NodeAddress{Type:"InternalIP", Address:"172.20.0.5"}, v1.NodeAddress{Type:"InternalIP", Address:"fc00:f853:ccd:e793::5"}}, Zone:"", Region:""}
I0502 01:36:19.948068 3355312 instances.go:75] instance metadata for dev-worker: &cloudprovider.InstanceMetadata{ProviderID:"kind://dev/kind/dev-worker", InstanceType:"kind-node", NodeAddresses:[]v1.NodeAddress{v1.NodeAddress{Type:"Hostname", Address:"dev-worker"}, v1.NodeAddress{Type:"InternalIP", Address:"172.20.0.4"}, v1.NodeAddress{Type:"InternalIP", Address:"fc00:f853:ccd:e793::4"}}, Zone:"", Region:""}
I0502 01:36:19.958828 3355312 instances.go:75] instance metadata for dev-worker2: &cloudprovider.InstanceMetadata{ProviderID:"kind://dev/kind/dev-worker2", InstanceType:"kind-node", NodeAddresses:[]v1.NodeAddress{v1.NodeAddress{Type:"Hostname", Address:"dev-worker2"}, v1.NodeAddress{Type:"InternalIP", Address:"172.20.0.3"}, v1.NodeAddress{Type:"InternalIP", Address:"fc00:f853:ccd:e793::3"}}, Zone:"", Region:""}
I0502 01:36:19.968019 3355312 node_controller.go:267] Update 3 nodes status took 76.311386ms.
I0502 01:36:19.998143 3355312 server.go:92] creating container for loadbalancer
I0502 01:36:20.002280 3355312 controller.go:167] probe HTTP address https://127.0.0.1:38287
I0502 01:36:20.007753 3355312 controller.go:88] Creating new cloud provider for cluster local
I0502 01:36:20.016195 3355312 controller.go:95] Starting cloud controller for cluster local
I0502 01:36:20.016239 3355312 controller.go:231] Starting service controller
I0502 01:36:20.016688 3355312 shared_informer.go:311] Waiting for caches to sync for service
I0502 01:36:20.016279 3355312 node_controller.go:165] Sending events to api server.
I0502 01:36:20.016833 3355312 node_controller.go:174] Waiting for informer caches to sync
I0502 01:36:20.018611 3355312 reflector.go:351] Caches populated for *v1.Service from pkg/mod/k8s.io/client-go@v0.29.3/tools/cache/reflector.go:229
I0502 01:36:20.018660 3355312 reflector.go:351] Caches populated for *v1.Node from pkg/mod/k8s.io/client-go@v0.29.3/tools/cache/reflector.go:229
I0502 01:36:20.116903 3355312 shared_informer.go:318] Caches are synced for service
I0502 01:36:20.116952 3355312 controller.go:733] Syncing backends for all LB services.
I0502 01:36:20.116965 3355312 controller.go:737] Successfully updated 0 out of 0 load balancers to direct traffic to the updated set of nodes
I0502 01:36:20.117005 3355312 instances.go:47] Check instance metadata for local-control-plane
I0502 01:36:20.189322 3355312 instances.go:75] instance metadata for local-control-plane: &cloudprovider.InstanceMetadata{ProviderID:"kind://local/kind/local-control-plane", InstanceType:"kind-node", NodeAddresses:[]v1.NodeAddress{v1.NodeAddress{Type:"Hostname", Address:"local-control-plane"}, v1.NodeAddress{Type:"InternalIP", Address:"172.20.0.2"}, v1.NodeAddress{Type:"InternalIP", Address:"fc00:f853:ccd:e793::2"}}, Zone:"", Region:""}
I0502 01:36:20.197984 3355312 node_controller.go:267] Update 1 nodes status took 81.017025ms.
I0502 01:36:20.302184 3355312 server.go:100] updating loadbalancer
I0502 01:36:20.302213 3355312 proxy.go:126] address type Hostname, only InternalIP supported
I0502 01:36:20.302243 3355312 proxy.go:126] address type Hostname, only InternalIP supported
I0502 01:36:20.302248 3355312 proxy.go:140] haproxy config info: &{HealthCheckPort:10256 ServicePorts:map[IPv4_80:{BindAddress:*:80 Backends:map[dev-worker:172.20.0.4:30546 dev-worker2:172.20.0.3:30546]}]}
I0502 01:36:20.302341 3355312 proxy.go:155] updating loadbalancer with config
global
  log /dev/log local0
  log /dev/log local1 notice
  daemon

resolvers docker
  nameserver dns 127.0.0.11:53

defaults
  log global
  mode tcp
  option dontlognull
  # TODO: tune these
  timeout connect 5000
  timeout client 50000
  timeout server 50000
  # allow to boot despite dns don't resolve backends
  default-server init-addr none


frontend IPv4_80-frontend
  bind *:80
  default_backend IPv4_80-backend
  # reject connections if all backends are down
  tcp-request connection reject if { nbsrv(IPv4_80-backend) lt 1 }

backend IPv4_80-backend
  option httpchk GET /healthz
  server dev-worker 172.20.0.4:30546 check port 10256 inter 5s fall 3 rise 1
  server dev-worker2 172.20.0.3:30546 check port 10256 inter 5s fall 3 rise 1

I0502 01:36:20.376431 3355312 proxy.go:163] restarting loadbalancer
I0502 01:36:20.400019 3355312 server.go:116] get loadbalancer status
I0502 01:36:20.416813 3355312 controller.go:995] Patching status for service default/whoami
I0502 01:36:20.416938 3355312 event.go:376] "Event occurred" object="default/whoami" fieldPath="" kind="Service" apiVersion="v1" type="Normal" reason="EnsuredLoadBalancer" message="Ensured load balancer"
~> kubectl get svc whoami
NAME     TYPE           CLUSTER-IP    EXTERNAL-IP   PORT(S)        AGE
whoami   LoadBalancer   10.96.33.98   172.20.0.6    80:30546/TCP   18m

Tạo thêm custom domain whoami.lc để tiện truy cập bằng browser qua URL http://whoami.lc:

echo '172.20.0.6 whoami.lc' | sudo tee -a /etc/hosts
~> curl http://whoami.lc
Hostname: whoami-8666965f4d-s2786
IP: 127.0.0.1
IP: ::1
IP: 10.244.2.2
IP: fe80::6025:faff:fe11:d1bf
RemoteAddr: 172.20.0.4:20295
GET / HTTP/1.1
Host: whoami.lc
User-Agent: curl/7.68.0
Accept: */*

Với phương pháp sử dụng cloud-provider-kind, chúng ta sẽ hạn chế vấn đề xung đột khi mở port 80, 443 nếu các bạn đang phải tham gia nhiều dự án chạy Docker và Kubernetes trên cùng một máy. Ví dụ:

  • http://project1.dev.lc: HTTP Server mở port 80 cho dự án project-1, sử dụng trực tiếp Docker container.
  • http://project2.dev.lc: HTTP Server mở port 80 cho dự án project-2, sử dụng Kubernetes với Kind.
  • Như vậy, dù khác platform nhưng cả hai đều chung domain dev.lc và có thể share Cookies với nhau.

Để tiện trong quá trình làm việc, mình chuyển cloud-provider-kind chạy dưới dạng systemd service trên Linux.

[Unit]
Description = LoadBalancer for KIND clusters
After = docker.service

[Service]
Type = simple
ExecStart = /home/nguyen.huu.kim/go/bin/cloud-provider-kind    # change your path
StandardOutput = journal
User = 319818391    # find your uid, run: id -u
Group = 319816193   # find your gid, run: id -g 

[Install]
WantedBy=multi-user.target

Lưu ý, các bạn hãy sửa lại:

  • ExecStart: Đổi lại path tới binary cho đúng
  • User: Đổi lại UID của bạn. Để biết UID, chạy lệnh: id -u
  • Group: Đổi lại GID của bạn. Để biết GID, chạy lệnh: id -g

Bật service cloud-provider-kind lên bằng lệnh:

sudo systemctl enable --now cloud-provider-kind
sudo systemctl status cloud-provider-kind
● cloud-provider-kind.service - LoadBalancer for KIND clusters
     Loaded: loaded (/etc/systemd/system/cloud-provider-kind.service; enabled; vendor preset: enabled)
     Active: active (running) since Thu 2024-05-02 01:47:25 +07; 25s ago
   Main PID: 3366284 (cloud-provider-)
      Tasks: 13 (limit: 18951)
     Memory: 10.2M
     CGroup: /system.slice/cloud-provider-kind.service
             └─3366284 /home/nguyen.huu.kim/go/bin/cloud-provider-kind

Thg 5 02 01:47:26 b120823-pc cloud-provider-kind[3366284]:   # reject connections if all backends are down
Thg 5 02 01:47:26 b120823-pc cloud-provider-kind[3366284]:   tcp-request connection reject if { nbsrv(IPv4_80-backend) lt 1 }
Thg 5 02 01:47:26 b120823-pc cloud-provider-kind[3366284]: backend IPv4_80-backend
Thg 5 02 01:47:26 b120823-pc cloud-provider-kind[3366284]:   option httpchk GET /healthz
Thg 5 02 01:47:26 b120823-pc cloud-provider-kind[3366284]:   server dev-worker 172.20.0.4:30546 check port 10256 inter 5s fall 3 rise 1
Thg 5 02 01:47:26 b120823-pc cloud-provider-kind[3366284]:   server dev-worker2 172.20.0.3:30546 check port 10256 inter 5s fall 3 rise 1
Thg 5 02 01:47:26 b120823-pc cloud-provider-kind[3366284]: I0502 01:47:26.322713 3366284 proxy.go:163] restarting loadbalancer
Thg 5 02 01:47:26 b120823-pc cloud-provider-kind[3366284]: I0502 01:47:26.347448 3366284 server.go:116] get loadbalancer status
Thg 5 02 01:47:26 b120823-pc cloud-provider-kind[3366284]: I0502 01:47:26.363572 3366284 controller.go:995] Patching status for service default/whoami
Thg 5 02 01:47:26 b120823-pc cloud-provider-kind[3366284]: I0502 01:47:26.363608 3366284 event.go:376] "Event occurred" object="default/whoami" fieldPath="" k>

Service trên đã được kích hoạt và tự động chạy sau khi máy khởi động. Từ giờ tắt đi bật lại khỏi phải quan tâm về LoadBalancer nữa. 👍️

Cài Ingress Nignx

Trường hợp trong môi trường phát triển cũng cần dùng Ingress thì mình setup thêm Ingress Controller với Nginx như này chẳng hạn:

helm upgrade --install ingress-nginx ingress-nginx \
  --repo https://kubernetes.github.io/ingress-nginx \
  --namespace ingress-nginx --create-namespace

Chỉ cần trỏ lại custom domain về External-IP của Nginx Ingress là xong.

~> kubectl get svc
NAME                                 TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)                      AGE
ingress-nginx-controller             LoadBalancer   10.96.1.26     172.20.0.6    80:30156/TCP,443:32387/TCP   63s
ingress-nginx-controller-admission   ClusterIP      10.96.144.17   <none>        443/TCP                      63s

Cài đặt metrics-server

Trong một số trường hợp, cần cài đặt metrics-server. Kind không có built-in command để bật như Minikube nên mình cài tiếp qua Helm như tương tự cài Nginx Ingress Controller.

helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server
helm repo update
helm upgrade --install metrics-server metrics-server/metrics-server \
    --set args[0]=--kubelet-insecure-tls \
    --namespace metrics-server --create-namespace
~> kubectl get pod -n metrics-server
NAME                              READY   STATUS    RESTARTS   AGE
metrics-server-8549dcfdd6-fzwhh   1/1     Running   0          28s
~> kubectl top pod
NAME                                       CPU(cores)   MEMORY(bytes)   
ingress-nginx-controller-c8f499cfc-qd5nv   2m           58Mi

Tổng kết

Trên đây là cách mình sử dụng kind để tạo Kubernetes cluster cho môi trường phát triển. Follow mình để biết thêm các kiến thức thú vị khác về lĩnh vực web nhé!

Nếu các bạn đang muốn tìm hiểu về Kubernetes một cách bài bản mà chưa biết bắt đầu từ đâu, hay bạn muốn chuẩn bị để ôn thi chứng chỉ CKAD, CKA, hãy liên hệ mình qua Facebook Fanpage phía dưới để được tự vấn nhé.


Mọi người ủng hộ mình bằng cách giúp mình một lượt like và subscribe DevSuccess101 trên nền tảng mà bạn yêu thích phía dưới nhé. Cảm ơn các bạn đã đón đọc.

✴️ Website: https://devsuccess101.com
✴️ Subscribe kênh! https://l.devsuccess101.com/subscribe
✴️ Join our Discord community for help https://l.devsuccess101.com/discord
✴️ Donate: Momo, Paypal

✴️ TikTok: https://l.devsuccess101.com/tiktok
✴️ YouTube: https://l.devsuccess101.com/youtube
✴️ Viblo: https://l.devsuccess101.com/viblo
✴️ Facebook: https://l.devsuccess101.com/facebook
✴️ Discord: https://l.devsuccess101.com/discord


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí