การใช้งาน Kubernetes เบื้องต้น
Kubernetes ตอนที่ ๔ การใช้งานเบื้องต้น
บทความนี้ต่อเนื่องมาจาก
แรกเริ่ม ผมหัดใช้ Kubernetes โดยอาศัยคู่มือจาก kubernetes.io ซึ่งเค้าจัดเป็นกลุ่มๆ
- Getting Started สำหรับคนที่เข้ามาครั้งแรก
- Concepts สำหรับทฤษฏี (คล้ายๆ บทความก่อนหน้า)
- Tasks สำหรับโจทย์เพื่อฝึกฝน SysOps (แนะนำให้คนที่ต้องการฝึกฝนจริงจัง ทำทุกๆ Tasks ในนี้)
- Tutorials คล้ายๆ Tasks แต่จัดเป็นชุดๆ มีไม่เยอะเท่า Tasks
- Reference คู่มืออย่างละเอียด
บทความนี้จะอิงกับ Learn Kubernetes Basics โดยจะข้ามเนื้อหาบางส่วนที่กล่าวไปแล้วในบทความก่อนหน้า เช่นการ Install Kubernetes ซึ่งผู้อ่านอาจจะใช้ Docker Desktop หรือจะใช้ minikube, MicroK8s หรืออื่นๆได้ตามสะดวก
การใช้งาน Kubernetes โดยพื้นฐานแล้ว จะมีอย่างน้อย ๖ การใช้งาน
- สร้าง Kubernetes Cluster (เราจะข้ามเรื่องนี้ ตามที่ตกลงกันไว้ก่อนหน้า)
- ติดตั้ง App ของเราจาก Container
- ตรวจสอบ Pod ที่รัน App
- เปิด App เราสู่โลกภายนอก Cluster
- ขยาย App
- ติดตั้ง App เวอร์ชั่นใหม่
ติดตั้ง App ของเราจาก Container Image
การติดตั้ง App เราจะไม่ติดตั้ง Container หรือ Pod ตรงๆ (หรือที่เรียกกันว่า Naked Pod) แต่จะใช้ Deployment (ลองดูบทความก่อนหน้า) ซึ่งการใช้ Deployment มีข้อดีหลายอย่าง เช่น
- Self-healing กรณีที่ Pod ตาย (crash) Deployment จะรัน Pod ใหม่มาแทนที่
- สามารถเพิ่มหรือลดจำนวน Pod ได้
- สามารถลงเวอร์ชั่นใหม่ (roll out) หรือถอยหลัง (roll back)
การติดตั้ง App ผ่าน Deployment จะใช้สองอย่างคือ kubectl
สำหรับติดต่อกับ Cluster เพื่อติดตั้ง และ ไฟล์ YAML (อ่านว่า ยา-แม่ว) ที่อธิบายลักษณะของ Deployment+Pod+Container
ตัวอย่างของไฟล์ยาแม่ว เพื่อติดตั้ง nginx
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 kind: Deployment metadata: name: nginx-deployment spec: selector: matchLabels: app: nginx replicas: 1 template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.14.2 ports: - containerPort: 80
แต่ละบรรทัดมีความสำคัญทั้งหมด
สี่บรรทัดแรกบอกว่า Kubernetes API ที่ใช้เป็นเวอร์ชั่นไหน และเรากำลังจะประกาศอะไร (ในที่นี้คือ Deployment
) และชื่อของสิ่งที่เราประกาศ (ในที่นี้คือ nginx-deployment
)
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 kind: Deployment metadata: name: nginx-deployment
อีกห้าบรรทัดต่อมา เป็นการประกาศ spec
เบื้องต้นของ Deployment สองอย่างคือ label ของ Pod — matchLabels
เพื่อให้ Deployment รู้ว่ากำลังดูแล Pods ไหนบ้างผ่าน label และจำนวนของ Pod — replicas
(ในที่นี้คือ 1)
spec:
selector:
matchLabels:
app: nginx
replicas: 1
ที่เหลือเป็นการประกาศ template
ของ Pod ว่ามี labels
เป็นอะไร เพื่อใช้ match กับ Deployment (ตามที่บอกไปก่อนหน้า) และ spec ของ Pod ว่ามีกี่ Containers ชื่ออะไรบ้าง ใช้ Docker Image อะไร และมีการเปิด Port หมายเลขเท่าไหร่
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
ports:
- containerPort: 80
หลังจากเราสร้างไฟล์ YAML แล้ว (สมมุติว่าชื่อ deployment-nginx.yaml) เราสามารถรัน kubectl
เพื่อติดตั้ง Deployment ได้ด้วยคำสั่ง
$ kubectl apply -f deployment-nginx.yaml deployment.apps/nginx-deployment created
และเช็คสถานะของการติดตั้งได้จาก
$ kubectl get deployment NAME READY UP-TO-DATE AVAILABLE AGE nginx-deployment 1/1 1 1 36s
$ kubectl get pod NAME READY STATUS RESTARTS AGE nginx-deployment-7fd6966748-7x6js 1/1 Running 0 119s
ถ้าจะต่อเข้าไปที่ nginx ของเรา ที่เปิดรอที่ Port 80 สามารถใช้ port-forward
ตามตัวอย่างอย่างข้างล่างที่ forward จาก localhost:5580 ไปที่ 80
$ kubectl port-forward pod/nginx-deployment-7fd6966748-7x6js 5580:80 Forwarding from 127.0.0.1:5580 -> 80 Forwarding from [::1]:5580 -> 80
ลองเปิด browser และไปที่ http://localhost:5580
ตรวจสอบ Pod ที่รัน App
จากบทความที่แล้ว Pod เป็นหน่วยการทำงานที่เล็กที่สุดของ Kubernetes ถ้าเราต้องการจะรัน Container เล็กๆซักอันนึง เราต้องเอา Container ของเราใส่เข้าไปใน Pod ก่อนถึงจะรันได้
ในแต่ละ Pod จะมีได้ตั้งแต่หนึ่ง Container หรือมากกว่า และอาจจะมี Volume (ฮาร์ดไดรฟ์) ต่อเข้าไปเพื่อเก็บข้อมูลได้ตามต้องการ
ทุก Pod จะมี Private IP Address ซึ่งจะรู้จักกันเฉพาะใน Cluster เท่านั้น
สาเหตุสำคัญที่เป็นปัญหาที่สุดในตอนนี้คืออะไร?
Pod จะถูก Schedule ไปรันใน Node ซึ่งการจะตัดสินใจว่าจะรันใน Node ไหนนั้นมีวิธีการกำหนดได้หลายแบบ (taint, affinity, resource request และอื่นๆ) หรือไม่ก็ให้ Kubernetes เลือกให้เลย
คำสั่งที่ใช้ตรวจสอบ Pod มีไม่เยอะครับ โดยทั่วไปก็ใช้ตามนี้
kubectl get pod
เพื่อดูลิสต์ของ Pod
kubectl describe pod/POD_NAME
เพื่อดูข้อมูลลักษณะและสถานะของ Pod หรือจะใช้ kubectl get pod POD_NAME -o yaml
ก็ได้ คล้ายๆกัน แต่จะออกมาเป็น YAML หรือจะ -o json
สำหรับคนที่คล่อง JSON มากกว่า YAML
kubectl log POD_NAME
เพื่อดู log ของ Pod ซึ่งออกจาก stdout และ stderr และถ้าเราอยากตามดูไปเรื่อยๆ ให้ใช้ -f
สุดท้าย ถ้าเราอยากต่อเข้าไปใน Pod ให้รัน kubectl exec -it POD_NAME bash
ซึ่งอาจจะต้องเปลี่ยน parameter สุดท้าย (bash
) ไปตาม Docker Image (บางอันจะเป็น /bin/sh
หรืออาจจะไม่มีเลยก็ได้)
เปิด App เราสู่โลกภายนอก Cluster
โดยปกติแล้ว เราจะเปิด App Pod ของเราสู่โลกภายนอกด้วยสองวิธี
วิธีแรกคือใช้ Service กับ วิธีที่สองคือใช้ Ingress ตอนนี้เราจะพูดถึงแต่ Service ก่อนนะครับ ส่วนพวกการใช้ kubectl proxy
หรือ kubectl port-forward
นี่ไม่นับ จัดเป็นการ debug ผ่าน API
จากบทความก่อนหน้า Service เป็น Load Balancer โดนทำหน้าที่หลักสองอย่างคือ 1) เป็นตัวแทนงานบริการ (Service) ซึ่งรวมถึงการประกาศชื่อของงานและการรับงานเข้ามา และ 2) กระจายงานที่รับมาส่งต่อให้แต่ละ Pod
Service มี 4 ประเภท
- ClusterIP เอาไว้ใช้คุยกันระหว่าง Pods ภายใน Cluster ต่อจากข้างนอกเข้ามาไม่ได้
- NodePort เอาไว้ต่อจากข้างนอกเข้ามาหา Pods ข้างใน โดยเปิด Port รอที่ Node หลังจากนั้นจากส่งต่อ request ที่เข้ามาที่ Port ไปให้ Pod
- LoadBalancer เอาไว้ต่อจากข้างนอกเข้ามาหา Pods ข้างใน ใช้ได้เฉพาะกรณีที่รัน Kubernetes บน Cloud เช่นถ้าเป็น Kops หรือ EKS บน AWS เราก็จะได้ Elastic Load Balancer (ELB) สำหรับแต่ละ Service
- ExternalName เอาไว้ต่อจากข้างในออกไปข้างนอก ไม่มีการส่งต่อ traffic จะส่งต่อแค่ชื่อเครื่อง (DNS CNAME) ที่ใช้ๆกันก็เช่นเวลาที่เรารัน managed database บน cloud ชื่อมันจะยาวๆและอาจจะมีเปลี่ยนได้ กรณีนี้เราสามารถสร้าง ExternalName Service มาตั้งชื่อใหม่เข้าใจง่ายไม่เปลี่ยนแปลงตามชื่อ database ไว้ให้ Pods ใน Cluster เราใช้ได้ง่ายๆ
Service จะรู้ได้อย่างไรว่าจะส่งต่อ request ไปให้ Pods ไหน? Service จะเหมือนกับ Deployment และส่วนอื่นๆของ Kubernetes ที่จะใช้ Label ในการเชื่อมโยง
เราจะสร้าง Service ยังไง? เราสามารถสร้างได้ทั้งแบบสั่ง (Imperative) หรือแบบบอกความต้องการ (Declarative — ผ่านไฟล์ YAML)
ไฟล์ YAML ด้านล่างผมเอาไฟล์ที่เราทำไว้ก่อนหน้ามาคั่นด้วย ---
เพื่อแบ่งสัดส่วนและเติมคำอธิบาย Service ที่ต้องการไว้ด้านล่าง
apiVersion: apps/v1 # for versions before 1.9.0 use apps/v1beta2 kind: Deployment metadata: name: nginx-deployment spec: selector: matchLabels: app: nginx replicas: 1 template: metadata: labels: app: nginx spec: containers: - name: nginx image: nginx:1.14.2 ports: - containerPort: 80 ---
apiVersion: v1 kind: Service metadata: name: nginx-service labels: app: nginx-service spec: type: NodePort ports: - name: http port: 80 nodePort: 30080 selector: app:nginx
ลองทำความเข้าใจคร่าวๆนะครับ มองเผินๆก็หลักการเดียวกับ Deployment คือ
- มีส่วน
apiVersion
,kind
, และmetadata
คล้ายๆกัน ต่างกันที่ค่านิดหน่อย - ส่วน
spec
จะมีลักษณะต่างกัน แน่นอนเพราะว่า Service กับ Deployment มันคนละอย่างกัน
spec
ของ Service เราจะบอกว่า
- เป็น
NodePort
— ถ้าใครรัน Docker Desktop ก็จะต่อเข้า localhost ได้เลย - มีการส่งต่อ request จาก Port 30080 ของ Node ไปให้ที่ Port 80 ของ Pod (เราตั้งชื่อว่า
http
ด้วย แต่จริงๆแล้วไม่จำเป็น - ส่งต่อ request ไปที่ Pod(s) ที่มี Label
app: nginx
ไหนลองรันซะหน่อย (แบบ Declarative)
$ kubectl apply -f service-nginx.yaml deployment.apps/nginx-deployment unchanged service/nginx-service created
ลองเข้าจาก browser ดูครับ
และถ้าเราอยากสร้าง Service โดยไม่ต้องใช้ไฟล์ YAML (สร้างแบบ Imperative) ก็สามารถทำได้
ก่อนอื่น ลบ Service ที่สร้างไว้ก่อนหน้า
$ kubectl delete service/nginx-service service "nginx-service" deleted
หลังจากนั้นก็สร้างด้วยคำสั่ง kubectl expose
$ kubectl expose deployment/nginx-deployment --name=nginx-service --port=80 --type=NodePort service/nginx-service exposed
กรณีที่สร้างแบบนี้ เราจะไม่สามารถกำหนด Port ที่เราจะเปิดรอที่ Node ได้ ระบบจะหาให้เอง (ถ้าใช้ kubectl create service
สามารถกำหนดได้ แต่เราต้องทำ Endpoint มาประกอบร่างเอง วุ่นวายไปอีกแบบ)
แล้ว Port ที่ Node ของเราเป็น Port อะไรล่ะ?
$ kubectl get service NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 71d nginx-service NodePort 10.98.193.80 <none> 80:32537/TCP 5s
ในกรณีของผมจะเป็น 32537 เอาไปลองใน browser ได้เลย
ถ้าใครจะเอาไปทำ script ต่อ สามารถเลือกให้แสดงแค่ Port ของ Node ได้ด้วย kubectl get -o jsonpath=”{.spec.ports[0].nodePort}” service nginx-service
อ่านมานานเลย เหนื่อยรึยังครับ ถ้าเหนื่อยพักผ่อนได้ครับ เสร็จแล้วค่อยไปต่อ
ขยาย App
เราสามารถขยายจำนวน Pod(s) ของเราได้ทั้งแบบสั่ง (Imperative) และแบบบอกความต้องการ (Declarative)
ผมจะพูดย้ำๆว่าสามารถทำได้สองแบบเฉพาะช่วงแรกๆนี่เท่านั้น คุยกันไปเรื่อยๆ พอผมขี้เกียจ ผมจะบอกแค่แบบเดียว ซึ่งก็อาจจะบอกสลับไปสลับมา ดีไม่ดีเขียน script มา Patch ด้วยซ้ำ
ถ้าเราจะขยาย (หรือลด) จำนวน Pods ใน Deployment เราสามารถรัน kubectl edit deployment/nginx-deployment
อ๊ะๆ อย่าพึ่งรันนะครับ แนะนำก่อนว่า คำสั่ง edit
เนี่ยจะเปิด edit ตัว default ให้ ซึ่งอาจจะเป็น vi, vim, nano, pico, notepad, etc. กรณีของผม ผมใช้ vim
$ export EDITOR=vim $ echo $EDITOR vim
ถ้าผมรัน kubectl edit deployment/nginx-deployment
มันก็จะเปิด vim มาให้แบบนี้
หาบรรทัดที่บอก replicas
ซึ่งต้องอยู่ใต้ spec
หลังจากนั้นก็แก้ได้ตามใจ เช่นผมจะลองแก้เป็น replicas: 2
หลังจากนั้นจะ :wq
(write + quit)
ไหนลองดูว่าตอนนี้ Deployment ของเรามีกี่ Pods แล้ว
$ kubectl get deployment NAME READY UP-TO-DATE AVAILABLE AGE nginx-deployment 2/2 2 2 25h
ถ้าเราไม่อยาก kubectl edit
สามารถใช้ kubectl scale
ได้ ง่าย+สะดวกกว่าด้วย เช่นถ้าเราจะเพิ่มเป็น 3 Pods
$ kubectl scale --replicas=3 deployment/nginx-deployment deployment.extensions/nginx-deployment scaled $ kubectl get deployment NAME READY UP-TO-DATE AVAILABLE AGE nginx-deployment 3/3 3 3 25h
ติดตั้ง App เวอร์ชั่นใหม่
คำสั่งที่เราใช้สำหรับการติดตั้งเวอร์ชั่นใหม่จะเป็น kubectl set image
ส่วนการถอยหลัง (ยกเลิกการติดตั้ง) และการเช็คประวัติเป็นคำสั่งเดียวกัน kubectl rollout
ลองเช็คประวัติกันหน่อย
$ kubectl rollout history deployment/nginx-deployment deployment.extensions/nginx-deployment REVISION CHANGE-CAUSE 1 <none>
ยังไม่มีประวัติอะไรเลย เพราะลงยังไม่เคยเปลี่ยนเวอร์ชั่น และตอนนี้เรารัน nginx:1.14.2
$ kubectl describe deployment/nginx-deployment | grep Image Image: nginx:1.14.2
เข้าไปเช็คใน Official Hub แล้วเก่ามาก เดี๋ยวเรามาติดตั้ง nginx เวอร์ชั่น 1.17.9 กันดีกว่า
$ kubectl set image deployment/nginx-deployment nginx=nginx:1.17.9 --record deployment.extensions/nginx-deployment image updated
$ kubectl rollout status deployment/nginx-deployment deployment "nginx-deployment" successfully rolled out
$ kubectl rollout history deployment/nginx-deployment deployment.extensions/nginx-deployment REVISION CHANGE-CAUSE 1 <none> 2 kubectl set image deployment/nginx-deployment nginx=nginx:1.17.9 --record=true
คำสั่งแรกเป็นการติดตั้ง nginx เวอร์ชั่น 1.17.9 ซึ่งในการติดตั้งเราจะต้องบอกด้วยว่าตั้งที่ Container ไหน nginx=nginx:1.17.9
— เนื่องด้วยใน Pod ของเราสามารถมีได้หลาย Containers
คำสั่งที่สองเป็นการเช็ค status ของการ roll out ถ้าเรามีหลาย Pod และ Image ใช้เวลาสตาร์ตนาน (เช่น Java + Bootstrap) เราจะเห็นว่า Kubernetes จะค่อยๆ ติดตั้งไปทีละ Pod(s) จนครบ
คำสั่งที่สามเอาไว้เช็คประวัติ
ถ้าสมมุติเราเช็คแล้วว่า nginx เวอร์ชั่น 1.17.9 ทำงานไม่ปกติ เราจะถอยหลังกลับไปเวอร์ชั่นก่อนหน้าได้ด้วย kubectl rollout undo
$ kubectl rollout undo deployment/nginx-deployment deployment.extensions/nginx-deployment rolled back
$ kubectl describe deployment/nginx-deployment | grep Image Image: nginx:1.14.2
กลับไปเวอร์ชั่นก่อนหน้าแล้ว
ก่อนจะจบ สำหรับใครที่คิดว่า การใช้ grep Image
เพื่อดูเวอร์ชั่นมันไม่สบายตา เราสามารถใช้ kubectl get
ร่วมกับ option -o jsonpath
ได้ตามตัวอย่างด้านล่างครับ
$ kubectl get deployment nginx-deployment -o jsonpath='{.spec.template.spec.containers[0].image}' nginx:1.14.2