Kubernetes 如何將 container 相互連接? 假設在多台機器的環境中,每一台機器都安裝了 Docker,要讓這些起在這些機器上的 container 相互連結,若僅依賴 Docker 本身所提供的網路功能的話,就只能透過 HOST_IP:PORT
的方式來提供 container 相互溝通的能力;這樣的方式在同一個 Host 上所使用的 port 是不能重複的,這種方法在大型 or 多人的的環境上是不可行的,要與每個使用者溝通協調可以使用的 port 幾乎是不可能的事情。
因此在 k8s 中提出了 pod 的概念,並且透過額外建立 overlay network 的方式讓每個 pod 擁有自己的 IP(cluster private IP),且可以相互溝通,當然也沒有 port conflict 的問題。
k8s 透過了 pod & overlay network 解決了 container 在不同 Host 之間的溝通問題,但還有另外一個問題,就是 pod IP 並不會固定,可能因為 container 重啟的關係而導致 IP 變更,此時其他 Application 就有可能無法透過原本的 IP 連線到該 pod,此時怎辦?
在 k8s 就額外了提供 Service
resource object,透過 DNS + Pod Load Balancer
的概念,來解決這個問題。
開放 Pod 在 Cluster 內部存取 首先透過以下的 YAML 定義 建立一個搭配兩個(replica=2) nginx Pod 的 Deployment:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 apiVersion: apps/v1 kind: Deployment metadata: name: my-nginx spec: selector: matchLabels: run: my-nginx replicas: 2 template: metadata: labels: run: my-nginx spec: containers: - name: my-nginx image: nginx ports: - containerPort: 80
套用了以上設定後,可以檢視一下目前的 Pod 清單:
1 2 3 4 $ kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE my-nginx-756f645cd7-dwzrp 1/1 Running 0 15s 10.233.76.12 leon-k8s-node05 <none> my-nginx-756f645cd7-zp8gq 1/1 Running 0 15s 10.233.103.203 leon-k8s-node04 <none>
可以看到每個 pod 都已經取得 IP,然後在任何一個 node 執行 curl 應該都可以得到類似以下結果:
1 2 3 4 5 6 7 8 $ curl http://10.233.76.12 <!DOCTYPE html> <html> <head > <title>Welcome to nginx!</title> ....(略) </body> </html>
建立 Service 透過上一個部份,有了兩個在 cluster 內部都可以存取到的 pod,正常情況下,透過 pod IP 存取都是沒什麼問題的,但是若是 pod 因為某些原因重啟了(例如:node 掛掉)而造成 IP 變更時,那原本的 IP 就存取不到了,那怎麼辦?
於是 k8s 就提供了 Service 來解決這個問題,透過將 pod IP 這一層抽象化,讓直接對外提供服務的不是 pod 本身而已 service,且搭配內部的 DNS service,讓 cluster 內部的其他 application 可以簡單的透過 domain name 來存取 service,而 service 會將 network traffic 平均的分流到 pod member 中。
此外,service 有些特性也需要注意一下:
那如何建立一個 service 呢? 搭配上面的 pod,可以套用以下設定 :
1 2 3 4 5 6 7 8 9 10 11 12 apiVersion: v1 kind: Service metadata: name: my-nginx labels: run: my-nginx spec: ports: - port: 80 protocol: TCP selector: run: my-nginx
也可以直接透過 expose 指令來處理:(expose 指令會協助新增 service)
kubectl expose deployment/my-nginx
接著來檢視一下建立 service 後的狀況:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 $ kubectl get svc/my-nginx NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE my-nginx ClusterIP 10.233.12.73 <none> 80/TCP 19s $ kubectl get svc/my-nginx NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE my-nginx ClusterIP 10.233.12.73 <none> 80/TCP 19s root@leon-k8s-node00:/srv/k8s/connect_app_with_svc Name: my-nginx Namespace: default Labels: run=my-nginx Annotations: ... (略) Selector: run=my-nginx Type: ClusterIP IP: 10.233.12.73 Port: <unset > 80/TCP TargetPort: 80/TCP Endpoints: 10.233.103.203:80,10.233.76.12:80 Session Affinity: None Events: <none>
此時若是 curl http://[CLUSTER_IP]:[PORT] 也會得到跟上一個部份中一樣的結果
存取 Service 在 k8s cluster 中要存取 service 有兩種主要方式,分別是:
其中 Environment Variable
已經是內建的,而 DNS 的部份則是必須要安裝 addon 才會有,以下來看看兩種不同的方法是如何存取 service。
Environment Variable(環境變數) 當一個 pod 被建立時,kubelet 就會自動的增加一些與 service 相關的環境變數進到 pod 中,但這樣的作法其實造成了一些問題,為什麼這樣說呢? 以下做個簡單的實驗:
首先檢視一下在目前 pod 中的環境變數資訊:
1 2 3 4 $ kubectl exec my-nginx-756f645cd7-dwzrp -- printenv | grep SERVICE KUBERNETES_SERVICE_HOST=10.233.0.1 KUBERNETES_SERVICE_PORT=443 KUBERNETES_SERVICE_PORT_HTTPS=443
大家應該會發現,上面這個 IP(10.233.0.1
) 並不是屬於這個 pod 的 service 的 IP,但如果真的去 curl 這個 IP:Port 會得到以下結果:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 $ curl https://10.233.0.1 { "kind" : "Status" , "apiVersion" : "v1" , "metadata" : { }, "status" : "Failure" , "message" : "forbidden: User \"system:anonymous\" cannot get path \"/\"" , "reason" : "Forbidden" , "details" : { }, "code" : 403 }
會造成這樣的原因是因為這些 pod 建立的時間比 service 還要早,因此 kubelet 在填入環境變數的資訊時,並沒有 service 的訊息可以填入,因此才改填入 --service-cluster-ip-range
設定中的第一個 IP 來替代。
那要怎麼樣讓環境變數可以變成正確的值呢? 很簡單,只要把 pod 砍了再重新產生即可。
由於這些 pod 是由 Deployment 管理,因此可以透過將 Deployment 中的 replica 設定變為 0,再改回 2,這樣就可以讓 pod 重新產生,取得正確的 service 環境變數設定:
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 $ kubectl scale deployment/my-nginx --replicas=0 deployment.extensions/my-nginx scaled $ kubectl get deployment/my-nginx NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE deployment.apps/my-nginx 0 0 0 0 24h $ kubectl scale deployment/my-nginx --replicas=2 deployment.extensions/my-nginx scaled $ kubectl get pod -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE my-nginx-756f645cd7-cpvw6 1/1 Running 0 5m37s 10.233.76.13 leon-k8s-node05 <none> my-nginx-756f645cd7-vvmll 1/1 Running 0 5m37s 10.233.103.204 leon-k8s-node04 <none> $ kubectl exec my-nginx-756f645cd7-cpvw6 -- printenv | grep SERVICE MY_NGINX_SERVICE_HOST=10.233.12.73 MY_NGINX_SERVICE_PORT=80 KUBERNETES_SERVICE_HOST=10.233.0.1 KUBERNETES_SERVICE_PORT=443 KUBERNETES_SERVICE_PORT_HTTPS=443
從上面的實驗可以看出,在 service 建立之後才產生的 pod,就會取得正確的環境變數了。
DNS DNS 則是 addon,不會預設安裝進 k8s 中,但目前普遍 k8s 安裝相關的專案都會協助安裝 DNS service(kube-dns or CoreDNS),我們可以透過以下指令檢視:
1 2 3 4 $ kubectl -n kube-system get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE coredns ClusterIP 10.233.0.3 <none> 53/UDP,53/TCP,9153/TCP 35d kubernetes-dashboard ClusterIP 10.233.22.60 <none> 443/TCP 35d
從上面可以看出來,我的 k8s cluster 中的 DNS service 是 CoreDNS,並非 kube-dns。
接著以下要同時檢視之前建立的 my-nginx service 是否可以正確的在 cluster 內部解析:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 $ kubectl get svc NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE kubernetes ClusterIP 10.233.0.1 <none> 443/TCP 35d my-nginx ClusterIP 10.233.12.73 <none> 80/TCP 24h $ kubectl run curl --image=radial/busyboxplus:curl -i --tty [ root@curl-5cc7b478b6-29bjq:/ ]$ cat /etc/resolv.conf nameserver 10.233.0.3 search default.svc.cluster.local svc.cluster.local cluster.local qct.io options ndots:5 [ root@curl-5cc7b478b6-29bjq:/ ]$ nslookup my-nginx Server: 10.233.0.3 Address 1: 10.233.0.3 coredns.kube-system.svc.cluster.local Name: my-nginx Address 1: 10.233.12.73 my-nginx.default.svc.cluster.local
如何更安全的存取 Service 以下示範如何設定一個名稱為 my-secure-nginx
的 HTTPS service。
產生 TLS certificate & 建立 Secret 首先產生 TLS certificate,並取得 Base64 編碼結果:(記得 TLS certificate 的 domain 要設定為 my-secure-nginx
)
1 2 3 4 5 6 7 8 $ openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /tmp/nginx.key -out /tmp/nginx.crt -subj "/CN=my-secure-nginx/O=my-secure-nginx" $ cat /tmp/nginx.crt | base64 LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS ... (略) $ cat /tmp/nginx.key | base64 LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS ... (略)
套用以下 YAML 設定,建立一個 Secret resource object:
1 2 3 4 5 6 7 apiVersion: "v1" kind: "Secret" metadata: name: "my-secure-nginx-secret" data: nginx.crt: "LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS ... (略)" nginx.key: "LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS ... (略)"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 $ kubectl get secret/my-secure-nginx-secret NAME TYPE DATA AGE my-secure-nginx-secret Opaque 2 30s $ kubectl get secret/my-secure-nginx-secret -o yaml apiVersion: v1 data: nginx.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS ... (略) nginx.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS ... (略) kind: Secret metadata: ... (略) creationTimestamp: 2018-11-18T19:54:17Z name: my-secure-nginx-secret namespace: default resourceVersion: "6164328" selfLink: /api/v1/namespaces/default/secrets/my-secure-nginx-secret uid: bb87fa8e-eb6b-11e8-890b-66712a0dd587 type : Opaque
新增 nginx SSL 相關設定 在 k8s 中,一般我們會以 ConfigMap 的方式來作為處理設定檔的方式,因此套用以下的 YAML 設定來新增 nginx 設定檔:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 apiVersion: v1 kind: ConfigMap metadata: name: my-secure-nginx-config data: default.conf: | server { listen 80 default_server; listen [::]:80 default_server ipv6only=on; listen 443 ssl; root /usr/share/nginx/html; index index.html; server_name localhost; ssl_certificate /etc/nginx/ssl/nginx.crt; ssl_certificate_key /etc/nginx/ssl/nginx.key; location / { try_files $uri $uri/ =404; } }
1 2 3 4 kubectl get configmap/my-secure-nginx-config NAME DATA AGE my-secure-nginx-config 1 11s
新增 Deployment & Service 最後要新增 Deployment & Service 之前,要先確認以下幾件事情:
secret 已經新增,名稱為 my-secure-nginx-secret
,裡面的檔案為 nginx.crt
& nginx.key
ConfigMap 已經新增,名稱為 my-secure-nginx-config
,作為 nginx 的設定,檔名為 default.conf
nginx default 設定檔位置為 /etc/nginx/conf.d/default.conf
(檔名與 ConfigMap 中的 data 設定相同)
nginx 中預設存放 certificate 的路徑為 /etc/nginx/ssl
,檔名為 nginx.crt
& nginx.key
(與 secret 中的 data 設定相同)
確認了以上幾個要項之後,就可以套用以下設定來建立 Deployment & Service:
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 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 --- apiVersion: v1 kind: Service metadata: name: my-secure-nginx labels: run: my-secure-nginx spec: type: NodePort ports: - port: 80 protocol: TCP name: http - port: 443 protocol: TCP name: https selector: run: my-secure-nginx --- apiVersion: apps/v1 kind: Deployment metadata: name: my-secure-nginx spec: selector: matchLabels: run: my-secure-nginx replicas: 1 template: metadata: labels: run: my-secure-nginx spec: volumes: - name: secret-volume secret: secretName: my-secure-nginx-secret - name: configmap-volume configMap: name: my-secure-nginx-config containers: - name: nginxhttps image: ymqytw/nginxhttps:1.5 command: ["/home/auto-reload-nginx.sh" ] ports: - containerPort: 443 - containerPort: 80 livenessProbe: httpGet: path: /index.html port: 80 initialDelaySeconds: 30 timeoutSeconds: 1 volumeMounts: - mountPath: /etc/nginx/ssl name: secret-volume - mountPath: /etc/nginx/conf.d name: configmap-volume
從外部驗證 HTTPS 服務 套用以上設定後,檢視一下目前系統狀態:
1 2 3 4 5 6 7 8 9 10 11 12 13 $ kubectl get all NAME READY STATUS RESTARTS AGE pod/my-secure-nginx-66646484d-hwrkr 1/1 Running 0 10s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/my-secure-nginx NodePort 10.233.43.113 <none> 80:30779/TCP,443:31770/TCP 10s NAME DESIRED CURRENT UP-TO-DATE AVAILABLE AGE deployment.apps/my-secure-nginx 1 1 1 1 10s NAME DESIRED CURRENT READY AGE replicaset.apps/my-secure-nginx-66646484d 1 1 1 10s
由於 service 設定為 NodePort 的關係,以下測試可以從外面來進行:
1 2 3 4 5 6 7 8 9 10 $ curl http://10.107.13.10:30779 ... (略) <h1>Welcome to nginx!</h1> $ curl -k https://10.107.13.10:31770 ... (略) <h1>Welcome to nginx!</h1>
從內部驗證 HTTPS 服務 若從內部驗證,我們就可以使用已經存在的 TLS certificate 來進行驗證,首先套用以下設定:
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 28 apiVersion: apps/v1 kind: Deployment metadata: name: curl-deployment spec: selector: matchLabels: app: curlpod replicas: 1 template: metadata: labels: app: curlpod spec: volumes: - name: secret-volume secret: secretName: my-secure-nginx-secret containers: - name: curlpod command: - sh - -c - while true ; do sleep 1 ; done image: radial/busyboxplus:curl volumeMounts: - mountPath: /etc/nginx/ssl name: secret-volume
接著取得 pod 的名稱並進行驗證:
1 2 3 4 5 6 7 8 9 10 $ kubectl get pods -l app=curlpod NAME READY STATUS RESTARTS AGE curl-deployment-78959f7dcc-4csbk 1/1 Running 0 13s $ kubectl exec curl-deployment-78959f7dcc-4csbk -- curl https://my-secure-nginx --cacert /etc/nginx/ssl/nginx.crt ... (略) <h1>Welcome to nginx!</h1> ... (略)
看到 Welcome to nginx! 且沒有憑證相關問題的警告就沒錯啦!
References