CKAD 學習筆記 - 服務(service)

服務,用於串通客戶端及服務端的橋樑

Service 同其名稱,用來將某些常駐程式宣告為服務,讓其他的用戶端、甚至外部的使用者可以更輕鬆的存取資源。

在一般狀況下,pod 多會放在 kubernetes 內自有的網路中,因此我們無法直接把請求送入 pod 中;設想我們可能會有自己的工作機及 node 在同一個網路中,但即使如此,因為 pod 本身只有一個 10.x.x.x 的內網,這讓我們無法直接將 request 打去 pod 裡面:

k8s-and-laptop.png

在沒有 service 的時候,若我們需要去將請求送進去 pod 裡面,則會需要 ssh tunnel——即 ssh 到 node 上、然後從 node 發送請求:

access-pod-by-ssh-tunnel.png

當然這不會是一個合理的解決方案,在 kubernetes 上我們大概率會有某些 pod 是在提供 API 服務、甚至本身就是網頁前端,讓所有的使用者去開 tunnel 鐵定是個詭異的方案。

因此我們需要 service,當一個 service 被部署後,它會在 node 上建立 port forwarding 的規則,讓我們可以從外部直接將請求送到指定的 pod 中:

access-pod-by-service.png

透過這樣的機制外部的用戶端在知道這個開放的 port 的情況下,就可以接觸到目標的 pod 了。當然這只是 service 的其中一種用途,後面做更多說明。

service 的類別

kubernetes 有以下幾個 service 類別:

  1. Cluster IP:在 cluster 內開個靜態的虛擬 IP,供其他的 client 連接用;此為 service 預設的類別
  2. Node port:基於 cluster ip service,不過會在 node 上開個 port 對外
  3. Load balancer:把某組 pod 直接公開出去、接上 cloud provider 們提供的負載平衡器使用
  4. External name:建立 CNAME 紀錄,讓 DNS 直接將其解析為另一個位址

Cluster IP

在常見的架構下,我們可能會有多隻前端的 pod、多隻後端的 pod,這時候會衍生一個問題:當一個後端的 pod 死亡或新生的時,要怎麼讓前端的 pod 知道要避免去把請求往這邊打(或是開始往新生的 pod 打),那麼我們就會用到 cluster IP service。

cluster-ip.png

service 會在叢集內建立一個網路介面,故對於叢集內的其他服務而言,它們除了直接把請求往目標(以上圖為例,API pod 們)打以外,也可以將請求送往 service(以上圖為例,10.1.100.3),而 service 會將請求隨機分配給其中一個後方的 pod 接手。

所以在這樣的案例中,前端的 pod 只要將請求送到 service 上,再由 service 去將請求分發給後端的 pod,那我們就可以避免面對 service discovery 的問題。

另外補充一點,kubernetes 會為 service 建立 DNS 紀錄,故在上述的情境中 app pod 送請求的目標通常不是使用 IP 而會是對應的 hostname。

要建立起 service 會包含兩個部分,一個 kind: Service 的 resource file、及對應的 pod。首先是 resource file 的部分:

apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: ClusterIP # 實際上可以不用填,因為 cluster ip 是 service 的預設值
selector:
app.kubernetes.io/name: MyApp
ports:
- port: 8080
# target port 沒有給的時候它會設定為 `port` 的值
targetPort: 80

在這個定義檔裡面它會將 service 8080 port 接收到的請求轉給具有 app.kubernetes.io/name: MyApp 這套標籤的 pod 的 80 port。

補充一點名詞解釋,關於這裡遇到 porttargetPort 兩個名詞:

  • port 是聲明這個 service 在接收請求的 port
  • targetPort 是後方的 pod 接收請求的 port

不過在一些不會那麼複雜的服務中,我們很常見將 porttargetPort 設為一樣的值,近一步地、如果 targetPort 沒寫,那它的預設值就是 port。另外,如果 container 有聲明 containerPort 並為其命名,則 service 上的 targetPort 可以使用該名稱。

例如今天的 pod 定義為:

apiVersion: v1
kind: Pod
metadata:
name: nginx
labels:
app.kubernetes.io/name: MyApp
spec:
containers:
- name: nginx
image: nginx:stable
ports:
- containerPort: 80
name: http-web-svc

它聲明了開放 80 port,並為其命名為 http-web-svc。那麽此時 service 那端的 targetPort 就可以使用這個名稱來代替硬刻的數字。

ports:
- port: 80
targetPort: http

另外有查到一個有趣的文件,在 container v1 core definition 中提及 containerPort 算是一個用作文件化的參數而已,kubernetes 現行的設計上並不會阻止叢集裡面的服務去接觸其他 pod 自己有對全域(0.0.0.0)公開的 port。不過這段文字並非毫無用處,在我們有在 containers 這端定義 containerPort 的情況下,service 內的 port/targetPort 可用 name 呈現,即:

Node port

承前面所述,node port 會在 node 上開出對外的 port 供外部的使用者連接。故這邊先做幾個名詞解釋:

node-port.png

上面的舉例來說它開放了 nodePort 30080,則外部使用者可以把請求送去 192.168.1.110(node 對外的位址)上的 30080 port 而 service 會幫忙把請求 forward 給 pod 上的 targetPort 80。

這邊有個有趣的設定,在 node port 啟用的情況下,service 預設會在每個 node 有連接的所有網路上都開放這個 port,若不想要這個行為,則需要去設定 kube-proxy 的選項 nodePortAddresses

另外,node port service 有繼承 cluster IP service 的特性,service 會在叢集內建立一個網路介面,故對於叢集內的其他服務而言,它們除了可以直接把請求送去目標(10.224.0.2)以外,也可以將請求送去 service(10.96.0.1);同樣地,如果 service 上的 porttargetPort 不同時,那麼請求必須送到 port 上。

一個使用 node port 的範例如下:

apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: NodePort
selector:
app.kubernetes.io/name: MyApp
ports:
- port: 80
# target port 沒有給的時候它會設定為 `port` 的值
targetPort: 80
# node port 是要連接到的對外的 port,注意這個值必須落在 [30000, 32767] 間
# 並且如果沒有給,則 k8s 會從有效範圍內選一個賦予
nodePort: 30080

Load balancer

這個種類基本上就是讓你去把 service 接上 cloud service provider 提供的各類外部 load balancer 用的。

而這個功能基本上就是 node port、加上幾個 API 溝通讓那些外部的 load balancer 可以找到這個 service 對應的 port。

External name

這個 service 類別跟上述幾個的行為可以說是幾乎不一樣,它並不是在 service 內建立起一個網路的介面,而是建立起一個 DNS CNAME record。

舉例而言,今天我們有一個這樣的 service:

apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
type: ExternalName
externalName: database.example.com

則當 client 使用 hostname 對 my-service 發送請求時、DNS 將會解析到 CNAME 為 database.example.com,再進而將請求發到那邊去。

補充:external IP

無論使用哪一種 service 類別,還有一個選項可以參在一起使用—— externalIPs

它的使用情境是將這個 service 直接綁到某個 node 所能吃到的 IP 上:

apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app.kubernetes.io/name: MyApp
ports:
- name: http
protocol: TCP
port: 80
externalIPs:
- 80.11.12.10

在這個舉例中,若 node 本身是 80.11.12.10 會路由到的目標,那麼送往 80.11.12.10:80 的請求就會接上這支 service,並讓 MyApp 去接收到請求。

沒有 selector 的 service

雖然說前面的介紹都講說要用 port 跟 selector 搭配來寫 service,但實際上還有幾種情境下 service 是會沒有 selector 的,如前面說的 ExternalName,另一個則是 EndpointSlices

EndpointSlices 是個 Kubernetes v1.21 起支援的功能,可以將請求直接導到特定的 endpoint 去,可應用於目標並不存在於同個叢集內的情境。

本文中 resource file 皆取自官方文件:2022 The Kubernetes Authors | Documentation Distributed under CC BY 4.0