Basic Kubernetes Part 3 - Service คืออะไรมีไว้ใช้ทำอะไร

Basic Kubernetes Part 3 - Service คืออะไรมีไว้ใช้ทำอะไร

เราจะยิง Request ไปหา Pod ยังไง

ในตอนที่แล้วเราได้รู้แล้วว่า pod เปรียบเสมือนเครื่องที่ลง docker ที่สามารถ Run container ได้ ซึ่งก็แปลว่าเรามีเครื่องที่ Run Application ที่เราต้องการแล้ว แต่ปัญหาคือเราไม่สามารถยิง Request ไปหา pod แต่ละเครื่องได้แบบง่ายๆ เพราะ pod ถูกกำหนด IP ให้แบบอัตโนมัติ อีกทั้งเมื่อเรา Scale out pod ให้มีหลายตัวก็แปลว่า pod ที่ Run application ของเรามีหลายเครื่องที่ Run เหมือนกัน เราจะรู้ได้อย่างไรว่าเครื่องไหนเป็นที่เราอยากจะยิง Request ไปหาดังปัญหาในภาพ

จากภาพจะเห็นว่ามี Pod 6 ตัวคือ

  • client : 1 ตัว
  • nginx : 3 ตัว
  • whoami : 2 ตัว

ถ้า client ต้องการติดต่อไปหา nginx ตัว client จะรู้ได้ยังไงว่าจะยิงไป IP ไหน ( อย่าลืมว่า client ไม่รู้อะไรเลยนะครับ ) ถ้าเราคิดง่ายๆก็ให้ client describe pod ทุกตัวใน k8s เพื่อดูว่า pod ไหนเป็น pod nginx จากนั้นก็เอาไปเก็บไว้ในตารางของ client หลังจากนี้เวลาจะยิงก็ไปดูจากตาราง

ดูแล้วก็เป็นวิธีที่เป็นไปได้แต่ปัญหาคือ

  1. ถ้า k8s ทำการเพิ่มลดจำนวน pod เช่นสั่งเพิ่ม pod เป็น 10 ตัว หรือ ลดเหลือ 1 ตัว ตาราง ip ที่ client มีก็ไม่ใช่ตาราง ip ที่ถูกต้อง ทางแก้ก็คืออาจจะยิงไปถามบ่อยๆเอาก็ได้ แต่ต้องบ่อยแค่ไหนล่ะ บ่อยไปก็สร้างปัญหาให้ k8s ไม่บ่อยข้อมูล ip ก็ไม่ถูกต้อง

  2. การที่ทำแบบนี้แปลว่า k8s ต้องเปิดให้ client สามารถยิงมาถามว่า pod ใน k8s มีอะไรบ้าง และ เป็น pod อะไร การที่เปิดให้ทำอะไรแบบนี้ก็ดูเสี่ยงเพราะกลายเป็นว่า client สามารถรู้ได้ว่ามี pod อะไรบ้างใน k8s ถ้าสัก pod ใน k8s โดนโจมตี เขาก็สามารถโจมตีทุก pod ใน k8s ได้เลย

ในอีกแนวทางนึงถ้าเรามีอะไรสักอย่างโผล่ขึ้นมาแล้วเรายิงไปที่นั่นแล้วอะไรสักอย่างนั้นสามารถยิงไปหา pod ที่ถูกต้องได้ (จริงๆก็คือ Reverse proxy นั่นเอง) ถ้าเรามีอะไรสักอย่างนั้นเราก็แค่ยิงไปที่นั่นแล้วที่นั่นก็จะส่ง Request ไปหา nginx ต่อให้ ซึ่งมันจะทำงานดังภาพ

สิ่งนั้น ที่นั่น ไอนั่น ที่ผมพูดถึงก็คือ Service ของ k8s นั่นเอง

Service คืออะไร

คุณสามารถดู Configuration file ทั้งหมดได้ที่ :Link

ถ้าพูดกันแบบง่ายๆ Service คือตัวที่ทำให้เราสามารถ Request เข้าไปหา Pod ได้ โดยไม่ไปคอย Describe ดู IP ของ Pod ตัวนั้นว่าอยู่ที่ไหน แค่ยิงไปหา Service เดี๋ยว Service จะพา Request ไปหา Pod ให้เอง (นิยามที่ผมพูดไม่ถูกต้อง 100% ถ้าถูกต้องจริงๆดูได้ที่ https://kubernetes.io/docs/concepts/services-networking/service/)

สร้าง Service กัน

ก่อนที่จะทดลองสร้าง Service ผมอยากขอให้ทุกท่านลบ Deployment และ Pod ที่มีอยู่ทิ้งก่อน เพื่อให้การทดลองไม่มีผลกระทบจาก Pod ตัวก่อนๆที่เคยสร้าง โดยใช้คำสั่งด้านล่างเพื่อทำการลบ

1
2
kubectl delete deployment --all
kubectl delete pod --all

จากนั้นสร้างไฟล์ test-server-pod.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
apiVersion: v1
kind: Pod
metadata:
name: nginx-pod
labels:
app: nginx
owner: wasinee
spec:
containers:
- name: nginx-container
image: nginx:1.25.1
ports:
- containerPort: 80
---
apiVersion: v1
kind: Pod
metadata:
name: whoami-pod
labels:
app: whoami
owner: wasinee
spec:
containers:
- name: whoami-container
image: traefik/whoami
ports:
- containerPort: 80

โดยไฟล์นี้จะทำการสร้าง Pod ขึ้นมา 2 ตัวคือ nginx กับ whoami จากนั้นสั่งคำสั่งด้านล่างเพื่อสร้าง pod ขึ้นมา

1
kubectl apply -f test-server-pod.yml

จากนั้นสร้างไฟล์ client-pod.yml

1
2
3
4
5
6
7
8
9
10
11
apiVersion: v1
kind: Pod
metadata:
name: client-pod
labels:
app: busybox
spec:
containers:
- name: busybox
image: yauritux/busybox-curl
command: ["tail", "-f", "/dev/null"]

จากนั้นสั่งสร้าง pod ด้วยคำสั่ง

1
kubectl apply -f client-pod.yml

ซึ่งเมื่อสั่งเสร็จเราจะมีข้อ Pod ทั้งหมด 3 ตัวดังภาพ

จากนั้นทำการสั่ง Describe nginx-pod กับ whoami-pod เพื่อเอา IP ของ Pod ทั้ง 2 ซึ่งของผมคือ

  • nginx-pod : 10.42.0.103
  • whoami-pod : 10.42.0.104

จากนั้นทำการสั่ง

1
kubectl exec -ti client-pod sh

อธิบายคำสั่ง

  • kubectl exec เป็นการสั่งคำสั่งที่อยู่ใน pod
  • -ti คือการเชื่อมต่อ terminal ของ pod เข้ากับ terminal ของเรา
  • kubectl exec -ti client-pod sh รวมแล้วคือสั่งคำสั่ง sh ในเครื่อง pod และเชื่อมต่อ terminal ของเราเข้ากับ pod

ดังนั้นจะกลายเป็นว่าเราได้คุมคำสั่ง sh ในเครื่อง pod ซึ่งแปลว่าเราสามารถสั่ง command ต่างๆในเครื่อง pod ได้ทั้งหมด ซึ่งการที่เราทำอย่างนี้ก็เพื่อจำลองว่าเราเป็นเครื่อง client ที่จะยิงไปหา pod เครื่องอื่นๆ

ลองยิงไปหา pod เครื่องอื่นๆด้วยคำสั่งด้านล่าง

1
2
curl 10.42.0.103
curl 10.42.0.104

จากภาพจะเห็นว่าเราสามารถยิงไปหา nginx และ whoami ได้ จากนั้นลองสั่ง exit เพื่อออกจาก pod client-pod

สร้าง Service มาทดลองใช้งาน

ทำการสร้างไฟล์ service-1-basic.yml จากนั้นทำการสั่ง kubectl apply เพื่อสร้าง service

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
name: nginx-service
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP

จากนั้นสั่ง get service และทำการ describe service nginx-service เพื่อดูรายละเอียด

1
2
kubectl get service
kubectl describe service nginx-service

คราวนี้เราลองเข้าไปในเครื่องของ client-pod ด้วยคำสั่ง kubectl exec และสั่ง curl ไปที่ IP ของ Service ซึ่งของผมคือ 10.43.19.168 (ของคุณอาจไม่เหมือนกับของผม)

1
curl 10.43.19.168

จะเห็นว่าเราได้ Response เหมือนกับที่ยิงไปที่ 10.42.0.103 (nginx-pod) เลย ซึ่งนั่นก็แปลว่า เราสามารถยิงไปหา pod ผ่านการยิงไปที่ service ได้

คราวนี้เราลองมาดูรายละเอียดของ Service อีกรอบ

สังเกตตรงค่า Endpoint ของ Service Endpoint นี้คือ Endpoint ที่ Service นี้ชี้ไปหา ซึ่งค่าของผมมันคือ 10.42.0.103 ซึ่งมันคือ IP ของ nginx-pod นั่นก็แปลว่า Service นี้ชี้ไปหา nginx-pod ดังนั้น การยิงไปหา Service นี้ก็คือการยิงไปหา nginx-pod นั้นเอง

คราวนี้คุณอาจสงสัยว่า Service รู้ได้อย่างไรว่า Pod ไหนเป็น Pod nginx ขอให้ดูตรง Selector ครับ จะเห็นว่าค่าคือ

1
app=nginx

คุ้นๆค่านี้ไหมครับ ลองดูที่ label ของ nginx-pod ดูครับว่ามันคือค่าอะไร จะเห็นว่ามันคือค่าเดียวกันครับ ดังนั้น Service ใช้ตัว selector ไปหา label ของ pod ที่ตรงกับค่า selector มาเป็น Endpoint ของ Service นี้ครับ

อธิบายไฟล์ service-1-basic.yml

ส่วนของ apiVersion , kind , metadata ผมไม่ขอพูดนะครับ เนื่องจากเหมือนกับของ pod และ deployment จะขออธิบายในส่วนของ spec เป็นต้นไป

selector

ค่านี้เป็นค่าที่กำหนดว่าจะเอา pod ตัวไหนมาเป็น endpoint ที่ service ชี้ไปบ้าง โดยเอาไปหา pod ที่มีค่า label ตรงกับ selector

จากตัวอย่างไฟล์ service-1-basic.yml มีค่า selector คือ app=nginx ดังนั้น pod ทุกตัวที่มีค่า label app=nginx ซึ่งมี pod ที่มีค่า label app=nginx คือ nginx-pod

ports

ส่วนนี้เป็นการบอกว่า Service นี้เปิดที่ Port อะไรบ้าง สังเกตุว่า port มี s ดังนั้นสามารถกำหนดได้หลาย port

  • protocol ส่วนนี้เป็นการบอกว่า protocol ของ port นี้คืออะไร
  • port: port ของ service ที่เปิดคืออะไร
  • targetPort: port ของ endpoint เป้าหมายคือ port อะไร

type

อันนี้เป็นชนิดของ Service ส่วนนี้ขออธิบายทีหลัง

ทดลอง Selector เพื่อความเข้าใจ

สร้างไฟล์ service-2-test-selector.yml ด้วยข้อมูลข้างล่างจากนั้นสั่ง apply และ describe ตัว service

1
2
3
4
5
6
7
8
9
10
11
12
apiVersion: v1
kind: Service
metadata:
name: wasinee-service
spec:
selector:
owner: wasinee
ports:
- protocol: TCP
port: 80
targetPort: 80
type: ClusterIP

จากภาพเราจะเห็นว่า selector ของเราคือ owner=wasinee ซึ่ง pod ที่มี label ดังกล่าวมี 2 pod คือ nginx-pod กับ whoami-pod ซึ่งนั่นทำให้ตัว Endpoint ของ Service มี 2 IP คือ 10.42.0.103 (nginx-pod) , 10.42.0.104(whoami-pod)

ซึ่งถ้าเรา kubectl exec เข้า client-pod แล้วลองยิงไปที่ wasinee-service ( 10.43.123.125 IP ของคุณไม่เหมือนของผม ) บางครั้งเราจะได้ response ของ nginx-pod บางครั้งเราจะได้ response ของ whoami-pod ดังภาพ

ดังนั้น seletor ของ Service และ label ของ pod เป็นสิ่งสำคัญมาก ดังนั้นตอนสร้าง Pod ก็ควรเตรียม label ไว้ให้ Service ชี้มา และ selector ก็ควรกำหนดให้ตรงกับ pod ที่ต้องการจริงๆ

ทดลอง ports เพื่อความเข้าใจ

สร้างไฟล์ service-3-test-port.yml จากนั้น apply และ describe ตัว test-port-service

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
apiVersion: v1
kind: Service
metadata:
name: test-port-service
spec:
selector:
app: nginx
ports:
- name: port-a
protocol: TCP
port: 80
targetPort: 80
- name: port-b
protocol: TCP
port: 81
targetPort: 80
- name: port-c
protocol: TCP
port: 82
targetPort: 81

type: ClusterIP

ในการ test นี้เราสร้าง service ที่เปิด port 3 port คือ 80, 81, 82 จากนั้นเรามาลองยิงไปที่ service ตาม port เหล่านี้แล้วมาดูผลลัพธ์กันว่าเป็นอย่างไร ทำไมถึงเป็นอย่างนั้น

ยิงไป port 80

การยิงไปที่ port 80 ของ service จากนั้นตัว service ส่งต่อ request ไปที่ pod ที่ port 80 (targetPort) ซึ่งการยิงแบบนี้ทำให้ได้ response จาก nginx-pod กลับมา

ยิงไป port 81

การยิงไปที่ port 81 ของ service จากนั้นตัว service ส่งต่อ request ไปที่ pod ที่ port 80 (targetPort) ซึ่งการยิงแบบนี้ทำให้ได้ response จาก nginx-pod กลับมา

ยิงไป port 82

การยิงไปที่ port 82 ของ service จากนั้นตัว service ส่งต่อ request ไปที่ pod ที่ port 81 (targetPort) ซึ่งเมื่อยิงไปที่เครื่อง nginx-pod แล้วตัว nginx-pod ไม่ได้เปิด port 81 ดังนั้น ยิงไปจึงได้รับ Error เหมือนดังภาพ

จากการทดลองในส่วนนี้น่าจะทำให้เราเข้าใจเรื่อง port ของ service นะครับว่า port คืออะไร targetPort คืออะไร ตั้งค่ายังไงให้เหมาะสมกับงานที่เจอ

เรียก service ด้วยชื่อ

หลังจากทดลองกันมานานคุณอาจจะสงสัยว่า แล้วมันต่างจากการหา ip ผ่าน pod ยังไงวะ เพราะสุดท้ายก็ต้องไป describe service เอา ip มาอยู่ดี ที่ผมทำไปทั้งหมดเนี่ยเพื่อให้ง่ายในการทดลอง ในความเป็นจริงนั้นเวลาเราจะเรียก service นั้นเราจะเรียกด้วยชื่อครับ โดยชื่อ service นั้นมีหลักการตั้งแบบนี้

1
<serviceName>.<namespaceName>.svc.cluster.local
  • serviceName คือชื่อ service ที่เราต้องการยิงไปหา
  • namespaceName คือชื่อ namespace ที่ service นั้นอยู่ ( ตอนนี้เรายังไม่ได้กำหนด namespace ดังนั้น namespace ของเราคือ default )

สมมติถ้าเราจะยิงไป service : nginx-service เราจะต้องยิงไปที่

1
nginx-service.default.svc.cluster.local

ด้วยวิธีนี้เราก็ไม่จำเป็นต้องไปหา IP จากการ describe service เพื่อดู ip แค่เรารู้ชื่อ service กับ namespace เพียงเท่านี้ก็สามารถยิงไปหา service ที่ต้องการได้แล้ว

เครื่อง Client ที่อยู่ข้างนอกจะยิงเข้ามายังไง

ที่เราทดลองไปทั้งหมดนั้น client ที่เราทดลองอยู่ภายใน k8s แต่ถ้า client อยู่นอก k8s แล้ว เราจะยิงเข้าไปได้ยังไง

เราลองจำลองตัวเองเป็น client ที่อยู่นอก k8s สิ่งที่ client รู้เกี่ยวกับ app มีเพียงอย่างเดียวคือ เครื่องที่ run k8s อยู่มี IP อะไร ( ของผมคือ 192.168.156.101 )

คราวนี้โดยปกติแล้วเวลา 2 เครื่องติดต่อกันในระดับ Application layer นั้นจะต้องระบุ IP และ Port ดังนั้น ถ้าเราสามารถ Map port ไปหา Service ของ k8s ได้ เราก็สามารถยิงจากด้านนอก k8s เข้าไป pod ที่อยู่ใน k8s ได้ ซึ่ง k8s ก็ได้เปิดช่องทางนี้ให้กับเราโดยสร้าง Service type ชนิด NodePort ขึ้นมาให้กับเรา

ลองสร้าง ServiceType : NodePort

สร้างไฟล์ : service-4-test-node-port.yml จากนั้นสั่ง kubeclt apply และสั่ง kubectl describe ดูรายละเอียด

1
2
3
4
5
6
7
8
9
10
11
12
13
apiVersion: v1
kind: Service
metadata:
name: test-node-port-service
spec:
selector:
app: nginx
ports:
- protocol: TCP
port: 80
targetPort: 80
nodePort: 32010
type: NodePort

จะเห็นว่ามีกันเพื่ม NodePort 32010 ตาม config nodePort ที่เพิ่มเข้ามา และ Type เปลี่ยนเป็น NodePort ตามใน Config ไฟล์

คราวนี้ให้เราเปิด Browser ขึ้นมาและเปิดไปที่ url : http://<ip เครื่องที่ run k8s>:32010 โดยของผมเครื่อง run คือ 192.168.156.101 url จึงเป็น http://192.168.56.101:32010

ซึ่งเมื่อลองยิงไปจะเห็นว่าเราจะได้รับ Response กลับมาจาก nginx-pod ซึ่งนั่นทำให้เห็นว่าเราสามารถเชื่อมต่อกับ nginx-pod ผ่าน Service ที่เราพึ่งทำขึ้นมาได้ โดยเชื่อมต่อผ่าน Port : 32010

Service type

จากส่วนที่ผ่านมาเราจะเห็นว่าเรามีความต้องการใช้ Service ในหลายความต้องการ เช่น เรียกด้วยชื่อ service , เรียกจาก client ที่อยู่นอก cluster ดังนั้นเพื่อตอบสนองความต้องการที่หลากหลายจึงมีการกำหนด Service type เพื่อให้ระบุว่า Service นี้ต้องทำงานแบบไหน

จริงๆมีคนเขียนเกี่ยวกับ Service type ไว้ดีมากพร้อมมีภาพที่ทำให้เข้าใจง่าย ซึ่งผมเชื่อว่าดีกว่าผมอธิบาย คุณสามารถอ่านได้ที่ : https://thanwa.medium.com/kubernetes-%E0%B8%A7%E0%B9%88%E0%B8%B2%E0%B8%94%E0%B9%89%E0%B8%A7%E0%B8%A2%E0%B9%80%E0%B8%A3%E0%B8%B7%E0%B9%88%E0%B8%AD%E0%B8%87-services-%E0%B9%81%E0%B8%95%E0%B9%88%E0%B8%A5%E0%B8%B0%E0%B8%9B%E0%B8%A3%E0%B8%B0%E0%B9%80%E0%B8%A0%E0%B8%97-25bade6d4725

โดยในบทความนี้จะพูดถึงแค่ 2 Service type คือ ClusterIP กับ NodePort เท่านั้น ส่วน Service type อื่นๆผมไม่ขอพูดถึงเพราะว่า เราไม่สามารถทดลองใช้งานได้ง่ายๆและน่าจะยังไม่มีตัวอย่างที่เหมาะสมในการแสดงให้ดู

ClusterIP

Service Type ประเภทนี้จะอนุญาตให้เข้าถึงได้เฉพาะสิ่งที่อยู่ใน Cluster ของ k8s เท่านั้น จะเห็นว่าตอนเราสร้าง Service Type ด้วย ClusterIP เราจะต้องใช้ client-pod ที่อยู่ใน cluster ของ k8s ในการทดลองยิง request หากเราลองเปิด browser ของเครื่องเรา แล้วใส่ IP ของ Service แล้วยิงไปจะเห็นว่าไม่สามารถยิงไปหาได้

NodePort

Service Type ประเภทนี้จะอนุญาตให้เครื่องที่อยู่ภายนอกสามารถเชื่อมต่อกับ Pod (ผ่าน Service) ผ่าน IP ของเครื่องที่ Run k8s และ Port จากในตัวอย่างที่เราจะเห็นว่าเราสามารถใช้งาน nginx-pod ผ่าน browser โดยยิงไปที่ url : http://<ip เครื่องที่ run k8s>:32010

  • ข้อควรระวัง : ตัว NodePort มีข้อจำกัดที่จะใช้ Port ได้แค่ Port : 30000-32767 เท่านั้น

สรุป

สำหรับตอนนี้เราได้เรียนรู้เกี่ยวกับ Service รู้ที่มาที่ไปของมัน วิธีการสร้าง Service ผ่าน Configuration file ความแตกต่างระหว่าง Service type : ClusterIP กับ NodePort ส่วนในตอนต่อไปเราจะมีเรียนรู้เกี่ยวกับ Volume กัน