👋👋 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:
- Cách 1: Docker Official Docs - Install Docker Engine
- Cách 2: Dùng script install-docker để cài đặt Docker trong một dòng lệnh. Script này được tạo bởi Rancher. Danh sách các phiên bản đã hỗ trợ xem tại https://github.com/rancher/install-docker/tags.
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: */*
Để 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
StandardOutput = journal
User = 319818391
Group = 319816193 [Install]
WantedBy=multi-user.target
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é!
Mọi người ủng hộ mình bằng cách giúp mình một lượt like và subscribe Dev Success 101 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