การใช้งาน Kubernetes เบื้องต้น

การใช้งาน 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 โดยพื้นฐานแล้ว จะมีอย่างน้อย ๖ การใช้งาน

  1. สร้าง Kubernetes Cluster (เราจะข้ามเรื่องนี้ ตามที่ตกลงกันไว้ก่อนหน้า)
  2. ติดตั้ง App ของเราจาก Container
  3. ตรวจสอบ Pod ที่รัน App
  4. เปิด App เราสู่โลกภายนอก Cluster
  5. ขยาย App
  6. ติดตั้ง 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 ประเภท

  1. ClusterIP เอาไว้ใช้คุยกันระหว่าง Pods ภายใน Cluster ต่อจากข้างนอกเข้ามาไม่ได้
  2. NodePort เอาไว้ต่อจากข้างนอกเข้ามาหา Pods ข้างใน โดยเปิด Port รอที่ Node หลังจากนั้นจากส่งต่อ request ที่เข้ามาที่ Port ไปให้ Pod
  3. LoadBalancer เอาไว้ต่อจากข้างนอกเข้ามาหา Pods ข้างใน ใช้ได้เฉพาะกรณีที่รัน Kubernetes บน Cloud เช่นถ้าเป็น Kops หรือ EKS บน AWS เราก็จะได้ Elastic Load Balancer (ELB) สำหรับแต่ละ Service
  4. 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 คือ

  • มีส่วน apiVersionkind, และ 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
 
Tags

What do you think?

Related articles